From 667f9257ae4153870bfc06d771e89d00bfe70567 Mon Sep 17 00:00:00 2001 From: Shiv Tyagi Date: Fri, 7 Feb 2025 21:35:22 +0530 Subject: [PATCH 01/55] db: add Disabled property to UserRow struct --- internal/users/db/sql/create_schema.sql | 3 ++- internal/users/db/users.go | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/internal/users/db/sql/create_schema.sql b/internal/users/db/sql/create_schema.sql index 35351d6d64..23dc324895 100644 --- a/internal/users/db/sql/create_schema.sql +++ b/internal/users/db/sql/create_schema.sql @@ -5,7 +5,8 @@ CREATE TABLE IF NOT EXISTS users ( gecos TEXT DEFAULT "", dir TEXT DEFAULT "", shell TEXT DEFAULT "/bin/bash", - broker_id TEXT DEFAULT "" + broker_id TEXT DEFAULT "", + disabled BOOLEAN DEFAULT FALSE ); CREATE UNIQUE INDEX "idx_user_name" ON users ("name"); diff --git a/internal/users/db/users.go b/internal/users/db/users.go index 53218350c6..0fcd84d889 100644 --- a/internal/users/db/users.go +++ b/internal/users/db/users.go @@ -10,9 +10,9 @@ import ( "github.com/ubuntu/authd/log" ) -const allUserColumns = "name, uid, gid, gecos, dir, shell, broker_id" -const publicUserColumns = "name, uid, gid, gecos, dir, shell, broker_id" -const allUserColumnsWithPlaceholders = "name = ?, uid = ?, gid = ?, gecos = ?, dir = ?, shell = ?, broker_id = ?" +const allUserColumns = "name, uid, gid, gecos, dir, shell, broker_id, disabled" +const publicUserColumns = "name, uid, gid, gecos, dir, shell, broker_id, disabled" +const allUserColumnsWithPlaceholders = "name = ?, uid = ?, gid = ?, gecos = ?, dir = ?, shell = ?, broker_id = ?, disabled = ?" // UserRow represents a user row in the database. type UserRow struct { @@ -25,6 +25,8 @@ type UserRow struct { // BrokerID specifies the broker the user last successfully authenticated with. BrokerID string `yaml:"broker_id,omitempty"` + + Disabled bool `yaml:"disabled,omitempty"` } // NewUserRow creates a new UserRow. @@ -49,7 +51,7 @@ func userByID(db queryable, uid uint32) (UserRow, error) { row := db.QueryRow(query, uid) var u UserRow - err := row.Scan(&u.Name, &u.UID, &u.GID, &u.Gecos, &u.Dir, &u.Shell, &u.BrokerID) + err := row.Scan(&u.Name, &u.UID, &u.GID, &u.Gecos, &u.Dir, &u.Shell, &u.BrokerID, &u.Disabled) if errors.Is(err, sql.ErrNoRows) { return UserRow{}, NewUIDNotFoundError(uid) } @@ -73,7 +75,7 @@ func userByName(db queryable, name string) (UserRow, error) { row := db.QueryRow(query, name) var u UserRow - err := row.Scan(&u.Name, &u.UID, &u.GID, &u.Gecos, &u.Dir, &u.Shell, &u.BrokerID) + err := row.Scan(&u.Name, &u.UID, &u.GID, &u.Gecos, &u.Dir, &u.Shell, &u.BrokerID, &u.Disabled) if errors.Is(err, sql.ErrNoRows) { return UserRow{}, NewUserNotFoundError(name) } @@ -100,7 +102,7 @@ func allUsers(db queryable) ([]UserRow, error) { var users []UserRow for rows.Next() { var u UserRow - err := rows.Scan(&u.Name, &u.UID, &u.GID, &u.Gecos, &u.Dir, &u.Shell, &u.BrokerID) + err := rows.Scan(&u.Name, &u.UID, &u.GID, &u.Gecos, &u.Dir, &u.Shell, &u.BrokerID, &u.Disabled) if err != nil { return nil, fmt.Errorf("scan error: %w", err) } @@ -153,8 +155,8 @@ func userExists(db queryable, u UserRow) (bool, error) { // insertUser inserts a new user into the database. func insertUser(db queryable, u UserRow) error { log.Debugf(context.Background(), "Inserting user %v", u.Name) - query := fmt.Sprintf(`INSERT INTO users (%s) VALUES (?, ?, ?, ?, ?, ?, ?)`, allUserColumns) - _, err := db.Exec(query, u.Name, u.UID, u.GID, u.Gecos, u.Dir, u.Shell, u.BrokerID) + query := fmt.Sprintf(`INSERT INTO users (%s) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, allUserColumns) + _, err := db.Exec(query, u.Name, u.UID, u.GID, u.Gecos, u.Dir, u.Shell, u.BrokerID, u.Disabled) if err != nil { return fmt.Errorf("insert user error: %w", err) } @@ -165,7 +167,7 @@ func insertUser(db queryable, u UserRow) error { func updateUserByID(db queryable, u UserRow) error { log.Debugf(context.Background(), "Updating user %v", u.Name) query := fmt.Sprintf(`UPDATE users SET %s WHERE uid = ?`, allUserColumnsWithPlaceholders) - _, err := db.Exec(query, u.Name, u.UID, u.GID, u.Gecos, u.Dir, u.Shell, u.BrokerID, u.UID) + _, err := db.Exec(query, u.Name, u.UID, u.GID, u.Gecos, u.Dir, u.Shell, u.BrokerID, u.Disabled, u.UID) if err != nil { return fmt.Errorf("update user error: %w", err) } From 03edd77349839e9a3accf5278cab2e092604e388 Mon Sep 17 00:00:00 2001 From: Shiv Tyagi Date: Fri, 7 Feb 2025 21:36:23 +0530 Subject: [PATCH 02/55] users: add method to check if a user is disabled --- internal/users/manager.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/internal/users/manager.go b/internal/users/manager.go index 232691f796..fe70d1d101 100644 --- a/internal/users/manager.go +++ b/internal/users/manager.go @@ -468,6 +468,16 @@ func (m *Manager) UpdateBrokerForUser(username, brokerID string) error { return nil } +// IsUserDisabled returns true if the user with the given user name is disabled, false otherwise. +func (m *Manager) IsUserDisabled(username string) (bool, error) { + u, err := m.db.UserByName(username) + if err != nil { + return false, err + } + + return u.Disabled, nil +} + // UserByName returns the user information for the given user name. func (m *Manager) UserByName(username string) (types.UserEntry, error) { usr, err := m.db.UserByName(username) From 65485870d5f3b2f3997e57749ee0b7cdb3bc677c Mon Sep 17 00:00:00 2001 From: Shiv Tyagi Date: Fri, 7 Feb 2025 21:36:47 +0530 Subject: [PATCH 03/55] pam: check if the user is disabled before creating session --- internal/services/pam/pam.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/services/pam/pam.go b/internal/services/pam/pam.go index e22bf2c1aa..2b45b467e6 100644 --- a/internal/services/pam/pam.go +++ b/internal/services/pam/pam.go @@ -144,6 +144,15 @@ func (s Service) SelectBroker(ctx context.Context, req *authd.SBRequest) (resp * lang = "C" } + userIsDisabled, err := s.userManager.IsUserDisabled(username) + if err != nil && !errors.Is(err, users.NoDataFoundError{}) { + return nil, fmt.Errorf("could not check if user %q is disabled: %w", username, err) + } + // Throw an error if the user trying to authenticate already exists in the database and is disabled + if err == nil && userIsDisabled { + return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("user %s is disabled", username)) + } + var mode string switch req.GetMode() { case authd.SessionMode_LOGIN: From 1eaee8f60950032b72957d8adb8c7d5f89cc310a Mon Sep 17 00:00:00 2001 From: Shiv Tyagi Date: Sat, 15 Feb 2025 23:22:36 +0530 Subject: [PATCH 04/55] pam: add test case Error_when_user_is_disabled while selecting broker --- internal/services/pam/pam_test.go | 14 +++++++++++++- .../TestSelectBroker/cache-with-disabled-user.db | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 internal/services/pam/testdata/TestSelectBroker/cache-with-disabled-user.db diff --git a/internal/services/pam/pam_test.go b/internal/services/pam/pam_test.go index d773173573..dd7673c9b3 100644 --- a/internal/services/pam/pam_test.go +++ b/internal/services/pam/pam_test.go @@ -193,6 +193,7 @@ func TestSelectBroker(t *testing.T) { brokerID string username string sessionMode string + existingDB string currentUserNotRoot bool @@ -209,13 +210,24 @@ func TestSelectBroker(t *testing.T) { "Error_when_broker_does_not_exist": {username: "no broker", brokerID: "does not exist", wantErr: true}, "Error_when_broker_does_not_provide_a_session_ID": {username: "ns_no_id", wantErr: true}, "Error_when_starting_the_session": {username: "ns_error", wantErr: true}, + "Error_when_user_is_disabled": {username: "disabled", wantErr: true, existingDB: "cache-with-disabled-user.db"}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { t.Parallel() + cacheDir := t.TempDir() + if tc.existingDB != "" { + err := db.Z_ForTests_CreateDBFromYAML(filepath.Join(testutils.TestFamilyPath(t), tc.existingDB), cacheDir) + require.NoError(t, err, "Setup: could not create database from testdata") + } + + m, err := users.NewManager(users.DefaultConfig, cacheDir) + require.NoError(t, err, "Setup: could not create user manager") + t.Cleanup(func() { _ = m.Stop() }) + pm := newPermissionManager(t, tc.currentUserNotRoot) - client := newPamClient(t, nil, globalBrokerManager, &pm) + client := newPamClient(t, m, globalBrokerManager, &pm) switch tc.brokerID { case "": diff --git a/internal/services/pam/testdata/TestSelectBroker/cache-with-disabled-user.db b/internal/services/pam/testdata/TestSelectBroker/cache-with-disabled-user.db new file mode 100644 index 0000000000..4155b941b4 --- /dev/null +++ b/internal/services/pam/testdata/TestSelectBroker/cache-with-disabled-user.db @@ -0,0 +1,16 @@ +users: + - name: testselectbroker/error_when_user_is_disabled_separator_disabled + uid: 1111 + gid: 11111 + gecos: gecos for other user + dir: /home/disabled + shell: /bin/bash + broker_id: broker-id + disabled: true +groups: + - name: group1 + gid: 11111 + ugid: ugid +users_to_groups: + - uid: 1111 + gid: 11111 From 524825806d82f82525570183027e3749a822e32e Mon Sep 17 00:00:00 2001 From: Shiv Tyagi Date: Wed, 19 Feb 2025 01:39:07 +0530 Subject: [PATCH 05/55] internal/users: implement logic to enable/disable user This also adds tests for the same. --- internal/users/db/db_test.go | 14 +++ internal/users/db/update.go | 18 ++++ internal/users/manager.go | 18 ++++ internal/users/manager_test.go | 92 +++++++++++++++++++ .../users/testdata/db/disabled_user.db.yaml | 18 ++++ .../TestDisableUser/Successfully_disable_user | 65 +++++++++++++ .../TestEnableUser/Successfully_enable_user | 18 ++++ 7 files changed, 243 insertions(+) create mode 100644 internal/users/testdata/db/disabled_user.db.yaml create mode 100644 internal/users/testdata/golden/TestDisableUser/Successfully_disable_user create mode 100644 internal/users/testdata/golden/TestEnableUser/Successfully_enable_user diff --git a/internal/users/db/db_test.go b/internal/users/db/db_test.go index d6061cf5c3..7eb56b62be 100644 --- a/internal/users/db/db_test.go +++ b/internal/users/db/db_test.go @@ -842,6 +842,20 @@ func TestUpdateBrokerForUser(t *testing.T) { require.Error(t, err, "UpdateBrokerForUser for a nonexistent user should return an error") } +func TestUpdateDisabledFieldForUser(t *testing.T) { + t.Parallel() + + c := initDB(t, "one_user_and_group") + + // Update broker for existent user + err := c.UpdateDisabledFieldForUser("user1", true) + require.NoError(t, err, "UpdateDisabledFieldForUser for an existent user should not return an error") + + // Error when updating broker for nonexistent user + err = c.UpdateDisabledFieldForUser("nonexistent", false) + require.Error(t, err, "UpdateDisabledFieldForUser for a nonexistent user should return an error") +} + func TestRemoveDb(t *testing.T) { t.Parallel() diff --git a/internal/users/db/update.go b/internal/users/db/update.go index 8b036596f5..34445b5d96 100644 --- a/internal/users/db/update.go +++ b/internal/users/db/update.go @@ -182,3 +182,21 @@ func (m *Manager) UpdateBrokerForUser(username, brokerID string) error { return nil } + +// UpdateDisabledFieldForUser sets the Disabled field to a given value for a user. +func (m *Manager) UpdateDisabledFieldForUser(username string, disabled bool) error { + query := `UPDATE users SET disabled = ? WHERE name = ?` + res, err := m.db.Exec(query, disabled, username) + if err != nil { + return fmt.Errorf("failed to update disabled field for user: %w", err) + } + rowsAffected, err := res.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get rows affected: %w", err) + } + if rowsAffected == 0 { + return NewUserNotFoundError(username) + } + + return nil +} diff --git a/internal/users/manager.go b/internal/users/manager.go index fe70d1d101..064d2aca44 100644 --- a/internal/users/manager.go +++ b/internal/users/manager.go @@ -468,6 +468,24 @@ func (m *Manager) UpdateBrokerForUser(username, brokerID string) error { return nil } +// DisableUser sets the Disabled field to true for the given user. +func (m *Manager) DisableUser(username string) error { + if err := m.db.UpdateDisabledFieldForUser(username, true); err != nil { + return err + } + + return nil +} + +// EnableUser sets the Disabled field to false for the given user. +func (m *Manager) EnableUser(username string) error { + if err := m.db.UpdateDisabledFieldForUser(username, false); err != nil { + return err + } + + return nil +} + // IsUserDisabled returns true if the user with the given user name is disabled, false otherwise. func (m *Manager) IsUserDisabled(username string) (bool, error) { u, err := m.db.UserByName(username) diff --git a/internal/users/manager_test.go b/internal/users/manager_test.go index 61935e9559..b91c986f45 100644 --- a/internal/users/manager_test.go +++ b/internal/users/manager_test.go @@ -772,6 +772,98 @@ func TestUpdateBrokerForUser(t *testing.T) { } } +//nolint:dupl // This is not a duplicate test +func TestDisableUser(t *testing.T) { + tests := map[string]struct { + username string + + dbFile string + + wantErr bool + wantErrType error + }{ + "Successfully_disable_user": {}, + + "Error_if_user_does_not_exist": {username: "doesnotexist", wantErrType: db.NoDataFoundError{}}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // We don't care about the output of gpasswd in this test, but we still need to mock it. + _ = localgroupstestutils.SetupGroupMock(t, filepath.Join("testdata", "groups", "empty.group")) + + if tc.username == "" { + tc.username = "user1" + } + if tc.dbFile == "" { + tc.dbFile = "multiple_users_and_groups" + } + + dbDir := t.TempDir() + err := db.Z_ForTests_CreateDBFromYAML(filepath.Join("testdata", "db", tc.dbFile+".db.yaml"), dbDir) + require.NoError(t, err, "Setup: could not create database from testdata") + m := newManagerForTests(t, dbDir) + + err = m.DisableUser(tc.username) + + requireErrorAssertions(t, err, tc.wantErrType, tc.wantErr) + if tc.wantErrType != nil || tc.wantErr { + return + } + + got, err := db.Z_ForTests_DumpNormalizedYAML(userstestutils.GetManagerDB(m)) + require.NoError(t, err, "Created database should be valid yaml content") + + golden.CheckOrUpdate(t, got) + }) + } +} + +//nolint:dupl // This is not a duplicate test +func TestEnableUser(t *testing.T) { + tests := map[string]struct { + username string + + dbFile string + + wantErr bool + wantErrType error + }{ + "Successfully_enable_user": {}, + + "Error_if_user_does_not_exist": {username: "doesnotexist", wantErrType: db.NoDataFoundError{}}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // We don't care about the output of gpasswd in this test, but we still need to mock it. + _ = localgroupstestutils.SetupGroupMock(t, filepath.Join("testdata", "groups", "empty.group")) + + if tc.username == "" { + tc.username = "user1" + } + if tc.dbFile == "" { + tc.dbFile = "disabled_user" + } + + dbDir := t.TempDir() + err := db.Z_ForTests_CreateDBFromYAML(filepath.Join("testdata", "db", tc.dbFile+".db.yaml"), dbDir) + require.NoError(t, err, "Setup: could not create database from testdata") + m := newManagerForTests(t, dbDir) + + err = m.EnableUser(tc.username) + + requireErrorAssertions(t, err, tc.wantErrType, tc.wantErr) + if tc.wantErrType != nil || tc.wantErr { + return + } + + got, err := db.Z_ForTests_DumpNormalizedYAML(userstestutils.GetManagerDB(m)) + require.NoError(t, err, "Created database should be valid yaml content") + + golden.CheckOrUpdate(t, got) + }) + } +} + func TestUserByIDAndName(t *testing.T) { t.Parallel() diff --git a/internal/users/testdata/db/disabled_user.db.yaml b/internal/users/testdata/db/disabled_user.db.yaml new file mode 100644 index 0000000000..a953e84b62 --- /dev/null +++ b/internal/users/testdata/db/disabled_user.db.yaml @@ -0,0 +1,18 @@ +users: + - name: user1 + uid: 1111 + gid: 11111 + gecos: |- + User1 gecos + On multiple lines + dir: /home/user1 + shell: /bin/bash + broker_id: broker-id + disabled: true +groups: + - name: group1 + gid: 11111 + ugid: "12345678" +users_to_groups: + - uid: 1111 + gid: 11111 diff --git a/internal/users/testdata/golden/TestDisableUser/Successfully_disable_user b/internal/users/testdata/golden/TestDisableUser/Successfully_disable_user new file mode 100644 index 0000000000..256fcbe5fc --- /dev/null +++ b/internal/users/testdata/golden/TestDisableUser/Successfully_disable_user @@ -0,0 +1,65 @@ +users: + - name: user1 + uid: 1111 + gid: 11111 + gecos: |- + User1 gecos + On multiple lines + dir: /home/user1 + shell: /bin/bash + broker_id: broker-id + disabled: true + - name: user2 + uid: 2222 + gid: 22222 + gecos: User2 + dir: /home/user2 + shell: /bin/dash + broker_id: broker-id + - name: user3 + uid: 3333 + gid: 33333 + gecos: User3 + dir: /home/user3 + shell: /bin/zsh + broker_id: broker-id + - name: userwithoutbroker + uid: 4444 + gid: 44444 + gecos: userwithoutbroker + dir: /home/userwithoutbroker + shell: /bin/sh +groups: + - name: group1 + gid: 11111 + ugid: "12345678" + - name: group2withoutugid + gid: 22222 + ugid: "" + - name: group3 + gid: 33333 + ugid: "34567812" + - name: group4 + gid: 44444 + ugid: "45678123" + - name: commongroup + gid: 99999 + ugid: "87654321" +users_to_groups: + - uid: 1111 + gid: 11111 + - uid: 1111 + gid: 99999 + - uid: 2222 + gid: 22222 + - uid: 2222 + gid: 99999 + - uid: 3333 + gid: 33333 + - uid: 3333 + gid: 99999 + - uid: 4444 + gid: 44444 + - uid: 4444 + gid: 99999 +schema_version: 1 diff --git a/internal/users/testdata/golden/TestEnableUser/Successfully_enable_user b/internal/users/testdata/golden/TestEnableUser/Successfully_enable_user new file mode 100644 index 0000000000..0f479a482b --- /dev/null +++ b/internal/users/testdata/golden/TestEnableUser/Successfully_enable_user @@ -0,0 +1,18 @@ +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 +schema_version: 1 From d6edb3e7ab93f4b098313ff5994608fe89652e37 Mon Sep 17 00:00:00 2001 From: Shiv Tyagi Date: Wed, 19 Feb 2025 01:41:18 +0530 Subject: [PATCH 06/55] user service: add API methods to enable/disable user --- internal/proto/authd/authd.pb.go | 396 +++++++++++------- internal/proto/authd/authd.proto | 11 + internal/proto/authd/authd_grpc.pb.go | 76 ++++ .../testdata/golden/TestRegisterGRPCServices | 6 + internal/services/user/user.go | 34 ++ 5 files changed, 378 insertions(+), 145 deletions(-) diff --git a/internal/proto/authd/authd.pb.go b/internal/proto/authd/authd.pb.go index 3c619673b2..333e7302f9 100644 --- a/internal/proto/authd/authd.pb.go +++ b/internal/proto/authd/authd.pb.go @@ -993,6 +993,94 @@ func (x *GetUserByIDRequest) GetId() uint32 { return 0 } +type DisableUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DisableUserRequest) Reset() { + *x = DisableUserRequest{} + mi := &file_authd_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DisableUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DisableUserRequest) ProtoMessage() {} + +func (x *DisableUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_authd_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DisableUserRequest.ProtoReflect.Descriptor instead. +func (*DisableUserRequest) Descriptor() ([]byte, []int) { + return file_authd_proto_rawDescGZIP(), []int{18} +} + +func (x *DisableUserRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type EnableUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EnableUserRequest) Reset() { + *x = EnableUserRequest{} + mi := &file_authd_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EnableUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EnableUserRequest) ProtoMessage() {} + +func (x *EnableUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_authd_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EnableUserRequest.ProtoReflect.Descriptor instead. +func (*EnableUserRequest) Descriptor() ([]byte, []int) { + return file_authd_proto_rawDescGZIP(), []int{19} +} + +func (x *EnableUserRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + type GetGroupByNameRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -1002,7 +1090,7 @@ type GetGroupByNameRequest struct { func (x *GetGroupByNameRequest) Reset() { *x = GetGroupByNameRequest{} - mi := &file_authd_proto_msgTypes[18] + mi := &file_authd_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1014,7 +1102,7 @@ func (x *GetGroupByNameRequest) String() string { func (*GetGroupByNameRequest) ProtoMessage() {} func (x *GetGroupByNameRequest) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[18] + mi := &file_authd_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1027,7 +1115,7 @@ func (x *GetGroupByNameRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetGroupByNameRequest.ProtoReflect.Descriptor instead. func (*GetGroupByNameRequest) Descriptor() ([]byte, []int) { - return file_authd_proto_rawDescGZIP(), []int{18} + return file_authd_proto_rawDescGZIP(), []int{20} } func (x *GetGroupByNameRequest) GetName() string { @@ -1046,7 +1134,7 @@ type GetGroupByIDRequest struct { func (x *GetGroupByIDRequest) Reset() { *x = GetGroupByIDRequest{} - mi := &file_authd_proto_msgTypes[19] + mi := &file_authd_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1058,7 +1146,7 @@ func (x *GetGroupByIDRequest) String() string { func (*GetGroupByIDRequest) ProtoMessage() {} func (x *GetGroupByIDRequest) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[19] + mi := &file_authd_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1071,7 +1159,7 @@ func (x *GetGroupByIDRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetGroupByIDRequest.ProtoReflect.Descriptor instead. func (*GetGroupByIDRequest) Descriptor() ([]byte, []int) { - return file_authd_proto_rawDescGZIP(), []int{19} + return file_authd_proto_rawDescGZIP(), []int{21} } func (x *GetGroupByIDRequest) GetId() uint32 { @@ -1095,7 +1183,7 @@ type User struct { func (x *User) Reset() { *x = User{} - mi := &file_authd_proto_msgTypes[20] + mi := &file_authd_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1107,7 +1195,7 @@ func (x *User) String() string { func (*User) ProtoMessage() {} func (x *User) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[20] + mi := &file_authd_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1120,7 +1208,7 @@ func (x *User) ProtoReflect() protoreflect.Message { // Deprecated: Use User.ProtoReflect.Descriptor instead. func (*User) Descriptor() ([]byte, []int) { - return file_authd_proto_rawDescGZIP(), []int{20} + return file_authd_proto_rawDescGZIP(), []int{22} } func (x *User) GetName() string { @@ -1174,7 +1262,7 @@ type Users struct { func (x *Users) Reset() { *x = Users{} - mi := &file_authd_proto_msgTypes[21] + mi := &file_authd_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1186,7 +1274,7 @@ func (x *Users) String() string { func (*Users) ProtoMessage() {} func (x *Users) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[21] + mi := &file_authd_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1199,7 +1287,7 @@ func (x *Users) ProtoReflect() protoreflect.Message { // Deprecated: Use Users.ProtoReflect.Descriptor instead. func (*Users) Descriptor() ([]byte, []int) { - return file_authd_proto_rawDescGZIP(), []int{21} + return file_authd_proto_rawDescGZIP(), []int{23} } func (x *Users) GetUsers() []*User { @@ -1222,7 +1310,7 @@ type Group struct { func (x *Group) Reset() { *x = Group{} - mi := &file_authd_proto_msgTypes[22] + mi := &file_authd_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1234,7 +1322,7 @@ func (x *Group) String() string { func (*Group) ProtoMessage() {} func (x *Group) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[22] + mi := &file_authd_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1247,7 +1335,7 @@ func (x *Group) ProtoReflect() protoreflect.Message { // Deprecated: Use Group.ProtoReflect.Descriptor instead. func (*Group) Descriptor() ([]byte, []int) { - return file_authd_proto_rawDescGZIP(), []int{22} + return file_authd_proto_rawDescGZIP(), []int{24} } func (x *Group) GetName() string { @@ -1287,7 +1375,7 @@ type Groups struct { func (x *Groups) Reset() { *x = Groups{} - mi := &file_authd_proto_msgTypes[23] + mi := &file_authd_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1299,7 +1387,7 @@ func (x *Groups) String() string { func (*Groups) ProtoMessage() {} func (x *Groups) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[23] + mi := &file_authd_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1312,7 +1400,7 @@ func (x *Groups) ProtoReflect() protoreflect.Message { // Deprecated: Use Groups.ProtoReflect.Descriptor instead. func (*Groups) Descriptor() ([]byte, []int) { - return file_authd_proto_rawDescGZIP(), []int{23} + return file_authd_proto_rawDescGZIP(), []int{25} } func (x *Groups) GetGroups() []*Group { @@ -1333,7 +1421,7 @@ type ABResponse_BrokerInfo struct { func (x *ABResponse_BrokerInfo) Reset() { *x = ABResponse_BrokerInfo{} - mi := &file_authd_proto_msgTypes[24] + mi := &file_authd_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1345,7 +1433,7 @@ func (x *ABResponse_BrokerInfo) String() string { func (*ABResponse_BrokerInfo) ProtoMessage() {} func (x *ABResponse_BrokerInfo) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[24] + mi := &file_authd_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1392,7 +1480,7 @@ type GAMResponse_AuthenticationMode struct { func (x *GAMResponse_AuthenticationMode) Reset() { *x = GAMResponse_AuthenticationMode{} - mi := &file_authd_proto_msgTypes[25] + mi := &file_authd_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1404,7 +1492,7 @@ func (x *GAMResponse_AuthenticationMode) String() string { func (*GAMResponse_AuthenticationMode) ProtoMessage() {} func (x *GAMResponse_AuthenticationMode) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[25] + mi := &file_authd_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1449,7 +1537,7 @@ type IARequest_AuthenticationData struct { func (x *IARequest_AuthenticationData) Reset() { *x = IARequest_AuthenticationData{} - mi := &file_authd_proto_msgTypes[26] + mi := &file_authd_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1461,7 +1549,7 @@ func (x *IARequest_AuthenticationData) String() string { func (*IARequest_AuthenticationData) ProtoMessage() {} func (x *IARequest_AuthenticationData) ProtoReflect() protoreflect.Message { - mi := &file_authd_proto_msgTypes[26] + mi := &file_authd_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1670,90 +1758,102 @@ var file_authd_proto_rawDesc = string([]byte{ 0x75, 0x6c, 0x64, 0x50, 0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x22, 0x24, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, - 0x64, 0x22, 0x2b, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x4e, - 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x25, - 0x0a, 0x13, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0d, 0x52, 0x02, 0x69, 0x64, 0x22, 0x84, 0x01, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x12, - 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0d, 0x52, 0x03, 0x67, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x65, 0x63, 0x6f, 0x73, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x65, 0x63, 0x6f, 0x73, 0x12, 0x18, 0x0a, 0x07, - 0x68, 0x6f, 0x6d, 0x65, 0x64, 0x69, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x68, - 0x6f, 0x6d, 0x65, 0x64, 0x69, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x22, 0x2a, 0x0a, 0x05, - 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x21, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x55, 0x73, 0x65, - 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x22, 0x5f, 0x0a, 0x05, 0x47, 0x72, 0x6f, 0x75, - 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x03, 0x67, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, - 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, - 0x73, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, 0x73, 0x73, 0x77, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x70, 0x61, 0x73, 0x73, 0x77, 0x64, 0x22, 0x2e, 0x0a, 0x06, 0x47, 0x72, 0x6f, - 0x75, 0x70, 0x73, 0x12, 0x24, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x72, 0x6f, 0x75, - 0x70, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2a, 0x3c, 0x0a, 0x0b, 0x53, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x44, 0x45, - 0x46, 0x49, 0x4e, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x4c, 0x4f, 0x47, 0x49, 0x4e, - 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x50, 0x41, 0x53, - 0x53, 0x57, 0x4f, 0x52, 0x44, 0x10, 0x02, 0x32, 0xd3, 0x03, 0x0a, 0x03, 0x50, 0x41, 0x4d, 0x12, - 0x33, 0x0a, 0x10, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x72, 0x6f, 0x6b, - 0x65, 0x72, 0x73, 0x12, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x1a, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x41, 0x42, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x76, 0x69, - 0x6f, 0x75, 0x73, 0x42, 0x72, 0x6f, 0x6b, 0x65, 0x72, 0x12, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, - 0x64, 0x2e, 0x47, 0x50, 0x42, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x61, - 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x50, 0x42, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x33, 0x0a, 0x0c, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x42, 0x72, 0x6f, 0x6b, 0x65, 0x72, - 0x12, 0x10, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x53, 0x42, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x53, 0x42, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, - 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x73, 0x12, - 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x41, 0x4d, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x41, 0x4d, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x18, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x6f, - 0x64, 0x65, 0x12, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x53, 0x41, 0x4d, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x53, 0x41, - 0x4d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x0f, 0x49, 0x73, 0x41, - 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x12, 0x10, 0x2e, 0x61, - 0x75, 0x74, 0x68, 0x64, 0x2e, 0x49, 0x41, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, - 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x49, 0x41, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x2c, 0x0a, 0x0a, 0x45, 0x6e, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, - 0x10, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x53, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, - 0x3c, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x42, 0x72, 0x6f, - 0x6b, 0x65, 0x72, 0x46, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x12, 0x13, 0x2e, 0x61, 0x75, 0x74, - 0x68, 0x64, 0x2e, 0x53, 0x44, 0x42, 0x46, 0x55, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x32, 0xcb, 0x02, - 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x39, 0x0a, - 0x0d, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, - 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x42, 0x79, - 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x75, - 0x74, 0x68, 0x64, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x12, 0x35, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x55, - 0x73, 0x65, 0x72, 0x42, 0x79, 0x49, 0x44, 0x12, 0x19, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, - 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x12, - 0x27, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x0c, 0x2e, 0x61, - 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0c, 0x2e, 0x61, 0x75, 0x74, - 0x68, 0x64, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x3c, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x2e, 0x61, 0x75, 0x74, - 0x68, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x4e, 0x61, 0x6d, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, - 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, - 0x75, 0x70, 0x42, 0x79, 0x49, 0x44, 0x12, 0x1a, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, - 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, - 0x12, 0x29, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x0c, - 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x61, - 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x42, 0x2e, 0x5a, 0x2c, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x62, 0x75, 0x6e, 0x74, 0x75, - 0x2f, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x64, 0x22, 0x28, 0x0a, 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x73, 0x65, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x27, 0x0a, 0x11, 0x45, + 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x2b, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, + 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x22, 0x25, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x49, + 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x22, 0x84, 0x01, 0x0a, 0x04, 0x55, 0x73, 0x65, + 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x67, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x65, 0x63, + 0x6f, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x65, 0x63, 0x6f, 0x73, 0x12, + 0x18, 0x0a, 0x07, 0x68, 0x6f, 0x6d, 0x65, 0x64, 0x69, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x68, 0x6f, 0x6d, 0x65, 0x64, 0x69, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x68, 0x65, + 0x6c, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x22, + 0x2a, 0x0a, 0x05, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x21, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, + 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x22, 0x5f, 0x0a, 0x05, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x67, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, + 0x6d, 0x62, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x6d, + 0x62, 0x65, 0x72, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, 0x73, 0x73, 0x77, 0x64, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x73, 0x73, 0x77, 0x64, 0x22, 0x2e, 0x0a, 0x06, + 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x24, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2a, 0x3c, 0x0a, 0x0b, + 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x55, + 0x4e, 0x44, 0x45, 0x46, 0x49, 0x4e, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x4c, 0x4f, + 0x47, 0x49, 0x4e, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, + 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x10, 0x02, 0x32, 0xd3, 0x03, 0x0a, 0x03, 0x50, + 0x41, 0x4d, 0x12, 0x33, 0x0a, 0x10, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x42, + 0x72, 0x6f, 0x6b, 0x65, 0x72, 0x73, 0x12, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x41, 0x42, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x50, 0x72, + 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x42, 0x72, 0x6f, 0x6b, 0x65, 0x72, 0x12, 0x11, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x50, 0x42, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x12, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x50, 0x42, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x0c, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x42, 0x72, 0x6f, + 0x6b, 0x65, 0x72, 0x12, 0x10, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x53, 0x42, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x53, 0x42, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x41, + 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x6f, 0x64, + 0x65, 0x73, 0x12, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x41, 0x4d, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x41, + 0x4d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x18, 0x53, 0x65, 0x6c, + 0x65, 0x63, 0x74, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x53, 0x41, + 0x4d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, + 0x2e, 0x53, 0x41, 0x4d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x0f, + 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x12, + 0x10, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x49, 0x41, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x49, 0x41, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x0a, 0x45, 0x6e, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x10, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x53, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x12, 0x3c, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, + 0x42, 0x72, 0x6f, 0x6b, 0x65, 0x72, 0x46, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x12, 0x13, 0x2e, + 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x53, 0x44, 0x42, 0x46, 0x55, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x32, 0xb9, 0x03, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x12, 0x39, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x42, 0x79, 0x4e, 0x61, 0x6d, + 0x65, 0x12, 0x1b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, + 0x72, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, + 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x12, 0x35, 0x0a, 0x0b, 0x47, + 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x42, 0x79, 0x49, 0x44, 0x12, 0x19, 0x2e, 0x61, 0x75, 0x74, + 0x68, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x55, 0x73, + 0x65, 0x72, 0x12, 0x27, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, + 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0c, 0x2e, + 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x36, 0x0a, 0x0b, 0x44, + 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x75, 0x74, + 0x68, 0x64, 0x2e, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x12, 0x34, 0x0a, 0x0a, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x73, 0x65, + 0x72, 0x12, 0x18, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, + 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x61, 0x75, + 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3c, 0x0a, 0x0e, 0x47, 0x65, 0x74, + 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x2e, 0x61, 0x75, + 0x74, 0x68, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x4e, 0x61, + 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x64, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x47, 0x72, + 0x6f, 0x75, 0x70, 0x42, 0x79, 0x49, 0x44, 0x12, 0x1a, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, + 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x12, 0x29, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, + 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, + 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x42, 0x2e, 0x5a, 0x2c, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x62, 0x75, 0x6e, 0x74, + 0x75, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x64, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -1769,7 +1869,7 @@ func file_authd_proto_rawDescGZIP() []byte { } var file_authd_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_authd_proto_msgTypes = make([]protoimpl.MessageInfo, 27) +var file_authd_proto_msgTypes = make([]protoimpl.MessageInfo, 29) var file_authd_proto_goTypes = []any{ (SessionMode)(0), // 0: authd.SessionMode (*Empty)(nil), // 1: authd.Empty @@ -1790,25 +1890,27 @@ var file_authd_proto_goTypes = []any{ (*ESRequest)(nil), // 16: authd.ESRequest (*GetUserByNameRequest)(nil), // 17: authd.GetUserByNameRequest (*GetUserByIDRequest)(nil), // 18: authd.GetUserByIDRequest - (*GetGroupByNameRequest)(nil), // 19: authd.GetGroupByNameRequest - (*GetGroupByIDRequest)(nil), // 20: authd.GetGroupByIDRequest - (*User)(nil), // 21: authd.User - (*Users)(nil), // 22: authd.Users - (*Group)(nil), // 23: authd.Group - (*Groups)(nil), // 24: authd.Groups - (*ABResponse_BrokerInfo)(nil), // 25: authd.ABResponse.BrokerInfo - (*GAMResponse_AuthenticationMode)(nil), // 26: authd.GAMResponse.AuthenticationMode - (*IARequest_AuthenticationData)(nil), // 27: authd.IARequest.AuthenticationData + (*DisableUserRequest)(nil), // 19: authd.DisableUserRequest + (*EnableUserRequest)(nil), // 20: authd.EnableUserRequest + (*GetGroupByNameRequest)(nil), // 21: authd.GetGroupByNameRequest + (*GetGroupByIDRequest)(nil), // 22: authd.GetGroupByIDRequest + (*User)(nil), // 23: authd.User + (*Users)(nil), // 24: authd.Users + (*Group)(nil), // 25: authd.Group + (*Groups)(nil), // 26: authd.Groups + (*ABResponse_BrokerInfo)(nil), // 27: authd.ABResponse.BrokerInfo + (*GAMResponse_AuthenticationMode)(nil), // 28: authd.GAMResponse.AuthenticationMode + (*IARequest_AuthenticationData)(nil), // 29: authd.IARequest.AuthenticationData } var file_authd_proto_depIdxs = []int32{ - 25, // 0: authd.ABResponse.brokers_infos:type_name -> authd.ABResponse.BrokerInfo + 27, // 0: authd.ABResponse.brokers_infos:type_name -> authd.ABResponse.BrokerInfo 0, // 1: authd.SBRequest.mode:type_name -> authd.SessionMode 9, // 2: authd.GAMRequest.supported_ui_layouts:type_name -> authd.UILayout - 26, // 3: authd.GAMResponse.authentication_modes:type_name -> authd.GAMResponse.AuthenticationMode + 28, // 3: authd.GAMResponse.authentication_modes:type_name -> authd.GAMResponse.AuthenticationMode 9, // 4: authd.SAMResponse.ui_layout_info:type_name -> authd.UILayout - 27, // 5: authd.IARequest.authentication_data:type_name -> authd.IARequest.AuthenticationData - 21, // 6: authd.Users.users:type_name -> authd.User - 23, // 7: authd.Groups.groups:type_name -> authd.Group + 29, // 5: authd.IARequest.authentication_data:type_name -> authd.IARequest.AuthenticationData + 23, // 6: authd.Users.users:type_name -> authd.User + 25, // 7: authd.Groups.groups:type_name -> authd.Group 1, // 8: authd.PAM.AvailableBrokers:input_type -> authd.Empty 2, // 9: authd.PAM.GetPreviousBroker:input_type -> authd.GPBRequest 6, // 10: authd.PAM.SelectBroker:input_type -> authd.SBRequest @@ -1820,25 +1922,29 @@ var file_authd_proto_depIdxs = []int32{ 17, // 16: authd.UserService.GetUserByName:input_type -> authd.GetUserByNameRequest 18, // 17: authd.UserService.GetUserByID:input_type -> authd.GetUserByIDRequest 1, // 18: authd.UserService.ListUsers:input_type -> authd.Empty - 19, // 19: authd.UserService.GetGroupByName:input_type -> authd.GetGroupByNameRequest - 20, // 20: authd.UserService.GetGroupByID:input_type -> authd.GetGroupByIDRequest - 1, // 21: authd.UserService.ListGroups:input_type -> authd.Empty - 4, // 22: authd.PAM.AvailableBrokers:output_type -> authd.ABResponse - 3, // 23: authd.PAM.GetPreviousBroker:output_type -> authd.GPBResponse - 7, // 24: authd.PAM.SelectBroker:output_type -> authd.SBResponse - 10, // 25: authd.PAM.GetAuthenticationModes:output_type -> authd.GAMResponse - 12, // 26: authd.PAM.SelectAuthenticationMode:output_type -> authd.SAMResponse - 14, // 27: authd.PAM.IsAuthenticated:output_type -> authd.IAResponse - 1, // 28: authd.PAM.EndSession:output_type -> authd.Empty - 1, // 29: authd.PAM.SetDefaultBrokerForUser:output_type -> authd.Empty - 21, // 30: authd.UserService.GetUserByName:output_type -> authd.User - 21, // 31: authd.UserService.GetUserByID:output_type -> authd.User - 22, // 32: authd.UserService.ListUsers:output_type -> authd.Users - 23, // 33: authd.UserService.GetGroupByName:output_type -> authd.Group - 23, // 34: authd.UserService.GetGroupByID:output_type -> authd.Group - 24, // 35: authd.UserService.ListGroups:output_type -> authd.Groups - 22, // [22:36] is the sub-list for method output_type - 8, // [8:22] is the sub-list for method input_type + 19, // 19: authd.UserService.DisableUser:input_type -> authd.DisableUserRequest + 20, // 20: authd.UserService.EnableUser:input_type -> authd.EnableUserRequest + 21, // 21: authd.UserService.GetGroupByName:input_type -> authd.GetGroupByNameRequest + 22, // 22: authd.UserService.GetGroupByID:input_type -> authd.GetGroupByIDRequest + 1, // 23: authd.UserService.ListGroups:input_type -> authd.Empty + 4, // 24: authd.PAM.AvailableBrokers:output_type -> authd.ABResponse + 3, // 25: authd.PAM.GetPreviousBroker:output_type -> authd.GPBResponse + 7, // 26: authd.PAM.SelectBroker:output_type -> authd.SBResponse + 10, // 27: authd.PAM.GetAuthenticationModes:output_type -> authd.GAMResponse + 12, // 28: authd.PAM.SelectAuthenticationMode:output_type -> authd.SAMResponse + 14, // 29: authd.PAM.IsAuthenticated:output_type -> authd.IAResponse + 1, // 30: authd.PAM.EndSession:output_type -> authd.Empty + 1, // 31: authd.PAM.SetDefaultBrokerForUser:output_type -> authd.Empty + 23, // 32: authd.UserService.GetUserByName:output_type -> authd.User + 23, // 33: authd.UserService.GetUserByID:output_type -> authd.User + 24, // 34: authd.UserService.ListUsers:output_type -> authd.Users + 1, // 35: authd.UserService.DisableUser:output_type -> authd.Empty + 1, // 36: authd.UserService.EnableUser:output_type -> authd.Empty + 25, // 37: authd.UserService.GetGroupByName:output_type -> authd.Group + 25, // 38: authd.UserService.GetGroupByID:output_type -> authd.Group + 26, // 39: authd.UserService.ListGroups:output_type -> authd.Groups + 24, // [24:40] is the sub-list for method output_type + 8, // [8:24] is the sub-list for method input_type 8, // [8:8] is the sub-list for extension type_name 8, // [8:8] is the sub-list for extension extendee 0, // [0:8] is the sub-list for field type_name @@ -1850,8 +1956,8 @@ func file_authd_proto_init() { return } file_authd_proto_msgTypes[8].OneofWrappers = []any{} - file_authd_proto_msgTypes[24].OneofWrappers = []any{} - file_authd_proto_msgTypes[26].OneofWrappers = []any{ + file_authd_proto_msgTypes[26].OneofWrappers = []any{} + file_authd_proto_msgTypes[28].OneofWrappers = []any{ (*IARequest_AuthenticationData_Secret)(nil), (*IARequest_AuthenticationData_Wait)(nil), (*IARequest_AuthenticationData_Skip)(nil), @@ -1863,7 +1969,7 @@ func file_authd_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_authd_proto_rawDesc), len(file_authd_proto_rawDesc)), NumEnums: 1, - NumMessages: 27, + NumMessages: 29, NumExtensions: 0, NumServices: 2, }, diff --git a/internal/proto/authd/authd.proto b/internal/proto/authd/authd.proto index 9ecf9264eb..40d5fd06a7 100644 --- a/internal/proto/authd/authd.proto +++ b/internal/proto/authd/authd.proto @@ -133,6 +133,9 @@ service UserService { rpc GetUserByName(GetUserByNameRequest) returns (User); rpc GetUserByID(GetUserByIDRequest) returns (User); rpc ListUsers(Empty) returns (Users); + rpc DisableUser(DisableUserRequest) returns (Empty); + rpc EnableUser(EnableUserRequest) returns (Empty); + rpc GetGroupByName(GetGroupByNameRequest) returns (Group); rpc GetGroupByID(GetGroupByIDRequest) returns (Group); rpc ListGroups(Empty) returns (Groups); @@ -147,6 +150,14 @@ message GetUserByIDRequest{ uint32 id = 1; } +message DisableUserRequest{ + string name = 1; +} + +message EnableUserRequest{ + string name = 1; +} + message GetGroupByNameRequest{ string name = 1; } diff --git a/internal/proto/authd/authd_grpc.pb.go b/internal/proto/authd/authd_grpc.pb.go index 9cae2925b1..e1bf09e8fb 100644 --- a/internal/proto/authd/authd_grpc.pb.go +++ b/internal/proto/authd/authd_grpc.pb.go @@ -390,6 +390,8 @@ const ( UserService_GetUserByName_FullMethodName = "/authd.UserService/GetUserByName" UserService_GetUserByID_FullMethodName = "/authd.UserService/GetUserByID" UserService_ListUsers_FullMethodName = "/authd.UserService/ListUsers" + UserService_DisableUser_FullMethodName = "/authd.UserService/DisableUser" + UserService_EnableUser_FullMethodName = "/authd.UserService/EnableUser" UserService_GetGroupByName_FullMethodName = "/authd.UserService/GetGroupByName" UserService_GetGroupByID_FullMethodName = "/authd.UserService/GetGroupByID" UserService_ListGroups_FullMethodName = "/authd.UserService/ListGroups" @@ -402,6 +404,8 @@ type UserServiceClient interface { GetUserByName(ctx context.Context, in *GetUserByNameRequest, opts ...grpc.CallOption) (*User, error) GetUserByID(ctx context.Context, in *GetUserByIDRequest, opts ...grpc.CallOption) (*User, error) ListUsers(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Users, error) + DisableUser(ctx context.Context, in *DisableUserRequest, opts ...grpc.CallOption) (*Empty, error) + EnableUser(ctx context.Context, in *EnableUserRequest, opts ...grpc.CallOption) (*Empty, error) GetGroupByName(ctx context.Context, in *GetGroupByNameRequest, opts ...grpc.CallOption) (*Group, error) GetGroupByID(ctx context.Context, in *GetGroupByIDRequest, opts ...grpc.CallOption) (*Group, error) ListGroups(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Groups, error) @@ -445,6 +449,26 @@ func (c *userServiceClient) ListUsers(ctx context.Context, in *Empty, opts ...gr return out, nil } +func (c *userServiceClient) DisableUser(ctx context.Context, in *DisableUserRequest, opts ...grpc.CallOption) (*Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Empty) + err := c.cc.Invoke(ctx, UserService_DisableUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) EnableUser(ctx context.Context, in *EnableUserRequest, opts ...grpc.CallOption) (*Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Empty) + err := c.cc.Invoke(ctx, UserService_EnableUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *userServiceClient) GetGroupByName(ctx context.Context, in *GetGroupByNameRequest, opts ...grpc.CallOption) (*Group, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Group) @@ -482,6 +506,8 @@ type UserServiceServer interface { GetUserByName(context.Context, *GetUserByNameRequest) (*User, error) GetUserByID(context.Context, *GetUserByIDRequest) (*User, error) ListUsers(context.Context, *Empty) (*Users, error) + DisableUser(context.Context, *DisableUserRequest) (*Empty, error) + EnableUser(context.Context, *EnableUserRequest) (*Empty, error) GetGroupByName(context.Context, *GetGroupByNameRequest) (*Group, error) GetGroupByID(context.Context, *GetGroupByIDRequest) (*Group, error) ListGroups(context.Context, *Empty) (*Groups, error) @@ -504,6 +530,12 @@ func (UnimplementedUserServiceServer) GetUserByID(context.Context, *GetUserByIDR func (UnimplementedUserServiceServer) ListUsers(context.Context, *Empty) (*Users, error) { return nil, status.Errorf(codes.Unimplemented, "method ListUsers not implemented") } +func (UnimplementedUserServiceServer) DisableUser(context.Context, *DisableUserRequest) (*Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DisableUser not implemented") +} +func (UnimplementedUserServiceServer) EnableUser(context.Context, *EnableUserRequest) (*Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method EnableUser not implemented") +} func (UnimplementedUserServiceServer) GetGroupByName(context.Context, *GetGroupByNameRequest) (*Group, error) { return nil, status.Errorf(codes.Unimplemented, "method GetGroupByName not implemented") } @@ -588,6 +620,42 @@ func _UserService_ListUsers_Handler(srv interface{}, ctx context.Context, dec fu return interceptor(ctx, in, info, handler) } +func _UserService_DisableUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DisableUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).DisableUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_DisableUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).DisableUser(ctx, req.(*DisableUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_EnableUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EnableUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).EnableUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_EnableUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).EnableUser(ctx, req.(*EnableUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _UserService_GetGroupByName_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetGroupByNameRequest) if err := dec(in); err != nil { @@ -661,6 +729,14 @@ var UserService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListUsers", Handler: _UserService_ListUsers_Handler, }, + { + MethodName: "DisableUser", + Handler: _UserService_DisableUser_Handler, + }, + { + MethodName: "EnableUser", + Handler: _UserService_EnableUser_Handler, + }, { MethodName: "GetGroupByName", Handler: _UserService_GetGroupByName_Handler, diff --git a/internal/services/testdata/golden/TestRegisterGRPCServices b/internal/services/testdata/golden/TestRegisterGRPCServices index eb115926b9..a2ef081d2b 100644 --- a/internal/services/testdata/golden/TestRegisterGRPCServices +++ b/internal/services/testdata/golden/TestRegisterGRPCServices @@ -27,6 +27,12 @@ authd.PAM: metadata: authd.proto authd.UserService: methods: + - name: DisableUser + isclientstream: false + isserverstream: false + - name: EnableUser + isclientstream: false + isserverstream: false - name: GetGroupByID isclientstream: false isserverstream: false diff --git a/internal/services/user/user.go b/internal/services/user/user.go index 0eb3ae9690..7c59afb95b 100644 --- a/internal/services/user/user.go +++ b/internal/services/user/user.go @@ -116,6 +116,40 @@ func (s Service) ListUsers(ctx context.Context, req *authd.Empty) (*authd.Users, return &res, nil } +// DisableUser marks a user as disabled. +func (s Service) DisableUser(ctx context.Context, req *authd.DisableUserRequest) (*authd.Empty, error) { + if err := s.permissionManager.CheckRequestIsFromRoot(ctx); err != nil { + return nil, err + } + + if req.GetName() == "" { + return nil, status.Error(codes.InvalidArgument, "no user name provided") + } + + if err := s.userManager.DisableUser(req.GetName()); err != nil { + return nil, err + } + + return &authd.Empty{}, nil +} + +// EnableUser marks a user as enabled. +func (s Service) EnableUser(ctx context.Context, req *authd.EnableUserRequest) (*authd.Empty, error) { + if err := s.permissionManager.CheckRequestIsFromRoot(ctx); err != nil { + return nil, err + } + + if req.GetName() == "" { + return nil, status.Error(codes.InvalidArgument, "no user name provided") + } + + if err := s.userManager.EnableUser(req.GetName()); err != nil { + return nil, err + } + + return &authd.Empty{}, nil +} + // GetGroupByName returns the group entry for the given group name. func (s Service) GetGroupByName(ctx context.Context, req *authd.GetGroupByNameRequest) (*authd.Group, error) { if req.GetName() == "" { From b431a3be3f69c17a1a38fac74449e775d336c64a Mon Sep 17 00:00:00 2001 From: Shiv Tyagi Date: Wed, 19 Feb 2025 12:17:23 +0530 Subject: [PATCH 07/55] user service: add tests for DisableUser/EnableUser API methods --- .../user/testdata/disabled-user.db.yaml | 49 +++++++++++++++ internal/services/user/user_test.go | 62 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 internal/services/user/testdata/disabled-user.db.yaml diff --git a/internal/services/user/testdata/disabled-user.db.yaml b/internal/services/user/testdata/disabled-user.db.yaml new file mode 100644 index 0000000000..6b087dd778 --- /dev/null +++ b/internal/services/user/testdata/disabled-user.db.yaml @@ -0,0 +1,49 @@ +users: + - name: user1 + uid: 1111 + gid: 11111 + gecos: |- + User1 gecos + On multiple lines + dir: /home/user1 + shell: /bin/bash + broker_id: broker-id + disabled: true + - name: user2 + uid: 2222 + gid: 22222 + gecos: User2 + dir: /home/user2 + shell: /bin/dash + broker_id: broker-id + - name: user3 + uid: 3333 + gid: 33333 + gecos: User3 + dir: /home/user3 + shell: /bin/zsh + broker_id: broker-id +groups: + - name: group1 + gid: 11111 + ugid: group1 + - name: group2 + gid: 22222 + ugid: group2 + - name: group3 + gid: 33333 + ugid: group3 + - name: commongroup + gid: 99999 + ugid: commongroup +users_to_groups: + - uid: 1111 + gid: 11111 + - uid: 2222 + gid: 22222 + - uid: 2222 + gid: 99999 + - uid: 3333 + gid: 33333 + - uid: 3333 + gid: 99999 diff --git a/internal/services/user/user_test.go b/internal/services/user/user_test.go index 720dfa1839..80f4a3d604 100644 --- a/internal/services/user/user_test.go +++ b/internal/services/user/user_test.go @@ -212,6 +212,68 @@ func TestListGroups(t *testing.T) { } } +func TestDisablePasswd(t *testing.T) { + tests := map[string]struct { + sourceDB string + + username string + currentUserNotRoot bool + + wantErr bool + }{ + "Successfully_disable_user": {username: "user1"}, + + "Error_when_username_is_empty": {wantErr: true}, + "Error_when_user_does_not_exist": {username: "doesnotexist", wantErr: true}, + "Error_when_not_root": {username: "notroot", currentUserNotRoot: true, wantErr: true}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + client := newUserServiceClient(t, tc.sourceDB) + + _, err := client.DisableUser(context.Background(), &authd.DisableUserRequest{Name: tc.username}) + if tc.wantErr { + require.Error(t, err, "DisablePasswd should return an error, but did not") + return + } + require.NoError(t, err, "DisablePasswd should not return an error, but did") + }) + } +} + +func TestEnableUser(t *testing.T) { + tests := map[string]struct { + sourceDB string + + username string + currentUserNotRoot bool + + wantErr bool + }{ + "Successfully_enable_user": {username: "user1"}, + + "Error_when_username_is_empty": {wantErr: true}, + "Error_when_user_does_not_exist": {username: "doesnotexist", wantErr: true}, + "Error_when_not_root": {username: "notroot", currentUserNotRoot: true, wantErr: true}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + if tc.sourceDB == "" { + tc.sourceDB = "disabled-user.db.yaml" + } + + client := newUserServiceClient(t, tc.sourceDB) + + _, err := client.EnableUser(context.Background(), &authd.EnableUserRequest{Name: tc.username}) + if tc.wantErr { + require.Error(t, err, "EnableUser should return an error, but did not") + return + } + require.NoError(t, err, "EnableUser should not return an error, but did") + }) + } +} + // newUserServiceClient returns a new gRPC client for the CLI service. func newUserServiceClient(t *testing.T, dbFile string) (client authd.UserServiceClient) { t.Helper() From eece7890a1c22faa7cda0ee6db5bcf792a58cfa6 Mon Sep 17 00:00:00 2001 From: Shiv Tyagi Date: Sat, 24 May 2025 14:15:39 +0530 Subject: [PATCH 08/55] create authctl cli tool --- cmd/authctl/main.go | 31 ++++++++++++++++++++++++++++ cmd/authctl/user/lock.go | 31 ++++++++++++++++++++++++++++ cmd/authctl/user/unlock.go | 31 ++++++++++++++++++++++++++++ cmd/authctl/user/user.go | 41 ++++++++++++++++++++++++++++++++++++++ debian/install | 3 +++ 5 files changed, 137 insertions(+) create mode 100644 cmd/authctl/main.go create mode 100644 cmd/authctl/user/lock.go create mode 100644 cmd/authctl/user/unlock.go create mode 100644 cmd/authctl/user/user.go diff --git a/cmd/authctl/main.go b/cmd/authctl/main.go new file mode 100644 index 0000000000..4b5a12ad5a --- /dev/null +++ b/cmd/authctl/main.go @@ -0,0 +1,31 @@ +// Package main implements Cobra commands for management operations on authd. +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/ubuntu/authd/cmd/authctl/user" +) + +const cmdName = "authctl" + +var rootCmd = &cobra.Command{ + Use: fmt.Sprintf("%s COMMAND", cmdName), + Short: "CLI tool to interact with authd", + Long: "authctl is a CLI tool which can be used to interact with authd.", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) {}, +} + +func init() { + rootCmd.AddCommand(user.UserCmd) +} + +func main() { + if err := rootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } +} diff --git a/cmd/authctl/user/lock.go b/cmd/authctl/user/lock.go new file mode 100644 index 0000000000..0a6375d56b --- /dev/null +++ b/cmd/authctl/user/lock.go @@ -0,0 +1,31 @@ +package user + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/ubuntu/authd/internal/proto/authd" +) + +// lockCmd is a command to disable a user. +var lockCmd = &cobra.Command{ + Use: "lock", + Short: "Lock (disable) a user managed by authd", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Printf("Disabling user %q\n...", args[0]) + + client, err := NewUserServiceClient() + if err != nil { + return err + } + + _, err = client.DisableUser(context.Background(), &authd.DisableUserRequest{Name: args[0]}) + if err != nil { + return err + } + + return nil + }, +} diff --git a/cmd/authctl/user/unlock.go b/cmd/authctl/user/unlock.go new file mode 100644 index 0000000000..e9211a3818 --- /dev/null +++ b/cmd/authctl/user/unlock.go @@ -0,0 +1,31 @@ +package user + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/ubuntu/authd/internal/proto/authd" +) + +// unlockCmd is a command to enable a user. +var unlockCmd = &cobra.Command{ + Use: "unlock", + Short: "Unlock (enable) a user managed by authd", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Printf("Enabling user %q...\n", args[0]) + + client, err := NewUserServiceClient() + if err != nil { + return err + } + + _, err = client.EnableUser(context.Background(), &authd.EnableUserRequest{Name: args[0]}) + if err != nil { + return err + } + + return nil + }, +} diff --git a/cmd/authctl/user/user.go b/cmd/authctl/user/user.go new file mode 100644 index 0000000000..d784394567 --- /dev/null +++ b/cmd/authctl/user/user.go @@ -0,0 +1,41 @@ +// Package user provides utilities for managing user operations. +package user + +import ( + "os" + + "github.com/spf13/cobra" + "github.com/ubuntu/authd/internal/consts" + "github.com/ubuntu/authd/internal/proto/authd" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +// UserCmd is a command to perform user-related operations. +var UserCmd = &cobra.Command{ + Use: "user", + Short: "Commands related to users", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) {}, +} + +// NewUserServiceClient creates and returns a new [authd.UserServiceClient]. +func NewUserServiceClient() (authd.UserServiceClient, error) { + authdSocket := os.Getenv("AUTHD_SOCKET") + if authdSocket == "" { + authdSocket = "unix://" + consts.DefaultSocketPath + } + + conn, err := grpc.NewClient(authdSocket, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, err + } + + client := authd.NewUserServiceClient(conn) + return client, nil +} + +func init() { + UserCmd.AddCommand(lockCmd) + UserCmd.AddCommand(unlockCmd) +} diff --git a/debian/install b/debian/install index 094a6ed57d..e9bc299602 100755 --- a/debian/install +++ b/debian/install @@ -3,6 +3,9 @@ # Install daemon usr/bin/authd ${env:AUTHD_DAEMONS_PATH} +# Install CLI tool +usr/bin/authctl /usr/bin/ + # Install authd config file debian/authd-config/authd.yaml /etc/authd/ From e2fd12b65d17b1834071b440b7d5947b5e0b2e70 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 20 May 2025 13:56:08 +0200 Subject: [PATCH 09/55] Make authctl print usage message when called without subcommand That's the default behavior of cobra, which was overridden with an empty Run function. --- cmd/authctl/main.go | 1 - cmd/authctl/user/user.go | 1 - 2 files changed, 2 deletions(-) diff --git a/cmd/authctl/main.go b/cmd/authctl/main.go index 4b5a12ad5a..79ef8ea333 100644 --- a/cmd/authctl/main.go +++ b/cmd/authctl/main.go @@ -16,7 +16,6 @@ var rootCmd = &cobra.Command{ Short: "CLI tool to interact with authd", Long: "authctl is a CLI tool which can be used to interact with authd.", Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) {}, } func init() { diff --git a/cmd/authctl/user/user.go b/cmd/authctl/user/user.go index d784394567..a811742bd2 100644 --- a/cmd/authctl/user/user.go +++ b/cmd/authctl/user/user.go @@ -16,7 +16,6 @@ var UserCmd = &cobra.Command{ Use: "user", Short: "Commands related to users", Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) {}, } // NewUserServiceClient creates and returns a new [authd.UserServiceClient]. From 0240ac6ef64afe3ff812e2663f99ee4c5078b26e Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 20 May 2025 14:01:33 +0200 Subject: [PATCH 10/55] authctl: Improve order of commands in usage message Print the `user` command before the `help` and `completion` commands. --- cmd/authctl/main.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/authctl/main.go b/cmd/authctl/main.go index 79ef8ea333..46516222f3 100644 --- a/cmd/authctl/main.go +++ b/cmd/authctl/main.go @@ -19,6 +19,11 @@ var rootCmd = &cobra.Command{ } func init() { + // Disable command sorting by name. This makes cobra print the commands in the + // order they are added to the root command and adds the `help` and `completion` + // commands at the end. + cobra.EnableCommandSorting = false + rootCmd.AddCommand(user.UserCmd) } From e41f43c7c5568f04d3f720322dbbc76f384daf0e Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 27 May 2025 20:41:50 +0200 Subject: [PATCH 11/55] Rename DisableUser -> LockUser, EnableUser -> UnlockUser --- cmd/authctl/user/lock.go | 6 +- cmd/authctl/user/unlock.go | 6 +- internal/proto/authd/authd.pb.go | 235 +++++++++--------- internal/proto/authd/authd.proto | 8 +- internal/proto/authd/authd_grpc.pb.go | 56 ++--- internal/services/pam/pam.go | 10 +- internal/services/pam/pam_test.go | 2 +- ...bled-user.db => cache-with-locked-user.db} | 6 +- .../testdata/golden/TestRegisterGRPCServices | 12 +- ...abled-user.db.yaml => locked-user.db.yaml} | 2 +- internal/services/user/user.go | 12 +- internal/services/user/user_test.go | 22 +- internal/users/db/db_test.go | 10 +- internal/users/db/sql/create_schema.sql | 2 +- internal/users/db/update.go | 10 +- internal/users/db/users.go | 18 +- internal/users/manager.go | 18 +- internal/users/manager_test.go | 12 +- ...abled_user.db.yaml => locked_user.db.yaml} | 2 +- .../Successfully_lock_user} | 2 +- .../Successfully_enable_user | 0 21 files changed, 225 insertions(+), 226 deletions(-) rename internal/services/pam/testdata/TestSelectBroker/{cache-with-disabled-user.db => cache-with-locked-user.db} (65%) rename internal/services/user/testdata/{disabled-user.db.yaml => locked-user.db.yaml} (97%) rename internal/users/testdata/db/{disabled_user.db.yaml => locked_user.db.yaml} (93%) rename internal/users/testdata/golden/{TestDisableUser/Successfully_disable_user => TestLockUser/Successfully_lock_user} (98%) rename internal/users/testdata/golden/{TestEnableUser => TestUnlockUser}/Successfully_enable_user (100%) diff --git a/cmd/authctl/user/lock.go b/cmd/authctl/user/lock.go index 0a6375d56b..10747f339a 100644 --- a/cmd/authctl/user/lock.go +++ b/cmd/authctl/user/lock.go @@ -8,20 +8,20 @@ import ( "github.com/ubuntu/authd/internal/proto/authd" ) -// lockCmd is a command to disable a user. +// lockCmd is a command to lock (disable) a user. var lockCmd = &cobra.Command{ Use: "lock", Short: "Lock (disable) a user managed by authd", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - fmt.Printf("Disabling user %q\n...", args[0]) + fmt.Printf("Locking user %q\n...", args[0]) client, err := NewUserServiceClient() if err != nil { return err } - _, err = client.DisableUser(context.Background(), &authd.DisableUserRequest{Name: args[0]}) + _, err = client.LockUser(context.Background(), &authd.LockUserRequest{Name: args[0]}) if err != nil { return err } diff --git a/cmd/authctl/user/unlock.go b/cmd/authctl/user/unlock.go index e9211a3818..43d92aae06 100644 --- a/cmd/authctl/user/unlock.go +++ b/cmd/authctl/user/unlock.go @@ -8,20 +8,20 @@ import ( "github.com/ubuntu/authd/internal/proto/authd" ) -// unlockCmd is a command to enable a user. +// unlockCmd is a command to unlock (enable) a user. var unlockCmd = &cobra.Command{ Use: "unlock", Short: "Unlock (enable) a user managed by authd", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - fmt.Printf("Enabling user %q...\n", args[0]) + fmt.Printf("Unlocking user %q...\n", args[0]) client, err := NewUserServiceClient() if err != nil { return err } - _, err = client.EnableUser(context.Background(), &authd.EnableUserRequest{Name: args[0]}) + _, err = client.UnlockUser(context.Background(), &authd.UnlockUserRequest{Name: args[0]}) if err != nil { return err } diff --git a/internal/proto/authd/authd.pb.go b/internal/proto/authd/authd.pb.go index 333e7302f9..45958b67f3 100644 --- a/internal/proto/authd/authd.pb.go +++ b/internal/proto/authd/authd.pb.go @@ -993,27 +993,27 @@ func (x *GetUserByIDRequest) GetId() uint32 { return 0 } -type DisableUserRequest struct { +type LockUserRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *DisableUserRequest) Reset() { - *x = DisableUserRequest{} +func (x *LockUserRequest) Reset() { + *x = LockUserRequest{} mi := &file_authd_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *DisableUserRequest) String() string { +func (x *LockUserRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DisableUserRequest) ProtoMessage() {} +func (*LockUserRequest) ProtoMessage() {} -func (x *DisableUserRequest) ProtoReflect() protoreflect.Message { +func (x *LockUserRequest) ProtoReflect() protoreflect.Message { mi := &file_authd_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1025,39 +1025,39 @@ func (x *DisableUserRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DisableUserRequest.ProtoReflect.Descriptor instead. -func (*DisableUserRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use LockUserRequest.ProtoReflect.Descriptor instead. +func (*LockUserRequest) Descriptor() ([]byte, []int) { return file_authd_proto_rawDescGZIP(), []int{18} } -func (x *DisableUserRequest) GetName() string { +func (x *LockUserRequest) GetName() string { if x != nil { return x.Name } return "" } -type EnableUserRequest struct { +type UnlockUserRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *EnableUserRequest) Reset() { - *x = EnableUserRequest{} +func (x *UnlockUserRequest) Reset() { + *x = UnlockUserRequest{} mi := &file_authd_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *EnableUserRequest) String() string { +func (x *UnlockUserRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*EnableUserRequest) ProtoMessage() {} +func (*UnlockUserRequest) ProtoMessage() {} -func (x *EnableUserRequest) ProtoReflect() protoreflect.Message { +func (x *UnlockUserRequest) ProtoReflect() protoreflect.Message { mi := &file_authd_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1069,12 +1069,12 @@ func (x *EnableUserRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use EnableUserRequest.ProtoReflect.Descriptor instead. -func (*EnableUserRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use UnlockUserRequest.ProtoReflect.Descriptor instead. +func (*UnlockUserRequest) Descriptor() ([]byte, []int) { return file_authd_proto_rawDescGZIP(), []int{19} } -func (x *EnableUserRequest) GetName() string { +func (x *UnlockUserRequest) GetName() string { if x != nil { return x.Name } @@ -1758,102 +1758,101 @@ var file_authd_proto_rawDesc = string([]byte{ 0x75, 0x6c, 0x64, 0x50, 0x72, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x22, 0x24, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, - 0x64, 0x22, 0x28, 0x0a, 0x12, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x73, 0x65, 0x72, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x27, 0x0a, 0x11, 0x45, - 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x2b, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, - 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, + 0x64, 0x22, 0x25, 0x0a, 0x0f, 0x4c, 0x6f, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x27, 0x0a, 0x11, 0x55, 0x6e, 0x6c, 0x6f, + 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x22, 0x25, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x49, - 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x22, 0x84, 0x01, 0x0a, 0x04, 0x55, 0x73, 0x65, - 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x67, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x65, 0x63, - 0x6f, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x65, 0x63, 0x6f, 0x73, 0x12, - 0x18, 0x0a, 0x07, 0x68, 0x6f, 0x6d, 0x65, 0x64, 0x69, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x68, 0x6f, 0x6d, 0x65, 0x64, 0x69, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x68, 0x65, - 0x6c, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x22, - 0x2a, 0x0a, 0x05, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x21, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, - 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x22, 0x5f, 0x0a, 0x05, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x67, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, - 0x6d, 0x62, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x6d, - 0x62, 0x65, 0x72, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, 0x73, 0x73, 0x77, 0x64, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x73, 0x73, 0x77, 0x64, 0x22, 0x2e, 0x0a, 0x06, - 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x24, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, - 0x72, 0x6f, 0x75, 0x70, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2a, 0x3c, 0x0a, 0x0b, - 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x55, - 0x4e, 0x44, 0x45, 0x46, 0x49, 0x4e, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x4c, 0x4f, - 0x47, 0x49, 0x4e, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, - 0x50, 0x41, 0x53, 0x53, 0x57, 0x4f, 0x52, 0x44, 0x10, 0x02, 0x32, 0xd3, 0x03, 0x0a, 0x03, 0x50, - 0x41, 0x4d, 0x12, 0x33, 0x0a, 0x10, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x42, - 0x72, 0x6f, 0x6b, 0x65, 0x72, 0x73, 0x12, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x41, 0x42, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x50, 0x72, - 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x42, 0x72, 0x6f, 0x6b, 0x65, 0x72, 0x12, 0x11, 0x2e, 0x61, - 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x50, 0x42, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x12, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x50, 0x42, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x0c, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x42, 0x72, 0x6f, - 0x6b, 0x65, 0x72, 0x12, 0x10, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x53, 0x42, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x53, 0x42, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x41, - 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x6f, 0x64, - 0x65, 0x73, 0x12, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x41, 0x4d, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x41, - 0x4d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x18, 0x53, 0x65, 0x6c, - 0x65, 0x63, 0x74, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x53, 0x41, - 0x4d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, - 0x2e, 0x53, 0x41, 0x4d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x0f, - 0x49, 0x73, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x12, - 0x10, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x49, 0x41, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x49, 0x41, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x0a, 0x45, 0x6e, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x10, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x53, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x12, 0x3c, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, - 0x42, 0x72, 0x6f, 0x6b, 0x65, 0x72, 0x46, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x12, 0x13, 0x2e, - 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x53, 0x44, 0x42, 0x46, 0x55, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x65, 0x22, 0x2b, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x4e, + 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x25, + 0x0a, 0x13, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x02, 0x69, 0x64, 0x22, 0x84, 0x01, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x03, 0x67, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x67, 0x65, 0x63, 0x6f, 0x73, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x65, 0x63, 0x6f, 0x73, 0x12, 0x18, 0x0a, 0x07, + 0x68, 0x6f, 0x6d, 0x65, 0x64, 0x69, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x68, + 0x6f, 0x6d, 0x65, 0x64, 0x69, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x68, 0x65, 0x6c, 0x6c, 0x22, 0x2a, 0x0a, 0x05, + 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x21, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x55, 0x73, 0x65, + 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x22, 0x5f, 0x0a, 0x05, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x03, 0x67, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, + 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, + 0x73, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x61, 0x73, 0x73, 0x77, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x70, 0x61, 0x73, 0x73, 0x77, 0x64, 0x22, 0x2e, 0x0a, 0x06, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x73, 0x12, 0x24, 0x0a, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x2a, 0x3c, 0x0a, 0x0b, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x44, 0x45, + 0x46, 0x49, 0x4e, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x4c, 0x4f, 0x47, 0x49, 0x4e, + 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x43, 0x48, 0x41, 0x4e, 0x47, 0x45, 0x5f, 0x50, 0x41, 0x53, + 0x53, 0x57, 0x4f, 0x52, 0x44, 0x10, 0x02, 0x32, 0xd3, 0x03, 0x0a, 0x03, 0x50, 0x41, 0x4d, 0x12, + 0x33, 0x0a, 0x10, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x72, 0x6f, 0x6b, + 0x65, 0x72, 0x73, 0x12, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x1a, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x41, 0x42, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x50, 0x72, 0x65, 0x76, 0x69, + 0x6f, 0x75, 0x73, 0x42, 0x72, 0x6f, 0x6b, 0x65, 0x72, 0x12, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x64, 0x2e, 0x47, 0x50, 0x42, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x50, 0x42, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x33, 0x0a, 0x0c, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x42, 0x72, 0x6f, 0x6b, 0x65, 0x72, + 0x12, 0x10, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x53, 0x42, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x53, 0x42, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, + 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x73, 0x12, + 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x41, 0x4d, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x41, 0x4d, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x41, 0x0a, 0x18, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, + 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x6f, + 0x64, 0x65, 0x12, 0x11, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x53, 0x41, 0x4d, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x53, 0x41, + 0x4d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x0f, 0x49, 0x73, 0x41, + 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x64, 0x12, 0x10, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x64, 0x2e, 0x49, 0x41, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, + 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x49, 0x41, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x2c, 0x0a, 0x0a, 0x45, 0x6e, 0x64, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, + 0x10, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x53, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, + 0x3c, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x42, 0x72, 0x6f, + 0x6b, 0x65, 0x72, 0x46, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x12, 0x13, 0x2e, 0x61, 0x75, 0x74, + 0x68, 0x64, 0x2e, 0x53, 0x44, 0x42, 0x46, 0x55, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x32, 0xb3, 0x03, + 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x39, 0x0a, + 0x0d, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, + 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x42, 0x79, + 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x75, + 0x74, 0x68, 0x64, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x12, 0x35, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x55, + 0x73, 0x65, 0x72, 0x42, 0x79, 0x49, 0x44, 0x12, 0x19, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, + 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x12, + 0x27, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x0c, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0c, 0x2e, 0x61, 0x75, 0x74, + 0x68, 0x64, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x30, 0x0a, 0x08, 0x4c, 0x6f, 0x63, 0x6b, + 0x55, 0x73, 0x65, 0x72, 0x12, 0x16, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x4c, 0x6f, 0x63, + 0x6b, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x61, + 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x34, 0x0a, 0x0a, 0x55, 0x6e, + 0x6c, 0x6f, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x12, 0x18, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, + 0x2e, 0x55, 0x6e, 0x6c, 0x6f, 0x63, 0x6b, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x32, 0xb9, 0x03, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x12, 0x39, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x42, 0x79, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x1b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, - 0x72, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, - 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x12, 0x35, 0x0a, 0x0b, 0x47, - 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x42, 0x79, 0x49, 0x44, 0x12, 0x19, 0x2e, 0x61, 0x75, 0x74, - 0x68, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0b, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x55, 0x73, - 0x65, 0x72, 0x12, 0x27, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, - 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0c, 0x2e, - 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x36, 0x0a, 0x0b, 0x44, - 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x73, 0x65, 0x72, 0x12, 0x19, 0x2e, 0x61, 0x75, 0x74, - 0x68, 0x64, 0x2e, 0x44, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x12, 0x34, 0x0a, 0x0a, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x55, 0x73, 0x65, - 0x72, 0x12, 0x18, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, - 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x61, 0x75, - 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3c, 0x0a, 0x0e, 0x47, 0x65, 0x74, - 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x2e, 0x61, 0x75, - 0x74, 0x68, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x4e, 0x61, - 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, - 0x64, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x47, 0x72, - 0x6f, 0x75, 0x70, 0x42, 0x79, 0x49, 0x44, 0x12, 0x1a, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, - 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x72, 0x6f, 0x75, - 0x70, 0x12, 0x29, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, - 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, - 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x42, 0x2e, 0x5a, 0x2c, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x62, 0x75, 0x6e, 0x74, - 0x75, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x64, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x12, 0x3c, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x1c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x72, + 0x6f, 0x75, 0x70, 0x42, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x38, + 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x49, 0x44, 0x12, 0x1a, + 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x65, 0x74, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, + 0x79, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0c, 0x2e, 0x61, 0x75, 0x74, + 0x68, 0x64, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x29, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, + 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x0c, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2e, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x73, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x75, 0x62, 0x75, 0x6e, 0x74, 0x75, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x64, 0x2f, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x75, + 0x74, 0x68, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -1890,8 +1889,8 @@ var file_authd_proto_goTypes = []any{ (*ESRequest)(nil), // 16: authd.ESRequest (*GetUserByNameRequest)(nil), // 17: authd.GetUserByNameRequest (*GetUserByIDRequest)(nil), // 18: authd.GetUserByIDRequest - (*DisableUserRequest)(nil), // 19: authd.DisableUserRequest - (*EnableUserRequest)(nil), // 20: authd.EnableUserRequest + (*LockUserRequest)(nil), // 19: authd.LockUserRequest + (*UnlockUserRequest)(nil), // 20: authd.UnlockUserRequest (*GetGroupByNameRequest)(nil), // 21: authd.GetGroupByNameRequest (*GetGroupByIDRequest)(nil), // 22: authd.GetGroupByIDRequest (*User)(nil), // 23: authd.User @@ -1922,8 +1921,8 @@ var file_authd_proto_depIdxs = []int32{ 17, // 16: authd.UserService.GetUserByName:input_type -> authd.GetUserByNameRequest 18, // 17: authd.UserService.GetUserByID:input_type -> authd.GetUserByIDRequest 1, // 18: authd.UserService.ListUsers:input_type -> authd.Empty - 19, // 19: authd.UserService.DisableUser:input_type -> authd.DisableUserRequest - 20, // 20: authd.UserService.EnableUser:input_type -> authd.EnableUserRequest + 19, // 19: authd.UserService.LockUser:input_type -> authd.LockUserRequest + 20, // 20: authd.UserService.UnlockUser:input_type -> authd.UnlockUserRequest 21, // 21: authd.UserService.GetGroupByName:input_type -> authd.GetGroupByNameRequest 22, // 22: authd.UserService.GetGroupByID:input_type -> authd.GetGroupByIDRequest 1, // 23: authd.UserService.ListGroups:input_type -> authd.Empty @@ -1938,8 +1937,8 @@ var file_authd_proto_depIdxs = []int32{ 23, // 32: authd.UserService.GetUserByName:output_type -> authd.User 23, // 33: authd.UserService.GetUserByID:output_type -> authd.User 24, // 34: authd.UserService.ListUsers:output_type -> authd.Users - 1, // 35: authd.UserService.DisableUser:output_type -> authd.Empty - 1, // 36: authd.UserService.EnableUser:output_type -> authd.Empty + 1, // 35: authd.UserService.LockUser:output_type -> authd.Empty + 1, // 36: authd.UserService.UnlockUser:output_type -> authd.Empty 25, // 37: authd.UserService.GetGroupByName:output_type -> authd.Group 25, // 38: authd.UserService.GetGroupByID:output_type -> authd.Group 26, // 39: authd.UserService.ListGroups:output_type -> authd.Groups diff --git a/internal/proto/authd/authd.proto b/internal/proto/authd/authd.proto index 40d5fd06a7..eb3ae7d63c 100644 --- a/internal/proto/authd/authd.proto +++ b/internal/proto/authd/authd.proto @@ -133,8 +133,8 @@ service UserService { rpc GetUserByName(GetUserByNameRequest) returns (User); rpc GetUserByID(GetUserByIDRequest) returns (User); rpc ListUsers(Empty) returns (Users); - rpc DisableUser(DisableUserRequest) returns (Empty); - rpc EnableUser(EnableUserRequest) returns (Empty); + rpc LockUser(LockUserRequest) returns (Empty); + rpc UnlockUser(UnlockUserRequest) returns (Empty); rpc GetGroupByName(GetGroupByNameRequest) returns (Group); rpc GetGroupByID(GetGroupByIDRequest) returns (Group); @@ -150,11 +150,11 @@ message GetUserByIDRequest{ uint32 id = 1; } -message DisableUserRequest{ +message LockUserRequest{ string name = 1; } -message EnableUserRequest{ +message UnlockUserRequest{ string name = 1; } diff --git a/internal/proto/authd/authd_grpc.pb.go b/internal/proto/authd/authd_grpc.pb.go index e1bf09e8fb..e17452054c 100644 --- a/internal/proto/authd/authd_grpc.pb.go +++ b/internal/proto/authd/authd_grpc.pb.go @@ -390,8 +390,8 @@ const ( UserService_GetUserByName_FullMethodName = "/authd.UserService/GetUserByName" UserService_GetUserByID_FullMethodName = "/authd.UserService/GetUserByID" UserService_ListUsers_FullMethodName = "/authd.UserService/ListUsers" - UserService_DisableUser_FullMethodName = "/authd.UserService/DisableUser" - UserService_EnableUser_FullMethodName = "/authd.UserService/EnableUser" + UserService_LockUser_FullMethodName = "/authd.UserService/LockUser" + UserService_UnlockUser_FullMethodName = "/authd.UserService/UnlockUser" UserService_GetGroupByName_FullMethodName = "/authd.UserService/GetGroupByName" UserService_GetGroupByID_FullMethodName = "/authd.UserService/GetGroupByID" UserService_ListGroups_FullMethodName = "/authd.UserService/ListGroups" @@ -404,8 +404,8 @@ type UserServiceClient interface { GetUserByName(ctx context.Context, in *GetUserByNameRequest, opts ...grpc.CallOption) (*User, error) GetUserByID(ctx context.Context, in *GetUserByIDRequest, opts ...grpc.CallOption) (*User, error) ListUsers(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Users, error) - DisableUser(ctx context.Context, in *DisableUserRequest, opts ...grpc.CallOption) (*Empty, error) - EnableUser(ctx context.Context, in *EnableUserRequest, opts ...grpc.CallOption) (*Empty, error) + LockUser(ctx context.Context, in *LockUserRequest, opts ...grpc.CallOption) (*Empty, error) + UnlockUser(ctx context.Context, in *UnlockUserRequest, opts ...grpc.CallOption) (*Empty, error) GetGroupByName(ctx context.Context, in *GetGroupByNameRequest, opts ...grpc.CallOption) (*Group, error) GetGroupByID(ctx context.Context, in *GetGroupByIDRequest, opts ...grpc.CallOption) (*Group, error) ListGroups(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Groups, error) @@ -449,20 +449,20 @@ func (c *userServiceClient) ListUsers(ctx context.Context, in *Empty, opts ...gr return out, nil } -func (c *userServiceClient) DisableUser(ctx context.Context, in *DisableUserRequest, opts ...grpc.CallOption) (*Empty, error) { +func (c *userServiceClient) LockUser(ctx context.Context, in *LockUserRequest, opts ...grpc.CallOption) (*Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Empty) - err := c.cc.Invoke(ctx, UserService_DisableUser_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, UserService_LockUser_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -func (c *userServiceClient) EnableUser(ctx context.Context, in *EnableUserRequest, opts ...grpc.CallOption) (*Empty, error) { +func (c *userServiceClient) UnlockUser(ctx context.Context, in *UnlockUserRequest, opts ...grpc.CallOption) (*Empty, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Empty) - err := c.cc.Invoke(ctx, UserService_EnableUser_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, UserService_UnlockUser_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -506,8 +506,8 @@ type UserServiceServer interface { GetUserByName(context.Context, *GetUserByNameRequest) (*User, error) GetUserByID(context.Context, *GetUserByIDRequest) (*User, error) ListUsers(context.Context, *Empty) (*Users, error) - DisableUser(context.Context, *DisableUserRequest) (*Empty, error) - EnableUser(context.Context, *EnableUserRequest) (*Empty, error) + LockUser(context.Context, *LockUserRequest) (*Empty, error) + UnlockUser(context.Context, *UnlockUserRequest) (*Empty, error) GetGroupByName(context.Context, *GetGroupByNameRequest) (*Group, error) GetGroupByID(context.Context, *GetGroupByIDRequest) (*Group, error) ListGroups(context.Context, *Empty) (*Groups, error) @@ -530,11 +530,11 @@ func (UnimplementedUserServiceServer) GetUserByID(context.Context, *GetUserByIDR func (UnimplementedUserServiceServer) ListUsers(context.Context, *Empty) (*Users, error) { return nil, status.Errorf(codes.Unimplemented, "method ListUsers not implemented") } -func (UnimplementedUserServiceServer) DisableUser(context.Context, *DisableUserRequest) (*Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method DisableUser not implemented") +func (UnimplementedUserServiceServer) LockUser(context.Context, *LockUserRequest) (*Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method LockUser not implemented") } -func (UnimplementedUserServiceServer) EnableUser(context.Context, *EnableUserRequest) (*Empty, error) { - return nil, status.Errorf(codes.Unimplemented, "method EnableUser not implemented") +func (UnimplementedUserServiceServer) UnlockUser(context.Context, *UnlockUserRequest) (*Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method UnlockUser not implemented") } func (UnimplementedUserServiceServer) GetGroupByName(context.Context, *GetGroupByNameRequest) (*Group, error) { return nil, status.Errorf(codes.Unimplemented, "method GetGroupByName not implemented") @@ -620,38 +620,38 @@ func _UserService_ListUsers_Handler(srv interface{}, ctx context.Context, dec fu return interceptor(ctx, in, info, handler) } -func _UserService_DisableUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(DisableUserRequest) +func _UserService_LockUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(LockUserRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(UserServiceServer).DisableUser(ctx, in) + return srv.(UserServiceServer).LockUser(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: UserService_DisableUser_FullMethodName, + FullMethod: UserService_LockUser_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(UserServiceServer).DisableUser(ctx, req.(*DisableUserRequest)) + return srv.(UserServiceServer).LockUser(ctx, req.(*LockUserRequest)) } return interceptor(ctx, in, info, handler) } -func _UserService_EnableUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(EnableUserRequest) +func _UserService_UnlockUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UnlockUserRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(UserServiceServer).EnableUser(ctx, in) + return srv.(UserServiceServer).UnlockUser(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: UserService_EnableUser_FullMethodName, + FullMethod: UserService_UnlockUser_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(UserServiceServer).EnableUser(ctx, req.(*EnableUserRequest)) + return srv.(UserServiceServer).UnlockUser(ctx, req.(*UnlockUserRequest)) } return interceptor(ctx, in, info, handler) } @@ -730,12 +730,12 @@ var UserService_ServiceDesc = grpc.ServiceDesc{ Handler: _UserService_ListUsers_Handler, }, { - MethodName: "DisableUser", - Handler: _UserService_DisableUser_Handler, + MethodName: "LockUser", + Handler: _UserService_LockUser_Handler, }, { - MethodName: "EnableUser", - Handler: _UserService_EnableUser_Handler, + MethodName: "UnlockUser", + Handler: _UserService_UnlockUser_Handler, }, { MethodName: "GetGroupByName", diff --git a/internal/services/pam/pam.go b/internal/services/pam/pam.go index 2b45b467e6..59596eb919 100644 --- a/internal/services/pam/pam.go +++ b/internal/services/pam/pam.go @@ -144,13 +144,13 @@ func (s Service) SelectBroker(ctx context.Context, req *authd.SBRequest) (resp * lang = "C" } - userIsDisabled, err := s.userManager.IsUserDisabled(username) + userIsLocked, err := s.userManager.IsUserLocked(username) if err != nil && !errors.Is(err, users.NoDataFoundError{}) { - return nil, fmt.Errorf("could not check if user %q is disabled: %w", username, err) + return nil, fmt.Errorf("could not check if user %q is locked: %w", username, err) } - // Throw an error if the user trying to authenticate already exists in the database and is disabled - if err == nil && userIsDisabled { - return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("user %s is disabled", username)) + // Throw an error if the user trying to authenticate already exists in the database and is locked + if err == nil && userIsLocked { + return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("user %s is locked", username)) } var mode string diff --git a/internal/services/pam/pam_test.go b/internal/services/pam/pam_test.go index dd7673c9b3..1495687204 100644 --- a/internal/services/pam/pam_test.go +++ b/internal/services/pam/pam_test.go @@ -210,7 +210,7 @@ func TestSelectBroker(t *testing.T) { "Error_when_broker_does_not_exist": {username: "no broker", brokerID: "does not exist", wantErr: true}, "Error_when_broker_does_not_provide_a_session_ID": {username: "ns_no_id", wantErr: true}, "Error_when_starting_the_session": {username: "ns_error", wantErr: true}, - "Error_when_user_is_disabled": {username: "disabled", wantErr: true, existingDB: "cache-with-disabled-user.db"}, + "Error_when_user_is_locked": {username: "locked", wantErr: true, existingDB: "cache-with-locked-user.db"}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { diff --git a/internal/services/pam/testdata/TestSelectBroker/cache-with-disabled-user.db b/internal/services/pam/testdata/TestSelectBroker/cache-with-locked-user.db similarity index 65% rename from internal/services/pam/testdata/TestSelectBroker/cache-with-disabled-user.db rename to internal/services/pam/testdata/TestSelectBroker/cache-with-locked-user.db index 4155b941b4..05bf68dab2 100644 --- a/internal/services/pam/testdata/TestSelectBroker/cache-with-disabled-user.db +++ b/internal/services/pam/testdata/TestSelectBroker/cache-with-locked-user.db @@ -1,12 +1,12 @@ users: - - name: testselectbroker/error_when_user_is_disabled_separator_disabled + - name: testselectbroker/error_when_user_is_locked_separator_locked uid: 1111 gid: 11111 gecos: gecos for other user - dir: /home/disabled + dir: /home/locked shell: /bin/bash broker_id: broker-id - disabled: true + locked: true groups: - name: group1 gid: 11111 diff --git a/internal/services/testdata/golden/TestRegisterGRPCServices b/internal/services/testdata/golden/TestRegisterGRPCServices index a2ef081d2b..ad43546e3c 100644 --- a/internal/services/testdata/golden/TestRegisterGRPCServices +++ b/internal/services/testdata/golden/TestRegisterGRPCServices @@ -27,12 +27,6 @@ authd.PAM: metadata: authd.proto authd.UserService: methods: - - name: DisableUser - isclientstream: false - isserverstream: false - - name: EnableUser - isclientstream: false - isserverstream: false - name: GetGroupByID isclientstream: false isserverstream: false @@ -51,6 +45,12 @@ authd.UserService: - name: ListUsers isclientstream: false isserverstream: false + - name: LockUser + isclientstream: false + isserverstream: false + - name: UnlockUser + isclientstream: false + isserverstream: false metadata: authd.proto grpc.health.v1.Health: methods: diff --git a/internal/services/user/testdata/disabled-user.db.yaml b/internal/services/user/testdata/locked-user.db.yaml similarity index 97% rename from internal/services/user/testdata/disabled-user.db.yaml rename to internal/services/user/testdata/locked-user.db.yaml index 6b087dd778..e177d3bfb8 100644 --- a/internal/services/user/testdata/disabled-user.db.yaml +++ b/internal/services/user/testdata/locked-user.db.yaml @@ -8,7 +8,7 @@ users: dir: /home/user1 shell: /bin/bash broker_id: broker-id - disabled: true + locked: true - name: user2 uid: 2222 gid: 22222 diff --git a/internal/services/user/user.go b/internal/services/user/user.go index 7c59afb95b..db1fb72553 100644 --- a/internal/services/user/user.go +++ b/internal/services/user/user.go @@ -116,8 +116,8 @@ func (s Service) ListUsers(ctx context.Context, req *authd.Empty) (*authd.Users, return &res, nil } -// DisableUser marks a user as disabled. -func (s Service) DisableUser(ctx context.Context, req *authd.DisableUserRequest) (*authd.Empty, error) { +// LockUser marks a user as locked. +func (s Service) LockUser(ctx context.Context, req *authd.LockUserRequest) (*authd.Empty, error) { if err := s.permissionManager.CheckRequestIsFromRoot(ctx); err != nil { return nil, err } @@ -126,15 +126,15 @@ func (s Service) DisableUser(ctx context.Context, req *authd.DisableUserRequest) return nil, status.Error(codes.InvalidArgument, "no user name provided") } - if err := s.userManager.DisableUser(req.GetName()); err != nil { + if err := s.userManager.LockUser(req.GetName()); err != nil { return nil, err } return &authd.Empty{}, nil } -// EnableUser marks a user as enabled. -func (s Service) EnableUser(ctx context.Context, req *authd.EnableUserRequest) (*authd.Empty, error) { +// UnlockUser marks a user as unlocked. +func (s Service) UnlockUser(ctx context.Context, req *authd.UnlockUserRequest) (*authd.Empty, error) { if err := s.permissionManager.CheckRequestIsFromRoot(ctx); err != nil { return nil, err } @@ -143,7 +143,7 @@ func (s Service) EnableUser(ctx context.Context, req *authd.EnableUserRequest) ( return nil, status.Error(codes.InvalidArgument, "no user name provided") } - if err := s.userManager.EnableUser(req.GetName()); err != nil { + if err := s.userManager.UnlockUser(req.GetName()); err != nil { return nil, err } diff --git a/internal/services/user/user_test.go b/internal/services/user/user_test.go index 80f4a3d604..80ddad7ec6 100644 --- a/internal/services/user/user_test.go +++ b/internal/services/user/user_test.go @@ -212,7 +212,7 @@ func TestListGroups(t *testing.T) { } } -func TestDisablePasswd(t *testing.T) { +func TestLockUser(t *testing.T) { tests := map[string]struct { sourceDB string @@ -221,7 +221,7 @@ func TestDisablePasswd(t *testing.T) { wantErr bool }{ - "Successfully_disable_user": {username: "user1"}, + "Successfully_lock_user": {username: "user1"}, "Error_when_username_is_empty": {wantErr: true}, "Error_when_user_does_not_exist": {username: "doesnotexist", wantErr: true}, @@ -231,17 +231,17 @@ func TestDisablePasswd(t *testing.T) { t.Run(name, func(t *testing.T) { client := newUserServiceClient(t, tc.sourceDB) - _, err := client.DisableUser(context.Background(), &authd.DisableUserRequest{Name: tc.username}) + _, err := client.LockUser(context.Background(), &authd.LockUserRequest{Name: tc.username}) if tc.wantErr { - require.Error(t, err, "DisablePasswd should return an error, but did not") + require.Error(t, err, "LockUser should return an error, but did not") return } - require.NoError(t, err, "DisablePasswd should not return an error, but did") + require.NoError(t, err, "LockUser should not return an error, but did") }) } } -func TestEnableUser(t *testing.T) { +func TestUnlockUser(t *testing.T) { tests := map[string]struct { sourceDB string @@ -250,7 +250,7 @@ func TestEnableUser(t *testing.T) { wantErr bool }{ - "Successfully_enable_user": {username: "user1"}, + "Successfully_unlock_user": {username: "user1"}, "Error_when_username_is_empty": {wantErr: true}, "Error_when_user_does_not_exist": {username: "doesnotexist", wantErr: true}, @@ -259,17 +259,17 @@ func TestEnableUser(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { if tc.sourceDB == "" { - tc.sourceDB = "disabled-user.db.yaml" + tc.sourceDB = "locked-user.db.yaml" } client := newUserServiceClient(t, tc.sourceDB) - _, err := client.EnableUser(context.Background(), &authd.EnableUserRequest{Name: tc.username}) + _, err := client.UnlockUser(context.Background(), &authd.UnlockUserRequest{Name: tc.username}) if tc.wantErr { - require.Error(t, err, "EnableUser should return an error, but did not") + require.Error(t, err, "UnlockUser should return an error, but did not") return } - require.NoError(t, err, "EnableUser should not return an error, but did") + require.NoError(t, err, "UnlockUser should not return an error, but did") }) } } diff --git a/internal/users/db/db_test.go b/internal/users/db/db_test.go index 7eb56b62be..dbe7a5b719 100644 --- a/internal/users/db/db_test.go +++ b/internal/users/db/db_test.go @@ -842,18 +842,18 @@ func TestUpdateBrokerForUser(t *testing.T) { require.Error(t, err, "UpdateBrokerForUser for a nonexistent user should return an error") } -func TestUpdateDisabledFieldForUser(t *testing.T) { +func TestUpdateLockedFieldForUser(t *testing.T) { t.Parallel() c := initDB(t, "one_user_and_group") // Update broker for existent user - err := c.UpdateDisabledFieldForUser("user1", true) - require.NoError(t, err, "UpdateDisabledFieldForUser for an existent user should not return an error") + err := c.UpdateLockedFieldForUser("user1", true) + require.NoError(t, err, "UpdateLockedFieldForUser for an existent user should not return an error") // Error when updating broker for nonexistent user - err = c.UpdateDisabledFieldForUser("nonexistent", false) - require.Error(t, err, "UpdateDisabledFieldForUser for a nonexistent user should return an error") + err = c.UpdateLockedFieldForUser("nonexistent", false) + require.Error(t, err, "UpdateLockedFieldForUser for a nonexistent user should return an error") } func TestRemoveDb(t *testing.T) { diff --git a/internal/users/db/sql/create_schema.sql b/internal/users/db/sql/create_schema.sql index 23dc324895..f08c89e399 100644 --- a/internal/users/db/sql/create_schema.sql +++ b/internal/users/db/sql/create_schema.sql @@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS users ( dir TEXT DEFAULT "", shell TEXT DEFAULT "/bin/bash", broker_id TEXT DEFAULT "", - disabled BOOLEAN DEFAULT FALSE + locked BOOLEAN DEFAULT FALSE ); CREATE UNIQUE INDEX "idx_user_name" ON users ("name"); diff --git a/internal/users/db/update.go b/internal/users/db/update.go index 34445b5d96..9029d2df09 100644 --- a/internal/users/db/update.go +++ b/internal/users/db/update.go @@ -183,12 +183,12 @@ func (m *Manager) UpdateBrokerForUser(username, brokerID string) error { return nil } -// UpdateDisabledFieldForUser sets the Disabled field to a given value for a user. -func (m *Manager) UpdateDisabledFieldForUser(username string, disabled bool) error { - query := `UPDATE users SET disabled = ? WHERE name = ?` - res, err := m.db.Exec(query, disabled, username) +// UpdateLockedFieldForUser sets the "locked" field of a user record. +func (m *Manager) UpdateLockedFieldForUser(username string, locked bool) error { + query := `UPDATE users SET locked = ? WHERE name = ?` + res, err := m.db.Exec(query, locked, username) if err != nil { - return fmt.Errorf("failed to update disabled field for user: %w", err) + return fmt.Errorf("failed to update locked field for user: %w", err) } rowsAffected, err := res.RowsAffected() if err != nil { diff --git a/internal/users/db/users.go b/internal/users/db/users.go index 0fcd84d889..90369ca933 100644 --- a/internal/users/db/users.go +++ b/internal/users/db/users.go @@ -10,9 +10,9 @@ import ( "github.com/ubuntu/authd/log" ) -const allUserColumns = "name, uid, gid, gecos, dir, shell, broker_id, disabled" -const publicUserColumns = "name, uid, gid, gecos, dir, shell, broker_id, disabled" -const allUserColumnsWithPlaceholders = "name = ?, uid = ?, gid = ?, gecos = ?, dir = ?, shell = ?, broker_id = ?, disabled = ?" +const allUserColumns = "name, uid, gid, gecos, dir, shell, broker_id, locked" +const publicUserColumns = "name, uid, gid, gecos, dir, shell, broker_id, locked" +const allUserColumnsWithPlaceholders = "name = ?, uid = ?, gid = ?, gecos = ?, dir = ?, shell = ?, broker_id = ?, locked = ?" // UserRow represents a user row in the database. type UserRow struct { @@ -26,7 +26,7 @@ type UserRow struct { // BrokerID specifies the broker the user last successfully authenticated with. BrokerID string `yaml:"broker_id,omitempty"` - Disabled bool `yaml:"disabled,omitempty"` + Locked bool `yaml:"locked,omitempty"` } // NewUserRow creates a new UserRow. @@ -51,7 +51,7 @@ func userByID(db queryable, uid uint32) (UserRow, error) { row := db.QueryRow(query, uid) var u UserRow - err := row.Scan(&u.Name, &u.UID, &u.GID, &u.Gecos, &u.Dir, &u.Shell, &u.BrokerID, &u.Disabled) + err := row.Scan(&u.Name, &u.UID, &u.GID, &u.Gecos, &u.Dir, &u.Shell, &u.BrokerID, &u.Locked) if errors.Is(err, sql.ErrNoRows) { return UserRow{}, NewUIDNotFoundError(uid) } @@ -75,7 +75,7 @@ func userByName(db queryable, name string) (UserRow, error) { row := db.QueryRow(query, name) var u UserRow - err := row.Scan(&u.Name, &u.UID, &u.GID, &u.Gecos, &u.Dir, &u.Shell, &u.BrokerID, &u.Disabled) + err := row.Scan(&u.Name, &u.UID, &u.GID, &u.Gecos, &u.Dir, &u.Shell, &u.BrokerID, &u.Locked) if errors.Is(err, sql.ErrNoRows) { return UserRow{}, NewUserNotFoundError(name) } @@ -102,7 +102,7 @@ func allUsers(db queryable) ([]UserRow, error) { var users []UserRow for rows.Next() { var u UserRow - err := rows.Scan(&u.Name, &u.UID, &u.GID, &u.Gecos, &u.Dir, &u.Shell, &u.BrokerID, &u.Disabled) + err := rows.Scan(&u.Name, &u.UID, &u.GID, &u.Gecos, &u.Dir, &u.Shell, &u.BrokerID, &u.Locked) if err != nil { return nil, fmt.Errorf("scan error: %w", err) } @@ -156,7 +156,7 @@ func userExists(db queryable, u UserRow) (bool, error) { func insertUser(db queryable, u UserRow) error { log.Debugf(context.Background(), "Inserting user %v", u.Name) query := fmt.Sprintf(`INSERT INTO users (%s) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, allUserColumns) - _, err := db.Exec(query, u.Name, u.UID, u.GID, u.Gecos, u.Dir, u.Shell, u.BrokerID, u.Disabled) + _, err := db.Exec(query, u.Name, u.UID, u.GID, u.Gecos, u.Dir, u.Shell, u.BrokerID, u.Locked) if err != nil { return fmt.Errorf("insert user error: %w", err) } @@ -167,7 +167,7 @@ func insertUser(db queryable, u UserRow) error { func updateUserByID(db queryable, u UserRow) error { log.Debugf(context.Background(), "Updating user %v", u.Name) query := fmt.Sprintf(`UPDATE users SET %s WHERE uid = ?`, allUserColumnsWithPlaceholders) - _, err := db.Exec(query, u.Name, u.UID, u.GID, u.Gecos, u.Dir, u.Shell, u.BrokerID, u.Disabled, u.UID) + _, err := db.Exec(query, u.Name, u.UID, u.GID, u.Gecos, u.Dir, u.Shell, u.BrokerID, u.Locked, u.UID) if err != nil { return fmt.Errorf("update user error: %w", err) } diff --git a/internal/users/manager.go b/internal/users/manager.go index 064d2aca44..9817ac1ef0 100644 --- a/internal/users/manager.go +++ b/internal/users/manager.go @@ -468,32 +468,32 @@ func (m *Manager) UpdateBrokerForUser(username, brokerID string) error { return nil } -// DisableUser sets the Disabled field to true for the given user. -func (m *Manager) DisableUser(username string) error { - if err := m.db.UpdateDisabledFieldForUser(username, true); err != nil { +// LockUser sets the "locked" field to true for the given user. +func (m *Manager) LockUser(username string) error { + if err := m.db.UpdateLockedFieldForUser(username, true); err != nil { return err } return nil } -// EnableUser sets the Disabled field to false for the given user. -func (m *Manager) EnableUser(username string) error { - if err := m.db.UpdateDisabledFieldForUser(username, false); err != nil { +// UnlockUser sets the "locked" field to false for the given user. +func (m *Manager) UnlockUser(username string) error { + if err := m.db.UpdateLockedFieldForUser(username, false); err != nil { return err } return nil } -// IsUserDisabled returns true if the user with the given user name is disabled, false otherwise. -func (m *Manager) IsUserDisabled(username string) (bool, error) { +// IsUserLocked returns true if the user with the given user name is locked, false otherwise. +func (m *Manager) IsUserLocked(username string) (bool, error) { u, err := m.db.UserByName(username) if err != nil { return false, err } - return u.Disabled, nil + return u.Locked, nil } // UserByName returns the user information for the given user name. diff --git a/internal/users/manager_test.go b/internal/users/manager_test.go index b91c986f45..69e5ebffa9 100644 --- a/internal/users/manager_test.go +++ b/internal/users/manager_test.go @@ -773,7 +773,7 @@ func TestUpdateBrokerForUser(t *testing.T) { } //nolint:dupl // This is not a duplicate test -func TestDisableUser(t *testing.T) { +func TestLockUser(t *testing.T) { tests := map[string]struct { username string @@ -782,7 +782,7 @@ func TestDisableUser(t *testing.T) { wantErr bool wantErrType error }{ - "Successfully_disable_user": {}, + "Successfully_lock_user": {}, "Error_if_user_does_not_exist": {username: "doesnotexist", wantErrType: db.NoDataFoundError{}}, } @@ -803,7 +803,7 @@ func TestDisableUser(t *testing.T) { require.NoError(t, err, "Setup: could not create database from testdata") m := newManagerForTests(t, dbDir) - err = m.DisableUser(tc.username) + err = m.LockUser(tc.username) requireErrorAssertions(t, err, tc.wantErrType, tc.wantErr) if tc.wantErrType != nil || tc.wantErr { @@ -819,7 +819,7 @@ func TestDisableUser(t *testing.T) { } //nolint:dupl // This is not a duplicate test -func TestEnableUser(t *testing.T) { +func TestUnlockUser(t *testing.T) { tests := map[string]struct { username string @@ -841,7 +841,7 @@ func TestEnableUser(t *testing.T) { tc.username = "user1" } if tc.dbFile == "" { - tc.dbFile = "disabled_user" + tc.dbFile = "locked_user" } dbDir := t.TempDir() @@ -849,7 +849,7 @@ func TestEnableUser(t *testing.T) { require.NoError(t, err, "Setup: could not create database from testdata") m := newManagerForTests(t, dbDir) - err = m.EnableUser(tc.username) + err = m.UnlockUser(tc.username) requireErrorAssertions(t, err, tc.wantErrType, tc.wantErr) if tc.wantErrType != nil || tc.wantErr { diff --git a/internal/users/testdata/db/disabled_user.db.yaml b/internal/users/testdata/db/locked_user.db.yaml similarity index 93% rename from internal/users/testdata/db/disabled_user.db.yaml rename to internal/users/testdata/db/locked_user.db.yaml index a953e84b62..398bd6a9ca 100644 --- a/internal/users/testdata/db/disabled_user.db.yaml +++ b/internal/users/testdata/db/locked_user.db.yaml @@ -8,7 +8,7 @@ users: dir: /home/user1 shell: /bin/bash broker_id: broker-id - disabled: true + locked: true groups: - name: group1 gid: 11111 diff --git a/internal/users/testdata/golden/TestDisableUser/Successfully_disable_user b/internal/users/testdata/golden/TestLockUser/Successfully_lock_user similarity index 98% rename from internal/users/testdata/golden/TestDisableUser/Successfully_disable_user rename to internal/users/testdata/golden/TestLockUser/Successfully_lock_user index 256fcbe5fc..5082abe9bb 100644 --- a/internal/users/testdata/golden/TestDisableUser/Successfully_disable_user +++ b/internal/users/testdata/golden/TestLockUser/Successfully_lock_user @@ -8,7 +8,7 @@ users: dir: /home/user1 shell: /bin/bash broker_id: broker-id - disabled: true + locked: true - name: user2 uid: 2222 gid: 22222 diff --git a/internal/users/testdata/golden/TestEnableUser/Successfully_enable_user b/internal/users/testdata/golden/TestUnlockUser/Successfully_enable_user similarity index 100% rename from internal/users/testdata/golden/TestEnableUser/Successfully_enable_user rename to internal/users/testdata/golden/TestUnlockUser/Successfully_enable_user From 7f92f877f0adb7fada313a93f9c4dd10fad0c405 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Fri, 13 Jun 2025 10:52:35 +0200 Subject: [PATCH 12/55] Avoid allUsers() in migration to lowercase names That function depends on columns that might not exist yet, if the column is only added by a later migration. --- internal/users/db/migration.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/internal/users/db/migration.go b/internal/users/db/migration.go index 1031cb1a8f..153fab68ec 100644 --- a/internal/users/db/migration.go +++ b/internal/users/db/migration.go @@ -171,15 +171,20 @@ var schemaMigrations = []schemaMigration{ err = commitOrRollBackTransaction(err, tx) }() - users, err := allUsers(tx) + rows, err := tx.Query(`SELECT name FROM users`) if err != nil { return fmt.Errorf("failed to get users from database: %w", err) } + defer rows.Close() var oldNames, newNames []string - for _, u := range users { - oldNames = append(oldNames, u.Name) - newNames = append(newNames, strings.ToLower(u.Name)) + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + return fmt.Errorf("failed to scan user name: %w", err) + } + oldNames = append(oldNames, name) + newNames = append(newNames, strings.ToLower(name)) } if err := renameUsersInGroupFile(oldNames, newNames); err != nil { From ead952bf7608cfd4cf901398aafcb13ea951d316 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Fri, 13 Jun 2025 10:32:59 +0200 Subject: [PATCH 13/55] Create databases for migration tests from SQLite dumps Z_ForTests_CreateDBFromYAML creates the database with the current schema, but we want to test migrating a database with an old schema. We don't want to commit a binary database, so we create it from a SQLite dump instead. To produce the SQLite dumps, I applied this patch and ran the tests: diff --git a/internal/users/db/testutils.go b/internal/users/db/testutils.go index af7fe3564..5f16a63a8 100644 --- a/internal/users/db/testutils.go +++ b/internal/users/db/testutils.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "os" + "os/exec" "path/filepath" "sort" @@ -201,6 +202,14 @@ func createDBFromYAMLReader(r io.Reader, destDir string) (err error) { } log.Debug(context.Background(), "Database created") + + cmd := exec.Command("sqlite3", db.path, ".dump") + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to dump database: %w, output: %s", err, string(out)) + } + fmt.Printf("XXX: Database dump:\n%s", string(out)) + return nil } --- internal/users/db/db_test.go | 28 +++++----- ...rs_multiple_groups_fully_uppercase.db.yaml | 21 -------- ..._users_multiple_groups_fully_uppercase.sql | 42 +++++++++++++++ ...ers_multiple_groups_with_uppercase.db.yaml | 21 -------- ...e_users_multiple_groups_with_uppercase.sql | 42 +++++++++++++++ internal/users/db/testutils.go | 52 +++++++++++++++++++ 6 files changed, 150 insertions(+), 56 deletions(-) delete mode 100644 internal/users/db/testdata/one_users_multiple_groups_fully_uppercase.db.yaml create mode 100644 internal/users/db/testdata/one_users_multiple_groups_fully_uppercase.sql delete mode 100644 internal/users/db/testdata/one_users_multiple_groups_with_uppercase.db.yaml create mode 100644 internal/users/db/testdata/one_users_multiple_groups_with_uppercase.sql diff --git a/internal/users/db/db_test.go b/internal/users/db/db_test.go index dbe7a5b719..b2ac71c063 100644 --- a/internal/users/db/db_test.go +++ b/internal/users/db/db_test.go @@ -118,8 +118,8 @@ func TestDatabaseRemovedWhenSchemaCreationFails(t *testing.T) { func TestMigrationToLowercaseUserAndGroupNames(t *testing.T) { // Create a database from the testdata dbDir := t.TempDir() - dbFile := "one_users_multiple_groups_with_uppercase.db.yaml" - err := db.Z_ForTests_CreateDBFromYAML(filepath.Join("testdata", dbFile), dbDir) + sqlDump := "one_users_multiple_groups_with_uppercase.sql" + err := db.Z_ForTests_CreateDBFromDump(filepath.Join("testdata", sqlDump), dbDir) require.NoError(t, err, "Setup: could not create database from testdata") // Create a temporary user group file for testing @@ -189,8 +189,8 @@ func TestMigrationToLowercaseUserAndGroupNamesEmptyDB(t *testing.T) { func TestMigrationToLowercaseUserAndGroupNamesAlreadyUpdated(t *testing.T) { // Create a database from the testdata dbDir := t.TempDir() - dbFile := "one_users_multiple_groups_with_uppercase.db.yaml" - err := db.Z_ForTests_CreateDBFromYAML(filepath.Join("testdata", dbFile), dbDir) + sqlDump := "one_users_multiple_groups_with_uppercase.sql" + err := db.Z_ForTests_CreateDBFromDump(filepath.Join("testdata", sqlDump), dbDir) require.NoError(t, err, "Setup: could not create database from testdata") // Create a temporary user group file for testing @@ -220,8 +220,8 @@ func TestMigrationToLowercaseUserAndGroupNamesAlreadyUpdated(t *testing.T) { func TestMigrationToLowercaseUserAndGroupNamesWithSymlinkedGroupFile(t *testing.T) { // Create a database from the testdata dbDir := t.TempDir() - dbFile := "one_users_multiple_groups_with_uppercase.db.yaml" - err := db.Z_ForTests_CreateDBFromYAML(filepath.Join("testdata", dbFile), dbDir) + sqlDump := "one_users_multiple_groups_with_uppercase.sql" + err := db.Z_ForTests_CreateDBFromDump(filepath.Join("testdata", sqlDump), dbDir) require.NoError(t, err, "Setup: could not create database from testdata") // Create a temporary user group file for testing @@ -271,8 +271,8 @@ func TestMigrationToLowercaseUserAndGroupNamesWithSymlinkedGroupFile(t *testing. func TestMigrationToLowercaseUserAndGroupNamesWithPreviousBackup(t *testing.T) { // Create a database from the testdata dbDir := t.TempDir() - dbFile := "one_users_multiple_groups_fully_uppercase.db.yaml" - err := db.Z_ForTests_CreateDBFromYAML(filepath.Join("testdata", dbFile), dbDir) + sqlDump := "one_users_multiple_groups_fully_uppercase.sql" + err := db.Z_ForTests_CreateDBFromDump(filepath.Join("testdata", sqlDump), dbDir) require.NoError(t, err, "Setup: could not create database from testdata") // Create a temporary user group file for testing @@ -318,8 +318,8 @@ func TestMigrationToLowercaseUserAndGroupNamesWithPreviousBackup(t *testing.T) { func TestMigrationToLowercaseUserAndGroupNamesWithSymlinkedPreviousBackup(t *testing.T) { // Create a database from the testdata dbDir := t.TempDir() - dbFile := "one_users_multiple_groups_fully_uppercase.db.yaml" - err := db.Z_ForTests_CreateDBFromYAML(filepath.Join("testdata", dbFile), dbDir) + sqlDump := "one_users_multiple_groups_fully_uppercase.sql" + err := db.Z_ForTests_CreateDBFromDump(filepath.Join("testdata", sqlDump), dbDir) require.NoError(t, err, "Setup: could not create database from testdata") // Create a temporary user group file for testing @@ -375,8 +375,8 @@ func TestMigrationToLowercaseUserAndGroupNamesWithSymlinkedPreviousBackup(t *tes func TestMigrationToLowercaseUserAndGroupNamesFails(t *testing.T) { // Create a database from the testdata dbDir := t.TempDir() - dbFile := "one_users_multiple_groups_fully_uppercase.db.yaml" - err := db.Z_ForTests_CreateDBFromYAML(filepath.Join("testdata", dbFile), dbDir) + sqlDump := "one_users_multiple_groups_fully_uppercase.sql" + err := db.Z_ForTests_CreateDBFromDump(filepath.Join("testdata", sqlDump), dbDir) require.NoError(t, err, "Setup: could not create database from testdata") // Create a temporary user group file for testing @@ -413,8 +413,8 @@ func TestMigrationToLowercaseUserAndGroupNamesFails(t *testing.T) { func TestMigrationToLowercaseUserAndGroupNamesWithBackupFailure(t *testing.T) { // Create a database from the testdata dbDir := t.TempDir() - dbFile := "one_users_multiple_groups_with_uppercase.db.yaml" - err := db.Z_ForTests_CreateDBFromYAML(filepath.Join("testdata", dbFile), dbDir) + sqlDump := "one_users_multiple_groups_with_uppercase.sql" + err := db.Z_ForTests_CreateDBFromDump(filepath.Join("testdata", sqlDump), dbDir) require.NoError(t, err, "Setup: could not create database from testdata") // Create a temporary user group file for testing diff --git a/internal/users/db/testdata/one_users_multiple_groups_fully_uppercase.db.yaml b/internal/users/db/testdata/one_users_multiple_groups_fully_uppercase.db.yaml deleted file mode 100644 index 52e8d06c22..0000000000 --- a/internal/users/db/testdata/one_users_multiple_groups_fully_uppercase.db.yaml +++ /dev/null @@ -1,21 +0,0 @@ -users: - - name: TESTUSER - uid: 1111 - gid: 11111 - gecos: testuser gecos - dir: /home/testuser - shell: /bin/bash - broker_id: broker-id -groups: - - name: testgroup - gid: 11111 - ugid: "12345678" - - name: TESTGROUP - gid: 22222 - ugid: "56781234" -users_to_groups: - - uid: 1111 - gid: 11111 - - uid: 1111 - gid: 22222 -schema_version: 0 diff --git a/internal/users/db/testdata/one_users_multiple_groups_fully_uppercase.sql b/internal/users/db/testdata/one_users_multiple_groups_fully_uppercase.sql new file mode 100644 index 0000000000..fb50c5c091 --- /dev/null +++ b/internal/users/db/testdata/one_users_multiple_groups_fully_uppercase.sql @@ -0,0 +1,42 @@ +PRAGMA foreign_keys=OFF; +BEGIN TRANSACTION; +CREATE TABLE users ( + name TEXT NOT NULL, -- Uniqueness is enforced by the index below + uid INT PRIMARY KEY, -- Uniqueness and not NULL is enforced by PRIMARY KEY + gid INT NOT NULL, + gecos TEXT DEFAULT "", + dir TEXT DEFAULT "", + shell TEXT DEFAULT "/bin/bash", + broker_id TEXT DEFAULT "" +); +INSERT INTO users VALUES('TESTUSER',1111,11111,'testuser gecos','/home/testuser','/bin/bash','broker-id'); +CREATE TABLE GROUPS ( + name TEXT NOT NULL, -- Uniqueness is enforced by the index below + gid INT PRIMARY KEY, -- Uniqueness and not NULL is enforced by PRIMARY KEY + ugid INT NOT NULL -- Uniqueness is enforced by the index below +); +INSERT INTO "GROUPS" VALUES('testgroup',11111,12345678); +INSERT INTO "GROUPS" VALUES('TESTGROUP',22222,56781234); +CREATE TABLE users_to_groups ( + uid INT NOT NULL, + gid INT NOT NULL, + PRIMARY KEY (uid, gid), + FOREIGN KEY (uid) REFERENCES users (uid) ON DELETE CASCADE, + FOREIGN KEY (gid) REFERENCES GROUPS (gid) ON DELETE CASCADE +); +INSERT INTO users_to_groups VALUES(1111,11111); +INSERT INTO users_to_groups VALUES(1111,22222); +CREATE TABLE users_to_local_groups ( + uid INT NOT NULL, + group_name TEXT NOT NULL, + PRIMARY KEY (uid, group_name), + FOREIGN KEY (uid) REFERENCES users (uid) ON DELETE CASCADE +); +CREATE TABLE schema_version ( + version INT PRIMARY KEY +); +INSERT INTO schema_version VALUES(0); +CREATE UNIQUE INDEX "idx_user_name" ON users ("name"); +CREATE UNIQUE INDEX "idx_group_name" ON GROUPS ("name"); +CREATE UNIQUE INDEX "idx_group_ugid" ON GROUPS ("ugid"); +COMMIT; diff --git a/internal/users/db/testdata/one_users_multiple_groups_with_uppercase.db.yaml b/internal/users/db/testdata/one_users_multiple_groups_with_uppercase.db.yaml deleted file mode 100644 index 1dd632c19c..0000000000 --- a/internal/users/db/testdata/one_users_multiple_groups_with_uppercase.db.yaml +++ /dev/null @@ -1,21 +0,0 @@ -users: - - name: TestUser - uid: 1111 - gid: 11111 - gecos: testuser gecos - dir: /home/testuser - shell: /bin/bash - broker_id: broker-id -groups: - - name: testgroup - gid: 11111 - ugid: "12345678" - - name: TestGroup - gid: 22222 - ugid: "56781234" -users_to_groups: - - uid: 1111 - gid: 11111 - - uid: 1111 - gid: 22222 -schema_version: 0 diff --git a/internal/users/db/testdata/one_users_multiple_groups_with_uppercase.sql b/internal/users/db/testdata/one_users_multiple_groups_with_uppercase.sql new file mode 100644 index 0000000000..53664d2c6a --- /dev/null +++ b/internal/users/db/testdata/one_users_multiple_groups_with_uppercase.sql @@ -0,0 +1,42 @@ +PRAGMA foreign_keys=OFF; +BEGIN TRANSACTION; +CREATE TABLE users ( + name TEXT NOT NULL, -- Uniqueness is enforced by the index below + uid INT PRIMARY KEY, -- Uniqueness and not NULL is enforced by PRIMARY KEY + gid INT NOT NULL, + gecos TEXT DEFAULT "", + dir TEXT DEFAULT "", + shell TEXT DEFAULT "/bin/bash", + broker_id TEXT DEFAULT "" +); +INSERT INTO users VALUES('TestUser',1111,11111,'testuser gecos','/home/testuser','/bin/bash','broker-id'); +CREATE TABLE GROUPS ( + name TEXT NOT NULL, -- Uniqueness is enforced by the index below + gid INT PRIMARY KEY, -- Uniqueness and not NULL is enforced by PRIMARY KEY + ugid INT NOT NULL -- Uniqueness is enforced by the index below +); +INSERT INTO "GROUPS" VALUES('testgroup',11111,12345678); +INSERT INTO "GROUPS" VALUES('TestGroup',22222,56781234); +CREATE TABLE users_to_groups ( + uid INT NOT NULL, + gid INT NOT NULL, + PRIMARY KEY (uid, gid), + FOREIGN KEY (uid) REFERENCES users (uid) ON DELETE CASCADE, + FOREIGN KEY (gid) REFERENCES GROUPS (gid) ON DELETE CASCADE +); +INSERT INTO users_to_groups VALUES(1111,11111); +INSERT INTO users_to_groups VALUES(1111,22222); +CREATE TABLE users_to_local_groups ( + uid INT NOT NULL, + group_name TEXT NOT NULL, + PRIMARY KEY (uid, group_name), + FOREIGN KEY (uid) REFERENCES users (uid) ON DELETE CASCADE +); +CREATE TABLE schema_version ( + version INT PRIMARY KEY +); +INSERT INTO schema_version VALUES(0); +CREATE UNIQUE INDEX "idx_user_name" ON users ("name"); +CREATE UNIQUE INDEX "idx_group_name" ON GROUPS ("name"); +CREATE UNIQUE INDEX "idx_group_ugid" ON GROUPS ("ugid"); +COMMIT; diff --git a/internal/users/db/testutils.go b/internal/users/db/testutils.go index 73499182a8..4ad09840ae 100644 --- a/internal/users/db/testutils.go +++ b/internal/users/db/testutils.go @@ -5,6 +5,7 @@ package db import ( "context" + "database/sql" "errors" "fmt" "io" @@ -12,6 +13,8 @@ import ( "path/filepath" "sort" + "github.com/ubuntu/authd/internal/consts" + "github.com/ubuntu/authd/internal/fileutils" "github.com/ubuntu/authd/internal/testsdetection" "github.com/ubuntu/authd/log" "gopkg.in/yaml.v3" @@ -200,3 +203,52 @@ func createDBFromYAMLReader(r io.Reader, destDir string) (err error) { log.Debug(context.Background(), "Database created") return nil } + +// Z_ForTests_CreateDBFromDump creates the database from the provided SQLite dump file. +// +// nolint:revive,nolintlint // We want to use underscores in the function name here. +func Z_ForTests_CreateDBFromDump(dumpFile, destDir string) error { + testsdetection.MustBeTesting() + + dumpFile, err := filepath.Abs(dumpFile) + if err != nil { + return err + } + + log.Debugf(context.Background(), "Creating SQLite database from dump %s", dumpFile) + + dbPath := filepath.Join(destDir, consts.DefaultDatabaseFileName) + if err := os.MkdirAll(destDir, 0700); err != nil { + return fmt.Errorf("could not create database directory %s: %w", destDir, err) + } + if err := fileutils.Touch(dbPath); err != nil { + return fmt.Errorf("could not create database file %s: %w", dbPath, err) + } + + db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared", dbPath)) + if err != nil { + return fmt.Errorf("could not open database file %s: %w", dbPath, err) + } + + // Enable foreign key support (this needs to be done for each connection, so we can't do it in the schema). + _, err = db.Exec("PRAGMA foreign_keys = ON;") + if err != nil { + return fmt.Errorf("failed to enable foreign keys: %w", err) + } + + dumpContent, err := os.ReadFile(dumpFile) + if err != nil { + return fmt.Errorf("could not read dump file %s: %w", dumpFile, err) + } + // Execute the dump content. + _, err = db.Exec(string(dumpContent)) + if err != nil { + return fmt.Errorf("could not execute dump content: %w", err) + } + + if err := db.Close(); err != nil { + return fmt.Errorf("could not close database: %w", err) + } + + return nil +} From a9467aa1aecf2dc958a264a8fdfc763778cafc36 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Fri, 13 Jun 2025 11:35:35 +0200 Subject: [PATCH 14/55] Add migration to add column 'locked' to users table --- internal/users/db/migration.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/internal/users/db/migration.go b/internal/users/db/migration.go index 153fab68ec..47dde36614 100644 --- a/internal/users/db/migration.go +++ b/internal/users/db/migration.go @@ -204,6 +204,40 @@ var schemaMigrations = []schemaMigration{ return err }, }, + { + description: "Add column 'locked' to users table", + migrate: func(m *Manager) error { + // Start a transaction to ensure atomicity + tx, err := m.db.Begin() + if err != nil { + return fmt.Errorf("failed to start transaction: %w", err) + } + + // Ensure the transaction is committed or rolled back + defer func() { + err = commitOrRollBackTransaction(err, tx) + }() + + // Check if the 'locked' column already exists + var exists bool + err = tx.QueryRow("SELECT EXISTS(SELECT 1 FROM pragma_table_info('users') WHERE name = 'locked')").Scan(&exists) + if err != nil { + return fmt.Errorf("failed to check if 'locked' column exists: %w", err) + } + if exists { + log.Debug(context.Background(), "'locked' column already exists in users table, skipping migration") + return nil + } + + // Add the 'locked' column to the users table + _, err = tx.Exec("ALTER TABLE users ADD COLUMN locked BOOLEAN DEFAULT FALSE") + if err != nil { + return fmt.Errorf("failed to add 'locked' column to users table: %w", err) + } + + return nil + }, + }, } func (m *Manager) maybeApplyMigrations() error { From 8a1f7d06c800541e6182c34cd86db2203c255472 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Fri, 13 Jun 2025 11:39:22 +0200 Subject: [PATCH 15/55] Test migration to add 'locked' column to users table --- internal/users/db/db_test.go | 18 +++++++++ .../TestMigrationAddLockedColumnToUsersTable | 18 +++++++++ ...e_user_and_group_without_locked_column.sql | 40 +++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 internal/users/db/testdata/golden/TestMigrationAddLockedColumnToUsersTable create mode 100644 internal/users/db/testdata/one_user_and_group_without_locked_column.sql diff --git a/internal/users/db/db_test.go b/internal/users/db/db_test.go index b2ac71c063..220f66d969 100644 --- a/internal/users/db/db_test.go +++ b/internal/users/db/db_test.go @@ -455,6 +455,24 @@ func TestMigrationToLowercaseUserAndGroupNamesWithBackupFailure(t *testing.T) { golden.CheckOrUpdate(t, string(userGroupContent), golden.WithPath("groups")) } +func TestMigrationAddLockedColumnToUsersTable(t *testing.T) { + // Create a database from the testdata + dbDir := t.TempDir() + sqlDump := "one_user_and_group_without_locked_column.sql" + err := db.Z_ForTests_CreateDBFromDump(filepath.Join("testdata", sqlDump), dbDir) + require.NoError(t, err, "Setup: could not create database from testdata") + + // Run the migrations + m, err := db.New(dbDir) + require.NoError(t, err) + + // Check the content of the SQLite database + dbContent, err := db.Z_ForTests_DumpNormalizedYAML(m) + require.NoError(t, err) + + golden.CheckOrUpdate(t, dbContent) +} + func TestUpdateUserEntry(t *testing.T) { t.Parallel() diff --git a/internal/users/db/testdata/golden/TestMigrationAddLockedColumnToUsersTable b/internal/users/db/testdata/golden/TestMigrationAddLockedColumnToUsersTable new file mode 100644 index 0000000000..df1e19d597 --- /dev/null +++ b/internal/users/db/testdata/golden/TestMigrationAddLockedColumnToUsersTable @@ -0,0 +1,18 @@ +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 +schema_version: 2 diff --git a/internal/users/db/testdata/one_user_and_group_without_locked_column.sql b/internal/users/db/testdata/one_user_and_group_without_locked_column.sql new file mode 100644 index 0000000000..6272520784 --- /dev/null +++ b/internal/users/db/testdata/one_user_and_group_without_locked_column.sql @@ -0,0 +1,40 @@ +PRAGMA foreign_keys=OFF; +BEGIN TRANSACTION; +CREATE TABLE users ( + name TEXT NOT NULL, -- Uniqueness is enforced by the index below + uid INT PRIMARY KEY, -- Uniqueness and not NULL is enforced by PRIMARY KEY + gid INT NOT NULL, + gecos TEXT DEFAULT "", + dir TEXT DEFAULT "", + shell TEXT DEFAULT "/bin/bash", + broker_id TEXT DEFAULT "" +); +INSERT INTO users VALUES('user1',1111,11111,replace('User1 gecos\nOn multiple lines','\n',char(10)),'/home/user1','/bin/bash','broker-id'); +CREATE TABLE GROUPS ( + name TEXT NOT NULL, -- Uniqueness is enforced by the index below + gid INT PRIMARY KEY, -- Uniqueness and not NULL is enforced by PRIMARY KEY + ugid INT NOT NULL -- Uniqueness is enforced by the index below +); +INSERT INTO "GROUPS" VALUES('group1',11111,12345678); +CREATE TABLE users_to_groups ( + uid INT NOT NULL, + gid INT NOT NULL, + PRIMARY KEY (uid, gid), + FOREIGN KEY (uid) REFERENCES users (uid) ON DELETE CASCADE, + FOREIGN KEY (gid) REFERENCES GROUPS (gid) ON DELETE CASCADE +); +INSERT INTO users_to_groups VALUES(1111,11111); +CREATE TABLE users_to_local_groups ( + uid INT NOT NULL, + group_name TEXT NOT NULL, + PRIMARY KEY (uid, group_name), + FOREIGN KEY (uid) REFERENCES users (uid) ON DELETE CASCADE +); +CREATE TABLE schema_version ( + version INT PRIMARY KEY +); +INSERT INTO schema_version VALUES(1); +CREATE UNIQUE INDEX "idx_user_name" ON users ("name"); +CREATE UNIQUE INDEX "idx_group_name" ON GROUPS ("name"); +CREATE UNIQUE INDEX "idx_group_ugid" ON GROUPS ("ugid"); +COMMIT; From a09d5fc68341df63d2987ca44e2e52c1284a15c5 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Fri, 13 Jun 2025 11:37:21 +0200 Subject: [PATCH 16/55] Update schema version in golden files We added a new schema migration, so the schema version is now 2. --- .../Migration_if_bbolt_exists_and_sqlite_does_not_exist/db | 2 +- .../pam/testdata/golden/TestIDGeneration/Generate_ID/cache.db | 2 +- .../Denies_authentication_when_broker_times_out/cache.db | 2 +- .../Error_on_empty_data_even_if_granted/cache.db | 2 +- .../cache.db | 2 +- .../TestIsAuthenticated/Error_when_authenticating/cache.db | 2 +- .../Error_when_broker_returns_invalid_access/cache.db | 2 +- .../Error_when_broker_returns_invalid_data/cache.db | 2 +- .../Error_when_broker_returns_invalid_userinfo/cache.db | 2 +- .../Error_when_calling_second_time_without_cancelling/cache.db | 2 +- .../golden/TestIsAuthenticated/Error_when_not_root/cache.db | 2 +- .../TestIsAuthenticated/Error_when_sessionID_is_empty/cache.db | 2 +- .../TestIsAuthenticated/Error_when_there_is_no_broker/cache.db | 2 +- .../TestIsAuthenticated/Successfully_authenticate/cache.db | 2 +- .../cache.db | 2 +- .../TestIsAuthenticated/Update_existing_DB_on_success/cache.db | 2 +- .../golden/TestIsAuthenticated/Update_local_groups/cache.db | 2 +- .../cache.db | 2 +- .../cache.db | 2 +- .../Deleting_existing_user_keeps_other_group_members_intact | 2 +- .../Deleting_last_user_from_a_group_keeps_the_group_record | 2 +- .../golden/TestMigrationToLowercaseUserAndGroupNames/db | 2 +- .../TestMigrationToLowercaseUserAndGroupNamesAlreadyUpdated/db | 2 +- .../golden/TestMigrationToLowercaseUserAndGroupNamesEmptyDB/db | 2 +- .../db | 2 +- .../db | 2 +- .../db | 2 +- .../db | 2 +- .../testdata/golden/TestNew/New_with_already_existing_database | 2 +- .../golden/TestNew/New_without_any_initialized_database | 2 +- .../TestUpdateUserEntry/Add_user_to_group_from_another_user | 2 +- .../db/testdata/golden/TestUpdateUserEntry/Insert_new_user | 2 +- .../Insert_new_user_without_optional_gecos_field | 2 +- .../testdata/golden/TestUpdateUserEntry/Remove_group_from_user | 2 +- .../Remove_user_from_a_group_still_part_from_another_user | 2 +- .../Update_only_user_even_if_we_have_multiple_of_them | 2 +- .../Update_user_by_adding_a_new_default_group | 2 +- .../TestUpdateUserEntry/Update_user_by_adding_a_new_group | 2 +- .../TestUpdateUserEntry/Update_user_by_adding_a_new_local_group | 2 +- .../TestUpdateUserEntry/Update_user_by_changing_attributes | 2 +- .../Update_user_by_removing_optional_gecos_field_if_not_set | 2 +- .../golden/TestUpdateUserEntry/Update_user_by_renaming_a_group | 2 +- .../Update_user_does_not_change_homedir_if_it_exists | 2 +- .../Update_user_does_not_change_shell_if_it_exists | 2 +- .../Updating_user_with_different_capitalization | 2 +- .../users/testdata/golden/TestLockUser/Successfully_lock_user | 2 +- ...create_manager_with_GID_range_next_to_systemd_dynamic_groups | 2 +- ..._create_manager_with_UID_range_next_to_systemd_dynamic_users | 2 +- .../Successfully_create_manager_with_custom_config | 2 +- .../Successfully_create_manager_with_default_config | 2 +- .../Warns_creating_manager_with_partially_invalid_GID_ranges | 2 +- .../Warns_creating_manager_with_partially_invalid_UID_ranges | 2 +- .../testdata/golden/TestUnlockUser/Successfully_enable_user | 2 +- .../TestUpdateBrokerForUser/Successfully_update_broker_for_user | 2 +- .../GID_does_not_change_if_group_with_same_UGID_exists | 2 +- ...oes_not_change_if_group_with_same_name_and_empty_UGID_exists | 2 +- .../Names_of_authd_groups_are_stored_in_lowercase | 2 +- .../Removing_last_user_from_a_group_keeps_the_group_record | 2 +- .../testdata/golden/TestUpdateUser/Successfully_update_user | 2 +- .../Successfully_update_user_updating_local_groups | 2 +- .../Successfully_update_user_updating_local_groups_with_changes | 2 +- .../Successfully_update_user_with_different_capitalization | 2 +- .../TestUpdateUser/UID_does_not_change_if_user_already_exists | 2 +- 63 files changed, 63 insertions(+), 63 deletions(-) diff --git a/cmd/authd/daemon/testdata/golden/TestMaybeMigrateBBoltToSQLite/Migration_if_bbolt_exists_and_sqlite_does_not_exist/db b/cmd/authd/daemon/testdata/golden/TestMaybeMigrateBBoltToSQLite/Migration_if_bbolt_exists_and_sqlite_does_not_exist/db index 9a59a4ead0..9c042d2317 100644 --- a/cmd/authd/daemon/testdata/golden/TestMaybeMigrateBBoltToSQLite/Migration_if_bbolt_exists_and_sqlite_does_not_exist/db +++ b/cmd/authd/daemon/testdata/golden/TestMaybeMigrateBBoltToSQLite/Migration_if_bbolt_exists_and_sqlite_does_not_exist/db @@ -61,4 +61,4 @@ users_to_groups: gid: 44444 - uid: 4444 gid: 99999 -schema_version: 1 +schema_version: 2 diff --git a/internal/services/pam/testdata/golden/TestIDGeneration/Generate_ID/cache.db b/internal/services/pam/testdata/golden/TestIDGeneration/Generate_ID/cache.db index 375d7c41a3..ac55ff4e4b 100644 --- a/internal/services/pam/testdata/golden/TestIDGeneration/Generate_ID/cache.db +++ b/internal/services/pam/testdata/golden/TestIDGeneration/Generate_ID/cache.db @@ -17,4 +17,4 @@ users_to_groups: gid: 1111 - uid: 1111 gid: 22222 -schema_version: 1 +schema_version: 2 diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Denies_authentication_when_broker_times_out/cache.db b/internal/services/pam/testdata/golden/TestIsAuthenticated/Denies_authentication_when_broker_times_out/cache.db index 8bd98be48c..03de3ff950 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Denies_authentication_when_broker_times_out/cache.db +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Denies_authentication_when_broker_times_out/cache.db @@ -1,4 +1,4 @@ users: [] groups: [] users_to_groups: [] -schema_version: 1 +schema_version: 2 diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_on_empty_data_even_if_granted/cache.db b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_on_empty_data_even_if_granted/cache.db index 8bd98be48c..03de3ff950 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_on_empty_data_even_if_granted/cache.db +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_on_empty_data_even_if_granted/cache.db @@ -1,4 +1,4 @@ users: [] groups: [] users_to_groups: [] -schema_version: 1 +schema_version: 2 diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_on_updating_local_groups_with_unexisting_file/cache.db b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_on_updating_local_groups_with_unexisting_file/cache.db index 963c89e45e..16b6008850 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_on_updating_local_groups_with_unexisting_file/cache.db +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_on_updating_local_groups_with_unexisting_file/cache.db @@ -17,4 +17,4 @@ users_to_groups: gid: 1111 - uid: 1111 gid: 22222 -schema_version: 1 +schema_version: 2 diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_authenticating/cache.db b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_authenticating/cache.db index 8bd98be48c..03de3ff950 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_authenticating/cache.db +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_authenticating/cache.db @@ -1,4 +1,4 @@ users: [] groups: [] users_to_groups: [] -schema_version: 1 +schema_version: 2 diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_access/cache.db b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_access/cache.db index 8bd98be48c..03de3ff950 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_access/cache.db +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_access/cache.db @@ -1,4 +1,4 @@ users: [] groups: [] users_to_groups: [] -schema_version: 1 +schema_version: 2 diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_data/cache.db b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_data/cache.db index 8bd98be48c..03de3ff950 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_data/cache.db +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_data/cache.db @@ -1,4 +1,4 @@ users: [] groups: [] users_to_groups: [] -schema_version: 1 +schema_version: 2 diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_userinfo/cache.db b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_userinfo/cache.db index 8bd98be48c..03de3ff950 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_userinfo/cache.db +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_userinfo/cache.db @@ -1,4 +1,4 @@ users: [] groups: [] users_to_groups: [] -schema_version: 1 +schema_version: 2 diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_calling_second_time_without_cancelling/cache.db b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_calling_second_time_without_cancelling/cache.db index cef36229a8..79940af015 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_calling_second_time_without_cancelling/cache.db +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_calling_second_time_without_cancelling/cache.db @@ -17,4 +17,4 @@ users_to_groups: gid: 1111 - uid: 1111 gid: 22222 -schema_version: 1 +schema_version: 2 diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_not_root/cache.db b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_not_root/cache.db index 8bd98be48c..03de3ff950 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_not_root/cache.db +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_not_root/cache.db @@ -1,4 +1,4 @@ users: [] groups: [] users_to_groups: [] -schema_version: 1 +schema_version: 2 diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_sessionID_is_empty/cache.db b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_sessionID_is_empty/cache.db index 8bd98be48c..03de3ff950 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_sessionID_is_empty/cache.db +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_sessionID_is_empty/cache.db @@ -1,4 +1,4 @@ users: [] groups: [] users_to_groups: [] -schema_version: 1 +schema_version: 2 diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_there_is_no_broker/cache.db b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_there_is_no_broker/cache.db index 8bd98be48c..03de3ff950 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_there_is_no_broker/cache.db +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_there_is_no_broker/cache.db @@ -1,4 +1,4 @@ users: [] groups: [] users_to_groups: [] -schema_version: 1 +schema_version: 2 diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Successfully_authenticate/cache.db b/internal/services/pam/testdata/golden/TestIsAuthenticated/Successfully_authenticate/cache.db index 86305c92c1..b4536c012f 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Successfully_authenticate/cache.db +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Successfully_authenticate/cache.db @@ -17,4 +17,4 @@ users_to_groups: gid: 1111 - uid: 1111 gid: 22222 -schema_version: 1 +schema_version: 2 diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled/cache.db b/internal/services/pam/testdata/golden/TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled/cache.db index 4b28b6b924..f87cf6d258 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled/cache.db +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled/cache.db @@ -17,4 +17,4 @@ users_to_groups: gid: 1111 - uid: 1111 gid: 22222 -schema_version: 1 +schema_version: 2 diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Update_existing_DB_on_success/cache.db b/internal/services/pam/testdata/golden/TestIsAuthenticated/Update_existing_DB_on_success/cache.db index 00ac6473e7..bf4add81e3 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Update_existing_DB_on_success/cache.db +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Update_existing_DB_on_success/cache.db @@ -26,4 +26,4 @@ users_to_groups: gid: 88888 - uid: 77777 gid: 88888 -schema_version: 1 +schema_version: 2 diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Update_local_groups/cache.db b/internal/services/pam/testdata/golden/TestIsAuthenticated/Update_local_groups/cache.db index 533c7c30fd..f91b0c5536 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Update_local_groups/cache.db +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Update_local_groups/cache.db @@ -17,4 +17,4 @@ users_to_groups: gid: 1111 - uid: 1111 gid: 22222 -schema_version: 1 +schema_version: 2 diff --git a/internal/services/pam/testdata/golden/TestSetDefaultBrokerForUser/Set_default_broker_for_existing_user_with_no_broker/cache.db b/internal/services/pam/testdata/golden/TestSetDefaultBrokerForUser/Set_default_broker_for_existing_user_with_no_broker/cache.db index 805aa6c6af..3bd5df74ef 100644 --- a/internal/services/pam/testdata/golden/TestSetDefaultBrokerForUser/Set_default_broker_for_existing_user_with_no_broker/cache.db +++ b/internal/services/pam/testdata/golden/TestSetDefaultBrokerForUser/Set_default_broker_for_existing_user_with_no_broker/cache.db @@ -73,4 +73,4 @@ users_to_groups: gid: 55555 - uid: 5555 gid: 99999 -schema_version: 1 +schema_version: 2 diff --git a/internal/services/pam/testdata/golden/TestSetDefaultBrokerForUser/Update_default_broker_for_existing_user_with_a_broker/cache.db b/internal/services/pam/testdata/golden/TestSetDefaultBrokerForUser/Update_default_broker_for_existing_user_with_a_broker/cache.db index cf71a7de8c..30d8a3b0a4 100644 --- a/internal/services/pam/testdata/golden/TestSetDefaultBrokerForUser/Update_default_broker_for_existing_user_with_a_broker/cache.db +++ b/internal/services/pam/testdata/golden/TestSetDefaultBrokerForUser/Update_default_broker_for_existing_user_with_a_broker/cache.db @@ -72,4 +72,4 @@ users_to_groups: gid: 55555 - uid: 5555 gid: 99999 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestDeleteUser/Deleting_existing_user_keeps_other_group_members_intact b/internal/users/db/testdata/golden/TestDeleteUser/Deleting_existing_user_keeps_other_group_members_intact index d5bf84ebf8..5b781502ba 100644 --- a/internal/users/db/testdata/golden/TestDeleteUser/Deleting_existing_user_keeps_other_group_members_intact +++ b/internal/users/db/testdata/golden/TestDeleteUser/Deleting_existing_user_keeps_other_group_members_intact @@ -48,4 +48,4 @@ users_to_groups: gid: 44444 - uid: 4444 gid: 99999 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestDeleteUser/Deleting_last_user_from_a_group_keeps_the_group_record b/internal/users/db/testdata/golden/TestDeleteUser/Deleting_last_user_from_a_group_keeps_the_group_record index 2ff9b6e1f5..bbfb783e2c 100644 --- a/internal/users/db/testdata/golden/TestDeleteUser/Deleting_last_user_from_a_group_keeps_the_group_record +++ b/internal/users/db/testdata/golden/TestDeleteUser/Deleting_last_user_from_a_group_keeps_the_group_record @@ -4,4 +4,4 @@ groups: gid: 11111 ugid: "12345678" users_to_groups: [] -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNames/db b/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNames/db index da365938b4..c2312b979b 100644 --- a/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNames/db +++ b/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNames/db @@ -13,4 +13,4 @@ groups: users_to_groups: - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesAlreadyUpdated/db b/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesAlreadyUpdated/db index da365938b4..c2312b979b 100644 --- a/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesAlreadyUpdated/db +++ b/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesAlreadyUpdated/db @@ -13,4 +13,4 @@ groups: users_to_groups: - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesEmptyDB/db b/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesEmptyDB/db index 8bd98be48c..03de3ff950 100644 --- a/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesEmptyDB/db +++ b/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesEmptyDB/db @@ -1,4 +1,4 @@ users: [] groups: [] users_to_groups: [] -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesWithBackupFailure/db b/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesWithBackupFailure/db index da365938b4..c2312b979b 100644 --- a/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesWithBackupFailure/db +++ b/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesWithBackupFailure/db @@ -13,4 +13,4 @@ groups: users_to_groups: - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesWithPreviousBackup/db b/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesWithPreviousBackup/db index da365938b4..c2312b979b 100644 --- a/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesWithPreviousBackup/db +++ b/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesWithPreviousBackup/db @@ -13,4 +13,4 @@ groups: users_to_groups: - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesWithSymlinkedGroupFile/db b/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesWithSymlinkedGroupFile/db index da365938b4..c2312b979b 100644 --- a/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesWithSymlinkedGroupFile/db +++ b/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesWithSymlinkedGroupFile/db @@ -13,4 +13,4 @@ groups: users_to_groups: - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesWithSymlinkedPreviousBackup/db b/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesWithSymlinkedPreviousBackup/db index da365938b4..c2312b979b 100644 --- a/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesWithSymlinkedPreviousBackup/db +++ b/internal/users/db/testdata/golden/TestMigrationToLowercaseUserAndGroupNamesWithSymlinkedPreviousBackup/db @@ -13,4 +13,4 @@ groups: users_to_groups: - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestNew/New_with_already_existing_database b/internal/users/db/testdata/golden/TestNew/New_with_already_existing_database index 9a59a4ead0..9c042d2317 100644 --- a/internal/users/db/testdata/golden/TestNew/New_with_already_existing_database +++ b/internal/users/db/testdata/golden/TestNew/New_with_already_existing_database @@ -61,4 +61,4 @@ users_to_groups: gid: 44444 - uid: 4444 gid: 99999 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestNew/New_without_any_initialized_database b/internal/users/db/testdata/golden/TestNew/New_without_any_initialized_database index 8bd98be48c..03de3ff950 100644 --- a/internal/users/db/testdata/golden/TestNew/New_without_any_initialized_database +++ b/internal/users/db/testdata/golden/TestNew/New_without_any_initialized_database @@ -1,4 +1,4 @@ users: [] groups: [] users_to_groups: [] -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestUpdateUserEntry/Add_user_to_group_from_another_user b/internal/users/db/testdata/golden/TestUpdateUserEntry/Add_user_to_group_from_another_user index 827c703025..71f41b4662 100644 --- a/internal/users/db/testdata/golden/TestUpdateUserEntry/Add_user_to_group_from_another_user +++ b/internal/users/db/testdata/golden/TestUpdateUserEntry/Add_user_to_group_from_another_user @@ -60,4 +60,4 @@ users_to_groups: gid: 44444 - uid: 4444 gid: 99999 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestUpdateUserEntry/Insert_new_user b/internal/users/db/testdata/golden/TestUpdateUserEntry/Insert_new_user index 362a15f68b..1ead371466 100644 --- a/internal/users/db/testdata/golden/TestUpdateUserEntry/Insert_new_user +++ b/internal/users/db/testdata/golden/TestUpdateUserEntry/Insert_new_user @@ -14,4 +14,4 @@ groups: users_to_groups: - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestUpdateUserEntry/Insert_new_user_without_optional_gecos_field b/internal/users/db/testdata/golden/TestUpdateUserEntry/Insert_new_user_without_optional_gecos_field index db54d238d5..3d09a971c2 100644 --- a/internal/users/db/testdata/golden/TestUpdateUserEntry/Insert_new_user_without_optional_gecos_field +++ b/internal/users/db/testdata/golden/TestUpdateUserEntry/Insert_new_user_without_optional_gecos_field @@ -12,4 +12,4 @@ groups: users_to_groups: - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestUpdateUserEntry/Remove_group_from_user b/internal/users/db/testdata/golden/TestUpdateUserEntry/Remove_group_from_user index 36b5459a96..647653fdc9 100644 --- a/internal/users/db/testdata/golden/TestUpdateUserEntry/Remove_group_from_user +++ b/internal/users/db/testdata/golden/TestUpdateUserEntry/Remove_group_from_user @@ -17,4 +17,4 @@ groups: users_to_groups: - uid: 1111 gid: 22222 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestUpdateUserEntry/Remove_user_from_a_group_still_part_from_another_user b/internal/users/db/testdata/golden/TestUpdateUserEntry/Remove_user_from_a_group_still_part_from_another_user index 411a1de501..a5079149a7 100644 --- a/internal/users/db/testdata/golden/TestUpdateUserEntry/Remove_user_from_a_group_still_part_from_another_user +++ b/internal/users/db/testdata/golden/TestUpdateUserEntry/Remove_user_from_a_group_still_part_from_another_user @@ -58,4 +58,4 @@ users_to_groups: gid: 44444 - uid: 4444 gid: 99999 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_only_user_even_if_we_have_multiple_of_them b/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_only_user_even_if_we_have_multiple_of_them index 187cd238b6..03418546c1 100644 --- a/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_only_user_even_if_we_have_multiple_of_them +++ b/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_only_user_even_if_we_have_multiple_of_them @@ -58,4 +58,4 @@ users_to_groups: gid: 44444 - uid: 4444 gid: 99999 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_adding_a_new_default_group b/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_adding_a_new_default_group index e9f01e9d24..6b7a9c27c0 100644 --- a/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_adding_a_new_default_group +++ b/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_adding_a_new_default_group @@ -19,4 +19,4 @@ users_to_groups: gid: 11111 - uid: 1111 gid: 22222 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_adding_a_new_group b/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_adding_a_new_group index cee0af7d4b..e4576a7e71 100644 --- a/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_adding_a_new_group +++ b/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_adding_a_new_group @@ -19,4 +19,4 @@ users_to_groups: gid: 11111 - uid: 1111 gid: 22222 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_adding_a_new_local_group b/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_adding_a_new_local_group index 362a15f68b..1ead371466 100644 --- a/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_adding_a_new_local_group +++ b/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_adding_a_new_local_group @@ -14,4 +14,4 @@ groups: users_to_groups: - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_changing_attributes b/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_changing_attributes index 244a44410e..5d1fe51aeb 100644 --- a/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_changing_attributes +++ b/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_changing_attributes @@ -12,4 +12,4 @@ groups: users_to_groups: - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_removing_optional_gecos_field_if_not_set b/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_removing_optional_gecos_field_if_not_set index db54d238d5..3d09a971c2 100644 --- a/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_removing_optional_gecos_field_if_not_set +++ b/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_removing_optional_gecos_field_if_not_set @@ -12,4 +12,4 @@ groups: users_to_groups: - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_renaming_a_group b/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_renaming_a_group index 40eb47d5c3..fbecc4550e 100644 --- a/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_renaming_a_group +++ b/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_by_renaming_a_group @@ -14,4 +14,4 @@ groups: users_to_groups: - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_does_not_change_homedir_if_it_exists b/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_does_not_change_homedir_if_it_exists index 362a15f68b..1ead371466 100644 --- a/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_does_not_change_homedir_if_it_exists +++ b/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_does_not_change_homedir_if_it_exists @@ -14,4 +14,4 @@ groups: users_to_groups: - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_does_not_change_shell_if_it_exists b/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_does_not_change_shell_if_it_exists index 362a15f68b..1ead371466 100644 --- a/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_does_not_change_shell_if_it_exists +++ b/internal/users/db/testdata/golden/TestUpdateUserEntry/Update_user_does_not_change_shell_if_it_exists @@ -14,4 +14,4 @@ groups: users_to_groups: - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/db/testdata/golden/TestUpdateUserEntry/Updating_user_with_different_capitalization b/internal/users/db/testdata/golden/TestUpdateUserEntry/Updating_user_with_different_capitalization index 362a15f68b..1ead371466 100644 --- a/internal/users/db/testdata/golden/TestUpdateUserEntry/Updating_user_with_different_capitalization +++ b/internal/users/db/testdata/golden/TestUpdateUserEntry/Updating_user_with_different_capitalization @@ -14,4 +14,4 @@ groups: users_to_groups: - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestLockUser/Successfully_lock_user b/internal/users/testdata/golden/TestLockUser/Successfully_lock_user index 5082abe9bb..e10d01edca 100644 --- a/internal/users/testdata/golden/TestLockUser/Successfully_lock_user +++ b/internal/users/testdata/golden/TestLockUser/Successfully_lock_user @@ -62,4 +62,4 @@ users_to_groups: gid: 44444 - uid: 4444 gid: 99999 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestNewManager/Successfully_create_manager_with_GID_range_next_to_systemd_dynamic_groups b/internal/users/testdata/golden/TestNewManager/Successfully_create_manager_with_GID_range_next_to_systemd_dynamic_groups index 5d465349a9..7f16f7e742 100644 --- a/internal/users/testdata/golden/TestNewManager/Successfully_create_manager_with_GID_range_next_to_systemd_dynamic_groups +++ b/internal/users/testdata/golden/TestNewManager/Successfully_create_manager_with_GID_range_next_to_systemd_dynamic_groups @@ -61,4 +61,4 @@ users_to_groups: gid: 44444 - uid: 4444 gid: 99999 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestNewManager/Successfully_create_manager_with_UID_range_next_to_systemd_dynamic_users b/internal/users/testdata/golden/TestNewManager/Successfully_create_manager_with_UID_range_next_to_systemd_dynamic_users index 5d465349a9..7f16f7e742 100644 --- a/internal/users/testdata/golden/TestNewManager/Successfully_create_manager_with_UID_range_next_to_systemd_dynamic_users +++ b/internal/users/testdata/golden/TestNewManager/Successfully_create_manager_with_UID_range_next_to_systemd_dynamic_users @@ -61,4 +61,4 @@ users_to_groups: gid: 44444 - uid: 4444 gid: 99999 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestNewManager/Successfully_create_manager_with_custom_config b/internal/users/testdata/golden/TestNewManager/Successfully_create_manager_with_custom_config index 5d465349a9..7f16f7e742 100644 --- a/internal/users/testdata/golden/TestNewManager/Successfully_create_manager_with_custom_config +++ b/internal/users/testdata/golden/TestNewManager/Successfully_create_manager_with_custom_config @@ -61,4 +61,4 @@ users_to_groups: gid: 44444 - uid: 4444 gid: 99999 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestNewManager/Successfully_create_manager_with_default_config b/internal/users/testdata/golden/TestNewManager/Successfully_create_manager_with_default_config index 5d465349a9..7f16f7e742 100644 --- a/internal/users/testdata/golden/TestNewManager/Successfully_create_manager_with_default_config +++ b/internal/users/testdata/golden/TestNewManager/Successfully_create_manager_with_default_config @@ -61,4 +61,4 @@ users_to_groups: gid: 44444 - uid: 4444 gid: 99999 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestNewManager/Warns_creating_manager_with_partially_invalid_GID_ranges b/internal/users/testdata/golden/TestNewManager/Warns_creating_manager_with_partially_invalid_GID_ranges index 5d465349a9..7f16f7e742 100644 --- a/internal/users/testdata/golden/TestNewManager/Warns_creating_manager_with_partially_invalid_GID_ranges +++ b/internal/users/testdata/golden/TestNewManager/Warns_creating_manager_with_partially_invalid_GID_ranges @@ -61,4 +61,4 @@ users_to_groups: gid: 44444 - uid: 4444 gid: 99999 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestNewManager/Warns_creating_manager_with_partially_invalid_UID_ranges b/internal/users/testdata/golden/TestNewManager/Warns_creating_manager_with_partially_invalid_UID_ranges index 5d465349a9..7f16f7e742 100644 --- a/internal/users/testdata/golden/TestNewManager/Warns_creating_manager_with_partially_invalid_UID_ranges +++ b/internal/users/testdata/golden/TestNewManager/Warns_creating_manager_with_partially_invalid_UID_ranges @@ -61,4 +61,4 @@ users_to_groups: gid: 44444 - uid: 4444 gid: 99999 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestUnlockUser/Successfully_enable_user b/internal/users/testdata/golden/TestUnlockUser/Successfully_enable_user index 0f479a482b..df1e19d597 100644 --- a/internal/users/testdata/golden/TestUnlockUser/Successfully_enable_user +++ b/internal/users/testdata/golden/TestUnlockUser/Successfully_enable_user @@ -15,4 +15,4 @@ groups: users_to_groups: - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestUpdateBrokerForUser/Successfully_update_broker_for_user b/internal/users/testdata/golden/TestUpdateBrokerForUser/Successfully_update_broker_for_user index 8f27fbf1b5..85a2ac6da5 100644 --- a/internal/users/testdata/golden/TestUpdateBrokerForUser/Successfully_update_broker_for_user +++ b/internal/users/testdata/golden/TestUpdateBrokerForUser/Successfully_update_broker_for_user @@ -61,4 +61,4 @@ users_to_groups: gid: 44444 - uid: 4444 gid: 99999 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestUpdateUser/GID_does_not_change_if_group_with_same_UGID_exists b/internal/users/testdata/golden/TestUpdateUser/GID_does_not_change_if_group_with_same_UGID_exists index 4ea8612145..52c8832676 100644 --- a/internal/users/testdata/golden/TestUpdateUser/GID_does_not_change_if_group_with_same_UGID_exists +++ b/internal/users/testdata/golden/TestUpdateUser/GID_does_not_change_if_group_with_same_UGID_exists @@ -17,4 +17,4 @@ users_to_groups: gid: 1111 - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestUpdateUser/GID_does_not_change_if_group_with_same_name_and_empty_UGID_exists b/internal/users/testdata/golden/TestUpdateUser/GID_does_not_change_if_group_with_same_name_and_empty_UGID_exists index 11a33baa3a..5b2e9128ed 100644 --- a/internal/users/testdata/golden/TestUpdateUser/GID_does_not_change_if_group_with_same_name_and_empty_UGID_exists +++ b/internal/users/testdata/golden/TestUpdateUser/GID_does_not_change_if_group_with_same_name_and_empty_UGID_exists @@ -17,4 +17,4 @@ users_to_groups: gid: 1111 - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestUpdateUser/Names_of_authd_groups_are_stored_in_lowercase b/internal/users/testdata/golden/TestUpdateUser/Names_of_authd_groups_are_stored_in_lowercase index 11a33baa3a..5b2e9128ed 100644 --- a/internal/users/testdata/golden/TestUpdateUser/Names_of_authd_groups_are_stored_in_lowercase +++ b/internal/users/testdata/golden/TestUpdateUser/Names_of_authd_groups_are_stored_in_lowercase @@ -17,4 +17,4 @@ users_to_groups: gid: 1111 - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestUpdateUser/Removing_last_user_from_a_group_keeps_the_group_record b/internal/users/testdata/golden/TestUpdateUser/Removing_last_user_from_a_group_keeps_the_group_record index f744472f5e..32929b27b5 100644 --- a/internal/users/testdata/golden/TestUpdateUser/Removing_last_user_from_a_group_keeps_the_group_record +++ b/internal/users/testdata/golden/TestUpdateUser/Removing_last_user_from_a_group_keeps_the_group_record @@ -15,4 +15,4 @@ groups: users_to_groups: - uid: 1111 gid: 1111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user b/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user index 11a33baa3a..5b2e9128ed 100644 --- a/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user +++ b/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user @@ -17,4 +17,4 @@ users_to_groups: gid: 1111 - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user_updating_local_groups b/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user_updating_local_groups index 11a33baa3a..5b2e9128ed 100644 --- a/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user_updating_local_groups +++ b/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user_updating_local_groups @@ -17,4 +17,4 @@ users_to_groups: gid: 1111 - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user_updating_local_groups_with_changes b/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user_updating_local_groups_with_changes index 11a33baa3a..5b2e9128ed 100644 --- a/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user_updating_local_groups_with_changes +++ b/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user_updating_local_groups_with_changes @@ -17,4 +17,4 @@ users_to_groups: gid: 1111 - uid: 1111 gid: 11111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user_with_different_capitalization b/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user_with_different_capitalization index 575dc64034..1f611d8210 100644 --- a/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user_with_different_capitalization +++ b/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user_with_different_capitalization @@ -15,4 +15,4 @@ groups: users_to_groups: - uid: 1111 gid: 1111 -schema_version: 1 +schema_version: 2 diff --git a/internal/users/testdata/golden/TestUpdateUser/UID_does_not_change_if_user_already_exists b/internal/users/testdata/golden/TestUpdateUser/UID_does_not_change_if_user_already_exists index f744472f5e..32929b27b5 100644 --- a/internal/users/testdata/golden/TestUpdateUser/UID_does_not_change_if_user_already_exists +++ b/internal/users/testdata/golden/TestUpdateUser/UID_does_not_change_if_user_already_exists @@ -15,4 +15,4 @@ groups: users_to_groups: - uid: 1111 gid: 1111 -schema_version: 1 +schema_version: 2 From ae7993c3840bf82a7c6c47b813a1e441b7b198fa Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Fri, 13 Jun 2025 11:46:34 +0200 Subject: [PATCH 17/55] Divide testdata for migrations into subdirectories The testdata is for a specific schema migration, which is now encoded in the filepath. --- internal/users/db/db_test.go | 16 ++++++++-------- .../one_user_and_group_without_locked_column.sql | 0 ...one_users_multiple_groups_fully_uppercase.sql | 0 .../one_users_multiple_groups_with_uppercase.sql | 0 4 files changed, 8 insertions(+), 8 deletions(-) rename internal/users/db/testdata/{ => TestMigrationAddLockedColumnToUsersTable}/one_user_and_group_without_locked_column.sql (100%) rename internal/users/db/testdata/{ => TestMigrationToLowercaseUserAndGroupNames}/one_users_multiple_groups_fully_uppercase.sql (100%) rename internal/users/db/testdata/{ => TestMigrationToLowercaseUserAndGroupNames}/one_users_multiple_groups_with_uppercase.sql (100%) diff --git a/internal/users/db/db_test.go b/internal/users/db/db_test.go index 220f66d969..a3cde0bbe2 100644 --- a/internal/users/db/db_test.go +++ b/internal/users/db/db_test.go @@ -118,7 +118,7 @@ func TestDatabaseRemovedWhenSchemaCreationFails(t *testing.T) { func TestMigrationToLowercaseUserAndGroupNames(t *testing.T) { // Create a database from the testdata dbDir := t.TempDir() - sqlDump := "one_users_multiple_groups_with_uppercase.sql" + sqlDump := "TestMigrationToLowercaseUserAndGroupNames/one_users_multiple_groups_with_uppercase.sql" err := db.Z_ForTests_CreateDBFromDump(filepath.Join("testdata", sqlDump), dbDir) require.NoError(t, err, "Setup: could not create database from testdata") @@ -189,7 +189,7 @@ func TestMigrationToLowercaseUserAndGroupNamesEmptyDB(t *testing.T) { func TestMigrationToLowercaseUserAndGroupNamesAlreadyUpdated(t *testing.T) { // Create a database from the testdata dbDir := t.TempDir() - sqlDump := "one_users_multiple_groups_with_uppercase.sql" + sqlDump := "TestMigrationToLowercaseUserAndGroupNames/one_users_multiple_groups_with_uppercase.sql" err := db.Z_ForTests_CreateDBFromDump(filepath.Join("testdata", sqlDump), dbDir) require.NoError(t, err, "Setup: could not create database from testdata") @@ -220,7 +220,7 @@ func TestMigrationToLowercaseUserAndGroupNamesAlreadyUpdated(t *testing.T) { func TestMigrationToLowercaseUserAndGroupNamesWithSymlinkedGroupFile(t *testing.T) { // Create a database from the testdata dbDir := t.TempDir() - sqlDump := "one_users_multiple_groups_with_uppercase.sql" + sqlDump := "TestMigrationToLowercaseUserAndGroupNames/one_users_multiple_groups_with_uppercase.sql" err := db.Z_ForTests_CreateDBFromDump(filepath.Join("testdata", sqlDump), dbDir) require.NoError(t, err, "Setup: could not create database from testdata") @@ -271,7 +271,7 @@ func TestMigrationToLowercaseUserAndGroupNamesWithSymlinkedGroupFile(t *testing. func TestMigrationToLowercaseUserAndGroupNamesWithPreviousBackup(t *testing.T) { // Create a database from the testdata dbDir := t.TempDir() - sqlDump := "one_users_multiple_groups_fully_uppercase.sql" + sqlDump := "TestMigrationToLowercaseUserAndGroupNames/one_users_multiple_groups_fully_uppercase.sql" err := db.Z_ForTests_CreateDBFromDump(filepath.Join("testdata", sqlDump), dbDir) require.NoError(t, err, "Setup: could not create database from testdata") @@ -318,7 +318,7 @@ func TestMigrationToLowercaseUserAndGroupNamesWithPreviousBackup(t *testing.T) { func TestMigrationToLowercaseUserAndGroupNamesWithSymlinkedPreviousBackup(t *testing.T) { // Create a database from the testdata dbDir := t.TempDir() - sqlDump := "one_users_multiple_groups_fully_uppercase.sql" + sqlDump := "TestMigrationToLowercaseUserAndGroupNames/one_users_multiple_groups_fully_uppercase.sql" err := db.Z_ForTests_CreateDBFromDump(filepath.Join("testdata", sqlDump), dbDir) require.NoError(t, err, "Setup: could not create database from testdata") @@ -375,7 +375,7 @@ func TestMigrationToLowercaseUserAndGroupNamesWithSymlinkedPreviousBackup(t *tes func TestMigrationToLowercaseUserAndGroupNamesFails(t *testing.T) { // Create a database from the testdata dbDir := t.TempDir() - sqlDump := "one_users_multiple_groups_fully_uppercase.sql" + sqlDump := "TestMigrationToLowercaseUserAndGroupNames/one_users_multiple_groups_fully_uppercase.sql" err := db.Z_ForTests_CreateDBFromDump(filepath.Join("testdata", sqlDump), dbDir) require.NoError(t, err, "Setup: could not create database from testdata") @@ -413,7 +413,7 @@ func TestMigrationToLowercaseUserAndGroupNamesFails(t *testing.T) { func TestMigrationToLowercaseUserAndGroupNamesWithBackupFailure(t *testing.T) { // Create a database from the testdata dbDir := t.TempDir() - sqlDump := "one_users_multiple_groups_with_uppercase.sql" + sqlDump := "TestMigrationToLowercaseUserAndGroupNames/one_users_multiple_groups_with_uppercase.sql" err := db.Z_ForTests_CreateDBFromDump(filepath.Join("testdata", sqlDump), dbDir) require.NoError(t, err, "Setup: could not create database from testdata") @@ -458,7 +458,7 @@ func TestMigrationToLowercaseUserAndGroupNamesWithBackupFailure(t *testing.T) { func TestMigrationAddLockedColumnToUsersTable(t *testing.T) { // Create a database from the testdata dbDir := t.TempDir() - sqlDump := "one_user_and_group_without_locked_column.sql" + sqlDump := "TestMigrationAddLockedColumnToUsersTable/one_user_and_group_without_locked_column.sql" err := db.Z_ForTests_CreateDBFromDump(filepath.Join("testdata", sqlDump), dbDir) require.NoError(t, err, "Setup: could not create database from testdata") diff --git a/internal/users/db/testdata/one_user_and_group_without_locked_column.sql b/internal/users/db/testdata/TestMigrationAddLockedColumnToUsersTable/one_user_and_group_without_locked_column.sql similarity index 100% rename from internal/users/db/testdata/one_user_and_group_without_locked_column.sql rename to internal/users/db/testdata/TestMigrationAddLockedColumnToUsersTable/one_user_and_group_without_locked_column.sql diff --git a/internal/users/db/testdata/one_users_multiple_groups_fully_uppercase.sql b/internal/users/db/testdata/TestMigrationToLowercaseUserAndGroupNames/one_users_multiple_groups_fully_uppercase.sql similarity index 100% rename from internal/users/db/testdata/one_users_multiple_groups_fully_uppercase.sql rename to internal/users/db/testdata/TestMigrationToLowercaseUserAndGroupNames/one_users_multiple_groups_fully_uppercase.sql diff --git a/internal/users/db/testdata/one_users_multiple_groups_with_uppercase.sql b/internal/users/db/testdata/TestMigrationToLowercaseUserAndGroupNames/one_users_multiple_groups_with_uppercase.sql similarity index 100% rename from internal/users/db/testdata/one_users_multiple_groups_with_uppercase.sql rename to internal/users/db/testdata/TestMigrationToLowercaseUserAndGroupNames/one_users_multiple_groups_with_uppercase.sql From a59a2295c597615591c04ff0cec999981a3a9f9e Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Mon, 16 Jun 2025 16:08:40 +0200 Subject: [PATCH 18/55] Add authctl completion scripts for bash, zsh, fish These were created by running: go run ./cmd/authctl/main.go completion bash > ./shell-completion/bash/authctl go run ./cmd/authctl/main.go completion zsh > ./shell-completion/zsh/_authctl go run ./cmd/authctl/main.go completion fish > ./shell-completion/fish/authctl.fish --- shell-completion/bash/authctl | 426 +++++++++++++++++++++++++++++ shell-completion/fish/authctl.fish | 235 ++++++++++++++++ shell-completion/generate.go | 9 + shell-completion/zsh/_authctl | 212 ++++++++++++++ 4 files changed, 882 insertions(+) create mode 100644 shell-completion/bash/authctl create mode 100644 shell-completion/fish/authctl.fish create mode 100644 shell-completion/generate.go create mode 100644 shell-completion/zsh/_authctl diff --git a/shell-completion/bash/authctl b/shell-completion/bash/authctl new file mode 100644 index 0000000000..3a97824dbe --- /dev/null +++ b/shell-completion/bash/authctl @@ -0,0 +1,426 @@ +# bash completion V2 for authctl -*- shell-script -*- + +__authctl_debug() +{ + if [[ -n ${BASH_COMP_DEBUG_FILE-} ]]; then + echo "$*" >> "${BASH_COMP_DEBUG_FILE}" + fi +} + +# Macs have bash3 for which the bash-completion package doesn't include +# _init_completion. This is a minimal version of that function. +__authctl_init_completion() +{ + COMPREPLY=() + _get_comp_words_by_ref "$@" cur prev words cword +} + +# This function calls the authctl program to obtain the completion +# results and the directive. It fills the 'out' and 'directive' vars. +__authctl_get_completion_results() { + local requestComp lastParam lastChar args + + # Prepare the command to request completions for the program. + # Calling ${words[0]} instead of directly authctl allows handling aliases + args=("${words[@]:1}") + requestComp="${words[0]} __complete ${args[*]}" + + lastParam=${words[$((${#words[@]}-1))]} + lastChar=${lastParam:$((${#lastParam}-1)):1} + __authctl_debug "lastParam ${lastParam}, lastChar ${lastChar}" + + if [[ -z ${cur} && ${lastChar} != = ]]; then + # If the last parameter is complete (there is a space following it) + # We add an extra empty parameter so we can indicate this to the go method. + __authctl_debug "Adding extra empty parameter" + requestComp="${requestComp} ''" + fi + + # When completing a flag with an = (e.g., authctl -n=) + # bash focuses on the part after the =, so we need to remove + # the flag part from $cur + if [[ ${cur} == -*=* ]]; then + cur="${cur#*=}" + fi + + __authctl_debug "Calling ${requestComp}" + # Use eval to handle any environment variables and such + out=$(eval "${requestComp}" 2>/dev/null) + + # Extract the directive integer at the very end of the output following a colon (:) + directive=${out##*:} + # Remove the directive + out=${out%:*} + if [[ ${directive} == "${out}" ]]; then + # There is not directive specified + directive=0 + fi + __authctl_debug "The completion directive is: ${directive}" + __authctl_debug "The completions are: ${out}" +} + +__authctl_process_completion_results() { + local shellCompDirectiveError=1 + local shellCompDirectiveNoSpace=2 + local shellCompDirectiveNoFileComp=4 + local shellCompDirectiveFilterFileExt=8 + local shellCompDirectiveFilterDirs=16 + local shellCompDirectiveKeepOrder=32 + + if (((directive & shellCompDirectiveError) != 0)); then + # Error code. No completion. + __authctl_debug "Received error from custom completion go code" + return + else + if (((directive & shellCompDirectiveNoSpace) != 0)); then + if [[ $(type -t compopt) == builtin ]]; then + __authctl_debug "Activating no space" + compopt -o nospace + else + __authctl_debug "No space directive not supported in this version of bash" + fi + fi + if (((directive & shellCompDirectiveKeepOrder) != 0)); then + if [[ $(type -t compopt) == builtin ]]; then + # no sort isn't supported for bash less than < 4.4 + if [[ ${BASH_VERSINFO[0]} -lt 4 || ( ${BASH_VERSINFO[0]} -eq 4 && ${BASH_VERSINFO[1]} -lt 4 ) ]]; then + __authctl_debug "No sort directive not supported in this version of bash" + else + __authctl_debug "Activating keep order" + compopt -o nosort + fi + else + __authctl_debug "No sort directive not supported in this version of bash" + fi + fi + if (((directive & shellCompDirectiveNoFileComp) != 0)); then + if [[ $(type -t compopt) == builtin ]]; then + __authctl_debug "Activating no file completion" + compopt +o default + else + __authctl_debug "No file completion directive not supported in this version of bash" + fi + fi + fi + + # Separate activeHelp from normal completions + local completions=() + local activeHelp=() + __authctl_extract_activeHelp + + if (((directive & shellCompDirectiveFilterFileExt) != 0)); then + # File extension filtering + local fullFilter="" filter filteringCmd + + # Do not use quotes around the $completions variable or else newline + # characters will be kept. + for filter in ${completions[*]}; do + fullFilter+="$filter|" + done + + filteringCmd="_filedir $fullFilter" + __authctl_debug "File filtering command: $filteringCmd" + $filteringCmd + elif (((directive & shellCompDirectiveFilterDirs) != 0)); then + # File completion for directories only + + local subdir + subdir=${completions[0]} + if [[ -n $subdir ]]; then + __authctl_debug "Listing directories in $subdir" + pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return + else + __authctl_debug "Listing directories in ." + _filedir -d + fi + else + __authctl_handle_completion_types + fi + + __authctl_handle_special_char "$cur" : + __authctl_handle_special_char "$cur" = + + # Print the activeHelp statements before we finish + __authctl_handle_activeHelp +} + +__authctl_handle_activeHelp() { + # Print the activeHelp statements + if ((${#activeHelp[*]} != 0)); then + if [ -z $COMP_TYPE ]; then + # Bash v3 does not set the COMP_TYPE variable. + printf "\n"; + printf "%s\n" "${activeHelp[@]}" + printf "\n" + __authctl_reprint_commandLine + return + fi + + # Only print ActiveHelp on the second TAB press + if [ $COMP_TYPE -eq 63 ]; then + printf "\n" + printf "%s\n" "${activeHelp[@]}" + + if ((${#COMPREPLY[*]} == 0)); then + # When there are no completion choices from the program, file completion + # may kick in if the program has not disabled it; in such a case, we want + # to know if any files will match what the user typed, so that we know if + # there will be completions presented, so that we know how to handle ActiveHelp. + # To find out, we actually trigger the file completion ourselves; + # the call to _filedir will fill COMPREPLY if files match. + if (((directive & shellCompDirectiveNoFileComp) == 0)); then + __authctl_debug "Listing files" + _filedir + fi + fi + + if ((${#COMPREPLY[*]} != 0)); then + # If there are completion choices to be shown, print a delimiter. + # Re-printing the command-line will automatically be done + # by the shell when it prints the completion choices. + printf -- "--" + else + # When there are no completion choices at all, we need + # to re-print the command-line since the shell will + # not be doing it itself. + __authctl_reprint_commandLine + fi + elif [ $COMP_TYPE -eq 37 ] || [ $COMP_TYPE -eq 42 ]; then + # For completion type: menu-complete/menu-complete-backward and insert-completions + # the completions are immediately inserted into the command-line, so we first + # print the activeHelp message and reprint the command-line since the shell won't. + printf "\n" + printf "%s\n" "${activeHelp[@]}" + + __authctl_reprint_commandLine + fi + fi +} + +__authctl_reprint_commandLine() { + # The prompt format is only available from bash 4.4. + # We test if it is available before using it. + if (x=${PS1@P}) 2> /dev/null; then + printf "%s" "${PS1@P}${COMP_LINE[@]}" + else + # Can't print the prompt. Just print the + # text the user had typed, it is workable enough. + printf "%s" "${COMP_LINE[@]}" + fi +} + +# Separate activeHelp lines from real completions. +# Fills the $activeHelp and $completions arrays. +__authctl_extract_activeHelp() { + local activeHelpMarker="_activeHelp_ " + local endIndex=${#activeHelpMarker} + + while IFS='' read -r comp; do + [[ -z $comp ]] && continue + + if [[ ${comp:0:endIndex} == $activeHelpMarker ]]; then + comp=${comp:endIndex} + __authctl_debug "ActiveHelp found: $comp" + if [[ -n $comp ]]; then + activeHelp+=("$comp") + fi + else + # Not an activeHelp line but a normal completion + completions+=("$comp") + fi + done <<<"${out}" +} + +__authctl_handle_completion_types() { + __authctl_debug "__authctl_handle_completion_types: COMP_TYPE is $COMP_TYPE" + + case $COMP_TYPE in + 37|42) + # Type: menu-complete/menu-complete-backward and insert-completions + # If the user requested inserting one completion at a time, or all + # completions at once on the command-line we must remove the descriptions. + # https://github.com/spf13/cobra/issues/1508 + + # If there are no completions, we don't need to do anything + (( ${#completions[@]} == 0 )) && return 0 + + local tab=$'\t' + + # Strip any description and escape the completion to handled special characters + IFS=$'\n' read -ra completions -d '' < <(printf "%q\n" "${completions[@]%%$tab*}") + + # Only consider the completions that match + IFS=$'\n' read -ra COMPREPLY -d '' < <(IFS=$'\n'; compgen -W "${completions[*]}" -- "${cur}") + + # compgen looses the escaping so we need to escape all completions again since they will + # all be inserted on the command-line. + IFS=$'\n' read -ra COMPREPLY -d '' < <(printf "%q\n" "${COMPREPLY[@]}") + ;; + + *) + # Type: complete (normal completion) + __authctl_handle_standard_completion_case + ;; + esac +} + +__authctl_handle_standard_completion_case() { + local tab=$'\t' + + # If there are no completions, we don't need to do anything + (( ${#completions[@]} == 0 )) && return 0 + + # Short circuit to optimize if we don't have descriptions + if [[ "${completions[*]}" != *$tab* ]]; then + # First, escape the completions to handle special characters + IFS=$'\n' read -ra completions -d '' < <(printf "%q\n" "${completions[@]}") + # Only consider the completions that match what the user typed + IFS=$'\n' read -ra COMPREPLY -d '' < <(IFS=$'\n'; compgen -W "${completions[*]}" -- "${cur}") + + # compgen looses the escaping so, if there is only a single completion, we need to + # escape it again because it will be inserted on the command-line. If there are multiple + # completions, we don't want to escape them because they will be printed in a list + # and we don't want to show escape characters in that list. + if (( ${#COMPREPLY[@]} == 1 )); then + COMPREPLY[0]=$(printf "%q" "${COMPREPLY[0]}") + fi + return 0 + fi + + local longest=0 + local compline + # Look for the longest completion so that we can format things nicely + while IFS='' read -r compline; do + [[ -z $compline ]] && continue + + # Before checking if the completion matches what the user typed, + # we need to strip any description and escape the completion to handle special + # characters because those escape characters are part of what the user typed. + # Don't call "printf" in a sub-shell because it will be much slower + # since we are in a loop. + printf -v comp "%q" "${compline%%$tab*}" &>/dev/null || comp=$(printf "%q" "${compline%%$tab*}") + + # Only consider the completions that match + [[ $comp == "$cur"* ]] || continue + + # The completions matches. Add it to the list of full completions including + # its description. We don't escape the completion because it may get printed + # in a list if there are more than one and we don't want show escape characters + # in that list. + COMPREPLY+=("$compline") + + # Strip any description before checking the length, and again, don't escape + # the completion because this length is only used when printing the completions + # in a list and we don't want show escape characters in that list. + comp=${compline%%$tab*} + if ((${#comp}>longest)); then + longest=${#comp} + fi + done < <(printf "%s\n" "${completions[@]}") + + # If there is a single completion left, remove the description text and escape any special characters + if ((${#COMPREPLY[*]} == 1)); then + __authctl_debug "COMPREPLY[0]: ${COMPREPLY[0]}" + COMPREPLY[0]=$(printf "%q" "${COMPREPLY[0]%%$tab*}") + __authctl_debug "Removed description from single completion, which is now: ${COMPREPLY[0]}" + else + # Format the descriptions + __authctl_format_comp_descriptions $longest + fi +} + +__authctl_handle_special_char() +{ + local comp="$1" + local char=$2 + if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then + local word=${comp%"${comp##*${char}}"} + local idx=${#COMPREPLY[*]} + while ((--idx >= 0)); do + COMPREPLY[idx]=${COMPREPLY[idx]#"$word"} + done + fi +} + +__authctl_format_comp_descriptions() +{ + local tab=$'\t' + local comp desc maxdesclength + local longest=$1 + + local i ci + for ci in ${!COMPREPLY[*]}; do + comp=${COMPREPLY[ci]} + # Properly format the description string which follows a tab character if there is one + if [[ "$comp" == *$tab* ]]; then + __authctl_debug "Original comp: $comp" + desc=${comp#*$tab} + comp=${comp%%$tab*} + + # $COLUMNS stores the current shell width. + # Remove an extra 4 because we add 2 spaces and 2 parentheses. + maxdesclength=$(( COLUMNS - longest - 4 )) + + # Make sure we can fit a description of at least 8 characters + # if we are to align the descriptions. + if ((maxdesclength > 8)); then + # Add the proper number of spaces to align the descriptions + for ((i = ${#comp} ; i < longest ; i++)); do + comp+=" " + done + else + # Don't pad the descriptions so we can fit more text after the completion + maxdesclength=$(( COLUMNS - ${#comp} - 4 )) + fi + + # If there is enough space for any description text, + # truncate the descriptions that are too long for the shell width + if ((maxdesclength > 0)); then + if ((${#desc} > maxdesclength)); then + desc=${desc:0:$(( maxdesclength - 1 ))} + desc+="…" + fi + comp+=" ($desc)" + fi + COMPREPLY[ci]=$comp + __authctl_debug "Final comp: $comp" + fi + done +} + +__start_authctl() +{ + local cur prev words cword split + + COMPREPLY=() + + # Call _init_completion from the bash-completion package + # to prepare the arguments properly + if declare -F _init_completion >/dev/null 2>&1; then + _init_completion -n =: || return + else + __authctl_init_completion -n =: || return + fi + + __authctl_debug + __authctl_debug "========= starting completion logic ==========" + __authctl_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword" + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $cword location, so we need + # to truncate the command-line ($words) up to the $cword location. + words=("${words[@]:0:$cword+1}") + __authctl_debug "Truncated words[*]: ${words[*]}," + + local out directive + __authctl_get_completion_results + __authctl_process_completion_results +} + +if [[ $(type -t compopt) = "builtin" ]]; then + complete -o default -F __start_authctl authctl +else + complete -o default -o nospace -F __start_authctl authctl +fi + +# ex: ts=4 sw=4 et filetype=sh diff --git a/shell-completion/fish/authctl.fish b/shell-completion/fish/authctl.fish new file mode 100644 index 0000000000..a59163359d --- /dev/null +++ b/shell-completion/fish/authctl.fish @@ -0,0 +1,235 @@ +# fish completion for authctl -*- shell-script -*- + +function __authctl_debug + set -l file "$BASH_COMP_DEBUG_FILE" + if test -n "$file" + echo "$argv" >> $file + end +end + +function __authctl_perform_completion + __authctl_debug "Starting __authctl_perform_completion" + + # Extract all args except the last one + set -l args (commandline -opc) + # Extract the last arg and escape it in case it is a space + set -l lastArg (string escape -- (commandline -ct)) + + __authctl_debug "args: $args" + __authctl_debug "last arg: $lastArg" + + # Disable ActiveHelp which is not supported for fish shell + set -l requestComp "AUTHCTL_ACTIVE_HELP=0 $args[1] __complete $args[2..-1] $lastArg" + + __authctl_debug "Calling $requestComp" + set -l results (eval $requestComp 2> /dev/null) + + # Some programs may output extra empty lines after the directive. + # Let's ignore them or else it will break completion. + # Ref: https://github.com/spf13/cobra/issues/1279 + for line in $results[-1..1] + if test (string trim -- $line) = "" + # Found an empty line, remove it + set results $results[1..-2] + else + # Found non-empty line, we have our proper output + break + end + end + + set -l comps $results[1..-2] + set -l directiveLine $results[-1] + + # For Fish, when completing a flag with an = (e.g., -n=) + # completions must be prefixed with the flag + set -l flagPrefix (string match -r -- '-.*=' "$lastArg") + + __authctl_debug "Comps: $comps" + __authctl_debug "DirectiveLine: $directiveLine" + __authctl_debug "flagPrefix: $flagPrefix" + + for comp in $comps + printf "%s%s\n" "$flagPrefix" "$comp" + end + + printf "%s\n" "$directiveLine" +end + +# this function limits calls to __authctl_perform_completion, by caching the result behind $__authctl_perform_completion_once_result +function __authctl_perform_completion_once + __authctl_debug "Starting __authctl_perform_completion_once" + + if test -n "$__authctl_perform_completion_once_result" + __authctl_debug "Seems like a valid result already exists, skipping __authctl_perform_completion" + return 0 + end + + set --global __authctl_perform_completion_once_result (__authctl_perform_completion) + if test -z "$__authctl_perform_completion_once_result" + __authctl_debug "No completions, probably due to a failure" + return 1 + end + + __authctl_debug "Performed completions and set __authctl_perform_completion_once_result" + return 0 +end + +# this function is used to clear the $__authctl_perform_completion_once_result variable after completions are run +function __authctl_clear_perform_completion_once_result + __authctl_debug "" + __authctl_debug "========= clearing previously set __authctl_perform_completion_once_result variable ==========" + set --erase __authctl_perform_completion_once_result + __authctl_debug "Successfully erased the variable __authctl_perform_completion_once_result" +end + +function __authctl_requires_order_preservation + __authctl_debug "" + __authctl_debug "========= checking if order preservation is required ==========" + + __authctl_perform_completion_once + if test -z "$__authctl_perform_completion_once_result" + __authctl_debug "Error determining if order preservation is required" + return 1 + end + + set -l directive (string sub --start 2 $__authctl_perform_completion_once_result[-1]) + __authctl_debug "Directive is: $directive" + + set -l shellCompDirectiveKeepOrder 32 + set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) % 2) + __authctl_debug "Keeporder is: $keeporder" + + if test $keeporder -ne 0 + __authctl_debug "This does require order preservation" + return 0 + end + + __authctl_debug "This doesn't require order preservation" + return 1 +end + + +# This function does two things: +# - Obtain the completions and store them in the global __authctl_comp_results +# - Return false if file completion should be performed +function __authctl_prepare_completions + __authctl_debug "" + __authctl_debug "========= starting completion logic ==========" + + # Start fresh + set --erase __authctl_comp_results + + __authctl_perform_completion_once + __authctl_debug "Completion results: $__authctl_perform_completion_once_result" + + if test -z "$__authctl_perform_completion_once_result" + __authctl_debug "No completion, probably due to a failure" + # Might as well do file completion, in case it helps + return 1 + end + + set -l directive (string sub --start 2 $__authctl_perform_completion_once_result[-1]) + set --global __authctl_comp_results $__authctl_perform_completion_once_result[1..-2] + + __authctl_debug "Completions are: $__authctl_comp_results" + __authctl_debug "Directive is: $directive" + + set -l shellCompDirectiveError 1 + set -l shellCompDirectiveNoSpace 2 + set -l shellCompDirectiveNoFileComp 4 + set -l shellCompDirectiveFilterFileExt 8 + set -l shellCompDirectiveFilterDirs 16 + + if test -z "$directive" + set directive 0 + end + + set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) % 2) + if test $compErr -eq 1 + __authctl_debug "Received error directive: aborting." + # Might as well do file completion, in case it helps + return 1 + end + + set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) % 2) + set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2) + if test $filefilter -eq 1; or test $dirfilter -eq 1 + __authctl_debug "File extension filtering or directory filtering not supported" + # Do full file completion instead + return 1 + end + + set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) % 2) + set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) % 2) + + __authctl_debug "nospace: $nospace, nofiles: $nofiles" + + # If we want to prevent a space, or if file completion is NOT disabled, + # we need to count the number of valid completions. + # To do so, we will filter on prefix as the completions we have received + # may not already be filtered so as to allow fish to match on different + # criteria than the prefix. + if test $nospace -ne 0; or test $nofiles -eq 0 + set -l prefix (commandline -t | string escape --style=regex) + __authctl_debug "prefix: $prefix" + + set -l completions (string match -r -- "^$prefix.*" $__authctl_comp_results) + set --global __authctl_comp_results $completions + __authctl_debug "Filtered completions are: $__authctl_comp_results" + + # Important not to quote the variable for count to work + set -l numComps (count $__authctl_comp_results) + __authctl_debug "numComps: $numComps" + + if test $numComps -eq 1; and test $nospace -ne 0 + # We must first split on \t to get rid of the descriptions to be + # able to check what the actual completion will be. + # We don't need descriptions anyway since there is only a single + # real completion which the shell will expand immediately. + set -l split (string split --max 1 \t $__authctl_comp_results[1]) + + # Fish won't add a space if the completion ends with any + # of the following characters: @=/:., + set -l lastChar (string sub -s -1 -- $split) + if not string match -r -q "[@=/:.,]" -- "$lastChar" + # In other cases, to support the "nospace" directive we trick the shell + # by outputting an extra, longer completion. + __authctl_debug "Adding second completion to perform nospace directive" + set --global __authctl_comp_results $split[1] $split[1]. + __authctl_debug "Completions are now: $__authctl_comp_results" + end + end + + if test $numComps -eq 0; and test $nofiles -eq 0 + # To be consistent with bash and zsh, we only trigger file + # completion when there are no other completions + __authctl_debug "Requesting file completion" + return 1 + end + end + + return 0 +end + +# Since Fish completions are only loaded once the user triggers them, we trigger them ourselves +# so we can properly delete any completions provided by another script. +# Only do this if the program can be found, or else fish may print some errors; besides, +# the existing completions will only be loaded if the program can be found. +if type -q "authctl" + # The space after the program name is essential to trigger completion for the program + # and not completion of the program name itself. + # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish. + complete --do-complete "authctl " > /dev/null 2>&1 +end + +# Remove any pre-existing completions for the program since we will be handling all of them. +complete -c authctl -e + +# this will get called after the two calls below and clear the $__authctl_perform_completion_once_result global +complete -c authctl -n '__authctl_clear_perform_completion_once_result' +# The call to __authctl_prepare_completions will setup __authctl_comp_results +# which provides the program's completion choices. +# If this doesn't require order preservation, we don't use the -k flag +complete -c authctl -n 'not __authctl_requires_order_preservation && __authctl_prepare_completions' -f -a '$__authctl_comp_results' +# otherwise we use the -k flag +complete -k -c authctl -n '__authctl_requires_order_preservation && __authctl_prepare_completions' -f -a '$__authctl_comp_results' diff --git a/shell-completion/generate.go b/shell-completion/generate.go new file mode 100644 index 0000000000..308ea397c4 --- /dev/null +++ b/shell-completion/generate.go @@ -0,0 +1,9 @@ +//go:build generate + +// TiCS: disabled // This is a helper file to generate the shell completion code. + +//go:generate sh -c "go run ../cmd/authctl/main.go completion bash > bash/authctl" +//go:generate sh -c "go run ../cmd/authctl/main.go completion zsh > zsh/_authctl" +//go:generate sh -c "go run ../cmd/authctl/main.go completion fish > fish/authctl.fish" + +package shell_completion diff --git a/shell-completion/zsh/_authctl b/shell-completion/zsh/_authctl new file mode 100644 index 0000000000..fa9ff15acd --- /dev/null +++ b/shell-completion/zsh/_authctl @@ -0,0 +1,212 @@ +#compdef authctl +compdef _authctl authctl + +# zsh completion for authctl -*- shell-script -*- + +__authctl_debug() +{ + local file="$BASH_COMP_DEBUG_FILE" + if [[ -n ${file} ]]; then + echo "$*" >> "${file}" + fi +} + +_authctl() +{ + local shellCompDirectiveError=1 + local shellCompDirectiveNoSpace=2 + local shellCompDirectiveNoFileComp=4 + local shellCompDirectiveFilterFileExt=8 + local shellCompDirectiveFilterDirs=16 + local shellCompDirectiveKeepOrder=32 + + local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace keepOrder + local -a completions + + __authctl_debug "\n========= starting completion logic ==========" + __authctl_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}" + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $CURRENT location, so we need + # to truncate the command-line ($words) up to the $CURRENT location. + # (We cannot use $CURSOR as its value does not work when a command is an alias.) + words=("${=words[1,CURRENT]}") + __authctl_debug "Truncated words[*]: ${words[*]}," + + lastParam=${words[-1]} + lastChar=${lastParam[-1]} + __authctl_debug "lastParam: ${lastParam}, lastChar: ${lastChar}" + + # For zsh, when completing a flag with an = (e.g., authctl -n=) + # completions must be prefixed with the flag + setopt local_options BASH_REMATCH + if [[ "${lastParam}" =~ '-.*=' ]]; then + # We are dealing with a flag with an = + flagPrefix="-P ${BASH_REMATCH}" + fi + + # Prepare the command to obtain completions + requestComp="${words[1]} __complete ${words[2,-1]}" + if [ "${lastChar}" = "" ]; then + # If the last parameter is complete (there is a space following it) + # We add an extra empty parameter so we can indicate this to the go completion code. + __authctl_debug "Adding extra empty parameter" + requestComp="${requestComp} \"\"" + fi + + __authctl_debug "About to call: eval ${requestComp}" + + # Use eval to handle any environment variables and such + out=$(eval ${requestComp} 2>/dev/null) + __authctl_debug "completion output: ${out}" + + # Extract the directive integer following a : from the last line + local lastLine + while IFS='\n' read -r line; do + lastLine=${line} + done < <(printf "%s\n" "${out[@]}") + __authctl_debug "last line: ${lastLine}" + + if [ "${lastLine[1]}" = : ]; then + directive=${lastLine[2,-1]} + # Remove the directive including the : and the newline + local suffix + (( suffix=${#lastLine}+2)) + out=${out[1,-$suffix]} + else + # There is no directive specified. Leave $out as is. + __authctl_debug "No directive found. Setting do default" + directive=0 + fi + + __authctl_debug "directive: ${directive}" + __authctl_debug "completions: ${out}" + __authctl_debug "flagPrefix: ${flagPrefix}" + + if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then + __authctl_debug "Completion received error. Ignoring completions." + return + fi + + local activeHelpMarker="_activeHelp_ " + local endIndex=${#activeHelpMarker} + local startIndex=$((${#activeHelpMarker}+1)) + local hasActiveHelp=0 + while IFS='\n' read -r comp; do + # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker) + if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then + __authctl_debug "ActiveHelp found: $comp" + comp="${comp[$startIndex,-1]}" + if [ -n "$comp" ]; then + compadd -x "${comp}" + __authctl_debug "ActiveHelp will need delimiter" + hasActiveHelp=1 + fi + + continue + fi + + if [ -n "$comp" ]; then + # If requested, completions are returned with a description. + # The description is preceded by a TAB character. + # For zsh's _describe, we need to use a : instead of a TAB. + # We first need to escape any : as part of the completion itself. + comp=${comp//:/\\:} + + local tab="$(printf '\t')" + comp=${comp//$tab/:} + + __authctl_debug "Adding completion: ${comp}" + completions+=${comp} + lastComp=$comp + fi + done < <(printf "%s\n" "${out[@]}") + + # Add a delimiter after the activeHelp statements, but only if: + # - there are completions following the activeHelp statements, or + # - file completion will be performed (so there will be choices after the activeHelp) + if [ $hasActiveHelp -eq 1 ]; then + if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then + __authctl_debug "Adding activeHelp delimiter" + compadd -x "--" + hasActiveHelp=0 + fi + fi + + if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then + __authctl_debug "Activating nospace." + noSpace="-S ''" + fi + + if [ $((directive & shellCompDirectiveKeepOrder)) -ne 0 ]; then + __authctl_debug "Activating keep order." + keepOrder="-V" + fi + + if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then + # File extension filtering + local filteringCmd + filteringCmd='_files' + for filter in ${completions[@]}; do + if [ ${filter[1]} != '*' ]; then + # zsh requires a glob pattern to do file filtering + filter="\*.$filter" + fi + filteringCmd+=" -g $filter" + done + filteringCmd+=" ${flagPrefix}" + + __authctl_debug "File filtering command: $filteringCmd" + _arguments '*:filename:'"$filteringCmd" + elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then + # File completion for directories only + local subdir + subdir="${completions[1]}" + if [ -n "$subdir" ]; then + __authctl_debug "Listing directories in $subdir" + pushd "${subdir}" >/dev/null 2>&1 + else + __authctl_debug "Listing directories in ." + fi + + local result + _arguments '*:dirname:_files -/'" ${flagPrefix}" + result=$? + if [ -n "$subdir" ]; then + popd >/dev/null 2>&1 + fi + return $result + else + __authctl_debug "Calling _describe" + if eval _describe $keepOrder "completions" completions $flagPrefix $noSpace; then + __authctl_debug "_describe found some completions" + + # Return the success of having called _describe + return 0 + else + __authctl_debug "_describe did not find completions." + __authctl_debug "Checking if we should do file completion." + if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then + __authctl_debug "deactivating file completion" + + # We must return an error code here to let zsh know that there were no + # completions found by _describe; this is what will trigger other + # matching algorithms to attempt to find completions. + # For example zsh can match letters in the middle of words. + return 1 + else + # Perform file completion + __authctl_debug "Activating file completion" + + # We must return the result of this command, so it must be the + # last command, or else we must store its result to return it. + _arguments '*:filename:_files'" ${flagPrefix}" + fi + fi + fi +} + +# don't run the completion function when being source-ed or eval-ed +if [ "$funcstack[1]" = "_authctl" ]; then + _authctl +fi From 2db6c5b034b007bd5b2c0a847dd6b2514d9f4535 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Mon, 16 Jun 2025 16:11:09 +0200 Subject: [PATCH 19/55] debian/install: Install shell completion scripts --- debian/install | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/debian/install b/debian/install index e9bc299602..b5b56345ae 100755 --- a/debian/install +++ b/debian/install @@ -21,3 +21,8 @@ ${env:BUILT_PAM_LIBS_PATH}/go-exec/pam_authd_exec.so ${env:AUTHD_PAM_MODULES_PAT # Install NSS library with right soname target/${DEB_HOST_RUST_TYPE}/release/libnss_authd.so => /usr/lib/${DEB_TARGET_GNU_TYPE}/libnss_authd.so.2 + +# Shell completion scripts +shell-completion/bash/authctl /usr/share/bash-completion/completions/ +shell-completion/zsh/_authctl /usr/share/zsh/vendor-completions/ +shell-completion/fish/authctl.fish /usr/share/fish/vendor_completions.d/ From 9add730702cb860f78416de6d7f5d0e4fed123be Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Mon, 16 Jun 2025 17:26:11 +0200 Subject: [PATCH 20/55] authctl: Hide the completion command from the usage message We ship the shell completion scripts with the Debian package now, so there is no need for the user to generate their own scripts, and we can avoid cluttering the usage message with that command. --- cmd/authctl/main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/authctl/main.go b/cmd/authctl/main.go index 46516222f3..9fa78c1ea8 100644 --- a/cmd/authctl/main.go +++ b/cmd/authctl/main.go @@ -16,6 +16,9 @@ var rootCmd = &cobra.Command{ Short: "CLI tool to interact with authd", Long: "authctl is a CLI tool which can be used to interact with authd.", Args: cobra.NoArgs, + CompletionOptions: cobra.CompletionOptions{ + HiddenDefaultCmd: true, + }, } func init() { From 1994a9ebe95476d9dd58d587ec203521d5f00860 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 17 Jun 2025 12:32:51 +0200 Subject: [PATCH 21/55] Specify required arguments in usage message --- cmd/authctl/user/lock.go | 2 +- cmd/authctl/user/unlock.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/authctl/user/lock.go b/cmd/authctl/user/lock.go index 10747f339a..cc85a2d688 100644 --- a/cmd/authctl/user/lock.go +++ b/cmd/authctl/user/lock.go @@ -10,7 +10,7 @@ import ( // lockCmd is a command to lock (disable) a user. var lockCmd = &cobra.Command{ - Use: "lock", + Use: "lock ", Short: "Lock (disable) a user managed by authd", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/cmd/authctl/user/unlock.go b/cmd/authctl/user/unlock.go index 43d92aae06..d32714f237 100644 --- a/cmd/authctl/user/unlock.go +++ b/cmd/authctl/user/unlock.go @@ -10,7 +10,7 @@ import ( // unlockCmd is a command to unlock (enable) a user. var unlockCmd = &cobra.Command{ - Use: "unlock", + Use: "unlock ", Short: "Unlock (enable) a user managed by authd", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { From 187cbec5f6d2ace33b32c8189261f6151bbf064e Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 17 Jun 2025 12:35:18 +0200 Subject: [PATCH 22/55] Fix "Locking user" message --- cmd/authctl/user/lock.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/authctl/user/lock.go b/cmd/authctl/user/lock.go index cc85a2d688..9673e6ffbc 100644 --- a/cmd/authctl/user/lock.go +++ b/cmd/authctl/user/lock.go @@ -14,7 +14,7 @@ var lockCmd = &cobra.Command{ Short: "Lock (disable) a user managed by authd", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - fmt.Printf("Locking user %q\n...", args[0]) + fmt.Printf("Locking user %q...\n", args[0]) client, err := NewUserServiceClient() if err != nil { From 5d48c3eae5dbdd2478da069f33d4cae7a84972ab Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 17 Jun 2025 13:24:41 +0200 Subject: [PATCH 23/55] authctl: Improve error messages printed for gRPC errors --- cmd/authctl/main.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/cmd/authctl/main.go b/cmd/authctl/main.go index 9fa78c1ea8..2213de58ba 100644 --- a/cmd/authctl/main.go +++ b/cmd/authctl/main.go @@ -7,6 +7,8 @@ import ( "github.com/spf13/cobra" "github.com/ubuntu/authd/cmd/authctl/user" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) const cmdName = "authctl" @@ -32,7 +34,20 @@ func init() { func main() { if err := rootCmd.Execute(); err != nil { - fmt.Fprintln(os.Stderr, err.Error()) + s, ok := status.FromError(err) + if !ok { + // If the error is not a gRPC status, we print it as is. + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + + switch s.Code() { + case codes.PermissionDenied: + fmt.Fprintln(os.Stderr, "Permission denied:", s.Message()) + default: + fmt.Fprintln(os.Stderr, "Error:", s.Message()) + } + os.Exit(1) } } From 1cd477ea88b246320de86ea2de5af084eb2eacbc Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 17 Jun 2025 13:50:39 +0200 Subject: [PATCH 24/55] authctl: Exit with the gRPC error code as exit code Might be useful for users who want to use authctl in scripts. --- cmd/authctl/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/authctl/main.go b/cmd/authctl/main.go index 2213de58ba..93abc24696 100644 --- a/cmd/authctl/main.go +++ b/cmd/authctl/main.go @@ -41,6 +41,7 @@ func main() { os.Exit(1) } + // If the error is a gRPC status, we print the message and exit with the appropriate code. switch s.Code() { case codes.PermissionDenied: fmt.Fprintln(os.Stderr, "Permission denied:", s.Message()) @@ -48,6 +49,6 @@ func main() { fmt.Fprintln(os.Stderr, "Error:", s.Message()) } - os.Exit(1) + os.Exit(int(s.Code())) } } From e8d92047e7ea84a46a3596de514560a982322d3b Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 17 Jun 2025 14:12:18 +0200 Subject: [PATCH 25/55] authctl: Avoid printing errors twice --- cmd/authctl/main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/authctl/main.go b/cmd/authctl/main.go index 93abc24696..63a3f839ff 100644 --- a/cmd/authctl/main.go +++ b/cmd/authctl/main.go @@ -21,6 +21,8 @@ var rootCmd = &cobra.Command{ CompletionOptions: cobra.CompletionOptions{ HiddenDefaultCmd: true, }, + // We handle errors ourselves + SilenceErrors: true, } func init() { From ca702064b3587ebfed62cc51f2e4ed7ddfff69b5 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 17 Jun 2025 14:12:34 +0200 Subject: [PATCH 26/55] authctl: Avoid printing usage message on error Except if the error is related to argument parsing. --- cmd/authctl/main.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmd/authctl/main.go b/cmd/authctl/main.go index 63a3f839ff..b48da08863 100644 --- a/cmd/authctl/main.go +++ b/cmd/authctl/main.go @@ -18,6 +18,10 @@ var rootCmd = &cobra.Command{ Short: "CLI tool to interact with authd", Long: "authctl is a CLI tool which can be used to interact with authd.", Args: cobra.NoArgs, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + // The command was successfully parsed, so we don't want cobra to print usage information on error. + cmd.SilenceUsage = true + }, CompletionOptions: cobra.CompletionOptions{ HiddenDefaultCmd: true, }, From d8c9f96741ff343dd5c183db65df6e08ec57b6af Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 17 Jun 2025 15:56:29 +0200 Subject: [PATCH 27/55] authctl: Simplify short usage string Apparently, everything after the first space is not used anywhere, so we can just omit it. --- cmd/authctl/main.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cmd/authctl/main.go b/cmd/authctl/main.go index b48da08863..2433a34abb 100644 --- a/cmd/authctl/main.go +++ b/cmd/authctl/main.go @@ -11,10 +11,8 @@ import ( "google.golang.org/grpc/status" ) -const cmdName = "authctl" - var rootCmd = &cobra.Command{ - Use: fmt.Sprintf("%s COMMAND", cmdName), + Use: "authctl", Short: "CLI tool to interact with authd", Long: "authctl is a CLI tool which can be used to interact with authd.", Args: cobra.NoArgs, From 9131dbc3587981839a8f147485469b3f67a8885a Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 17 Jun 2025 16:00:16 +0200 Subject: [PATCH 28/55] authctl: Improve long description --- cmd/authctl/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/authctl/main.go b/cmd/authctl/main.go index 2433a34abb..121aa11de4 100644 --- a/cmd/authctl/main.go +++ b/cmd/authctl/main.go @@ -14,7 +14,7 @@ import ( var rootCmd = &cobra.Command{ Use: "authctl", Short: "CLI tool to interact with authd", - Long: "authctl is a CLI tool which can be used to interact with authd.", + Long: "authctl is a command-line tool to interact with the authd service for user and group management.", Args: cobra.NoArgs, PersistentPreRun: func(cmd *cobra.Command, args []string) { // The command was successfully parsed, so we don't want cobra to print usage information on error. From 11863368923ebf8b6c85b4babdbcea6cb5140702 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 17 Jun 2025 15:58:44 +0200 Subject: [PATCH 29/55] Fix authctl exiting with 0 when called with unknown command I don't know why, but specifying "Args: cobra.NoArgs" without a Run or RunE function has this effect. --- cmd/authctl/main.go | 3 ++- cmd/authctl/user/user.go | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/authctl/main.go b/cmd/authctl/main.go index 121aa11de4..67859d4aa4 100644 --- a/cmd/authctl/main.go +++ b/cmd/authctl/main.go @@ -15,7 +15,6 @@ var rootCmd = &cobra.Command{ Use: "authctl", Short: "CLI tool to interact with authd", Long: "authctl is a command-line tool to interact with the authd service for user and group management.", - Args: cobra.NoArgs, PersistentPreRun: func(cmd *cobra.Command, args []string) { // The command was successfully parsed, so we don't want cobra to print usage information on error. cmd.SilenceUsage = true @@ -25,6 +24,8 @@ var rootCmd = &cobra.Command{ }, // We handle errors ourselves SilenceErrors: true, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { return cmd.Usage() }, } func init() { diff --git a/cmd/authctl/user/user.go b/cmd/authctl/user/user.go index a811742bd2..c6ea554413 100644 --- a/cmd/authctl/user/user.go +++ b/cmd/authctl/user/user.go @@ -16,6 +16,7 @@ var UserCmd = &cobra.Command{ Use: "user", Short: "Commands related to users", Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { return cmd.Usage() }, } // NewUserServiceClient creates and returns a new [authd.UserServiceClient]. From 8a3d6ef69303c8c970dcaac67bd02a3a9a799012 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 17 Jun 2025 23:10:18 +0200 Subject: [PATCH 30/55] Return gRPC errors in API methods --- internal/services/user/user.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/services/user/user.go b/internal/services/user/user.go index db1fb72553..a67fa258d2 100644 --- a/internal/services/user/user.go +++ b/internal/services/user/user.go @@ -127,7 +127,7 @@ func (s Service) LockUser(ctx context.Context, req *authd.LockUserRequest) (*aut } if err := s.userManager.LockUser(req.GetName()); err != nil { - return nil, err + return nil, grpcError(err) } return &authd.Empty{}, nil @@ -144,7 +144,7 @@ func (s Service) UnlockUser(ctx context.Context, req *authd.UnlockUserRequest) ( } if err := s.userManager.UnlockUser(req.GetName()); err != nil { - return nil, err + return nil, grpcError(err) } return &authd.Empty{}, nil From ed8068753d8f46ca8390eb3d418d719c68df4ef0 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Fri, 20 Jun 2025 13:44:09 +0200 Subject: [PATCH 31/55] authctl: Add tests for root command --- cmd/authctl/main_test.go | 68 +++++++++++++++++++ .../golden/TestRootCommand/Completion_command | 16 +++++ .../TestRootCommand/Error_on_invalid_command | 14 ++++ .../TestRootCommand/Error_on_invalid_flag | 14 ++++ .../golden/TestRootCommand/Help_command | 14 ++++ .../testdata/golden/TestRootCommand/Help_flag | 14 ++++ .../Usage_message_when_no_args | 12 ++++ internal/testutils/authctl.go | 30 ++++++++ 8 files changed, 182 insertions(+) create mode 100644 cmd/authctl/main_test.go create mode 100644 cmd/authctl/testdata/golden/TestRootCommand/Completion_command create mode 100644 cmd/authctl/testdata/golden/TestRootCommand/Error_on_invalid_command create mode 100644 cmd/authctl/testdata/golden/TestRootCommand/Error_on_invalid_flag create mode 100644 cmd/authctl/testdata/golden/TestRootCommand/Help_command create mode 100644 cmd/authctl/testdata/golden/TestRootCommand/Help_flag create mode 100644 cmd/authctl/testdata/golden/TestRootCommand/Usage_message_when_no_args create mode 100644 internal/testutils/authctl.go diff --git a/cmd/authctl/main_test.go b/cmd/authctl/main_test.go new file mode 100644 index 0000000000..644495a2c1 --- /dev/null +++ b/cmd/authctl/main_test.go @@ -0,0 +1,68 @@ +package main_test + +import ( + "fmt" + "os" + "os/exec" + "testing" + + "github.com/ubuntu/authd/internal/testutils" + "github.com/ubuntu/authd/internal/testutils/golden" +) + +var authctlPath string + +func TestRootCommand(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + args []string + expectedExitCode int + }{ + "Usage_message_when_no_args": {expectedExitCode: 0}, + "Help_command": {args: []string{"help"}, expectedExitCode: 0}, + "Help_flag": {args: []string{"--help"}, expectedExitCode: 0}, + "Completion_command": {args: []string{"completion"}, 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, tc.args...) + t.Logf("Running command: %s", cmd.String()) + outputBytes, err := cmd.CombinedOutput() + output := string(outputBytes) + exitCode := cmd.ProcessState.ExitCode() + + if tc.expectedExitCode == 0 && err != nil { + t.Logf("Command output:\n%s", output) + t.Errorf("Expected no error, but got: %v", err) + } + + if exitCode != tc.expectedExitCode { + t.Logf("Command output:\n%s", output) + t.Errorf("Expected exit code %d, got %d", tc.expectedExitCode, exitCode) + } + + golden.CheckOrUpdate(t, output) + }) + } +} + +func TestMain(m *testing.M) { + var cleanup func() + var err error + authctlPath, cleanup, err = testutils.BuildAuthctl() + if err != nil { + fmt.Fprintf(os.Stderr, "Setup: %v\n", err) + os.Exit(1) + } + defer cleanup() + + m.Run() +} diff --git a/cmd/authctl/testdata/golden/TestRootCommand/Completion_command b/cmd/authctl/testdata/golden/TestRootCommand/Completion_command new file mode 100644 index 0000000000..fb3a14a225 --- /dev/null +++ b/cmd/authctl/testdata/golden/TestRootCommand/Completion_command @@ -0,0 +1,16 @@ +Generate the autocompletion script for authctl for the specified shell. +See each sub-command's help for details on how to use the generated script. + +Usage: + authctl completion [command] + +Available Commands: + bash Generate the autocompletion script for bash + zsh Generate the autocompletion script for zsh + fish Generate the autocompletion script for fish + powershell Generate the autocompletion script for powershell + +Flags: + -h, --help help for completion + +Use "authctl completion [command] --help" for more information about a command. diff --git a/cmd/authctl/testdata/golden/TestRootCommand/Error_on_invalid_command b/cmd/authctl/testdata/golden/TestRootCommand/Error_on_invalid_command new file mode 100644 index 0000000000..31b2b174d7 --- /dev/null +++ b/cmd/authctl/testdata/golden/TestRootCommand/Error_on_invalid_command @@ -0,0 +1,14 @@ +Usage: + authctl [flags] + authctl [command] + +Available Commands: + user Commands related to users + help Help about any command + +Flags: + -h, --help help for authctl + +Use "authctl [command] --help" for more information about a command. + +unknown command "invalid-command" for "authctl" diff --git a/cmd/authctl/testdata/golden/TestRootCommand/Error_on_invalid_flag b/cmd/authctl/testdata/golden/TestRootCommand/Error_on_invalid_flag new file mode 100644 index 0000000000..f900c50639 --- /dev/null +++ b/cmd/authctl/testdata/golden/TestRootCommand/Error_on_invalid_flag @@ -0,0 +1,14 @@ +Usage: + authctl [flags] + authctl [command] + +Available Commands: + user Commands related to users + help Help about any command + +Flags: + -h, --help help for authctl + +Use "authctl [command] --help" for more information about a command. + +unknown flag: --invalid-flag diff --git a/cmd/authctl/testdata/golden/TestRootCommand/Help_command b/cmd/authctl/testdata/golden/TestRootCommand/Help_command new file mode 100644 index 0000000000..111485b844 --- /dev/null +++ b/cmd/authctl/testdata/golden/TestRootCommand/Help_command @@ -0,0 +1,14 @@ +authctl is a command-line tool to interact with the authd service for user and group management. + +Usage: + authctl [flags] + authctl [command] + +Available Commands: + user Commands related to users + help Help about any command + +Flags: + -h, --help help for authctl + +Use "authctl [command] --help" for more information about a command. diff --git a/cmd/authctl/testdata/golden/TestRootCommand/Help_flag b/cmd/authctl/testdata/golden/TestRootCommand/Help_flag new file mode 100644 index 0000000000..111485b844 --- /dev/null +++ b/cmd/authctl/testdata/golden/TestRootCommand/Help_flag @@ -0,0 +1,14 @@ +authctl is a command-line tool to interact with the authd service for user and group management. + +Usage: + authctl [flags] + authctl [command] + +Available Commands: + user Commands related to users + help Help about any command + +Flags: + -h, --help help for authctl + +Use "authctl [command] --help" for more information about a command. diff --git a/cmd/authctl/testdata/golden/TestRootCommand/Usage_message_when_no_args b/cmd/authctl/testdata/golden/TestRootCommand/Usage_message_when_no_args new file mode 100644 index 0000000000..46485fdb0b --- /dev/null +++ b/cmd/authctl/testdata/golden/TestRootCommand/Usage_message_when_no_args @@ -0,0 +1,12 @@ +Usage: + authctl [flags] + authctl [command] + +Available Commands: + user Commands related to users + help Help about any command + +Flags: + -h, --help help for authctl + +Use "authctl [command] --help" for more information about a command. diff --git a/internal/testutils/authctl.go b/internal/testutils/authctl.go new file mode 100644 index 0000000000..4b7aa7c8a8 --- /dev/null +++ b/internal/testutils/authctl.go @@ -0,0 +1,30 @@ +package testutils + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" +) + +// BuildAuthctl builds the authctl binary in a temporary directory for testing purposes. +func BuildAuthctl() (binaryPath string, cleanup func(), err error) { + tempDir, err := os.MkdirTemp("", "authctl") + if err != nil { + return "", nil, fmt.Errorf("failed to create temp directory: %w", err) + } + cleanup = func() { os.RemoveAll(tempDir) } + binaryPath = filepath.Join(tempDir, "authctl") + + cmd := exec.Command("go", "build", "-o", binaryPath, "./cmd/authctl/main.go") + cmd.Dir = ProjectRoot() + + fmt.Fprintln(os.Stderr, "Running command:", cmd.String()) + if output, err := cmd.CombinedOutput(); err != nil { + cleanup() + fmt.Printf("Command output:\n%s\n", output) + return "", nil, fmt.Errorf("failed to build authctl: %w", err) + } + + return binaryPath, cleanup, nil +} From 5540089041a3084fa810f6f75c5cd4f9cdc559d3 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Fri, 20 Jun 2025 15:38:08 +0200 Subject: [PATCH 32/55] authctl: Add integration test for `authctl user` --- .../TestUserCommand/Error_on_invalid_command | 14 ++++ .../TestUserCommand/Error_on_invalid_flag | 14 ++++ .../testdata/golden/TestUserCommand/Help_flag | 14 ++++ .../Usage_message_when_no_args | 12 ++++ cmd/authctl/user/user_test.go | 67 +++++++++++++++++++ 5 files changed, 121 insertions(+) create mode 100644 cmd/authctl/user/testdata/golden/TestUserCommand/Error_on_invalid_command create mode 100644 cmd/authctl/user/testdata/golden/TestUserCommand/Error_on_invalid_flag create mode 100644 cmd/authctl/user/testdata/golden/TestUserCommand/Help_flag create mode 100644 cmd/authctl/user/testdata/golden/TestUserCommand/Usage_message_when_no_args create mode 100644 cmd/authctl/user/user_test.go diff --git a/cmd/authctl/user/testdata/golden/TestUserCommand/Error_on_invalid_command b/cmd/authctl/user/testdata/golden/TestUserCommand/Error_on_invalid_command new file mode 100644 index 0000000000..ce84afc2e3 --- /dev/null +++ b/cmd/authctl/user/testdata/golden/TestUserCommand/Error_on_invalid_command @@ -0,0 +1,14 @@ +Usage: + authctl user [flags] + authctl user [command] + +Available Commands: + lock Lock (disable) a user managed by authd + unlock Unlock (enable) a user managed by authd + +Flags: + -h, --help help for user + +Use "authctl user [command] --help" for more information about a command. + +unknown command "invalid-command" for "authctl user" diff --git a/cmd/authctl/user/testdata/golden/TestUserCommand/Error_on_invalid_flag b/cmd/authctl/user/testdata/golden/TestUserCommand/Error_on_invalid_flag new file mode 100644 index 0000000000..d3b1824aea --- /dev/null +++ b/cmd/authctl/user/testdata/golden/TestUserCommand/Error_on_invalid_flag @@ -0,0 +1,14 @@ +Usage: + authctl user [flags] + authctl user [command] + +Available Commands: + lock Lock (disable) a user managed by authd + unlock Unlock (enable) a user managed by authd + +Flags: + -h, --help help for user + +Use "authctl user [command] --help" for more information about a command. + +unknown flag: --invalid-flag diff --git a/cmd/authctl/user/testdata/golden/TestUserCommand/Help_flag b/cmd/authctl/user/testdata/golden/TestUserCommand/Help_flag new file mode 100644 index 0000000000..e56b67c1da --- /dev/null +++ b/cmd/authctl/user/testdata/golden/TestUserCommand/Help_flag @@ -0,0 +1,14 @@ +Commands related to users + +Usage: + authctl user [flags] + authctl user [command] + +Available Commands: + lock Lock (disable) a user managed by authd + unlock Unlock (enable) a user managed by authd + +Flags: + -h, --help help for user + +Use "authctl user [command] --help" for more information about a command. diff --git a/cmd/authctl/user/testdata/golden/TestUserCommand/Usage_message_when_no_args b/cmd/authctl/user/testdata/golden/TestUserCommand/Usage_message_when_no_args new file mode 100644 index 0000000000..76259d0f98 --- /dev/null +++ b/cmd/authctl/user/testdata/golden/TestUserCommand/Usage_message_when_no_args @@ -0,0 +1,12 @@ +Usage: + authctl user [flags] + authctl user [command] + +Available Commands: + lock Lock (disable) a user managed by authd + unlock Unlock (enable) a user managed by authd + +Flags: + -h, --help help for user + +Use "authctl user [command] --help" for more information about a command. diff --git a/cmd/authctl/user/user_test.go b/cmd/authctl/user/user_test.go new file mode 100644 index 0000000000..9af0b3f268 --- /dev/null +++ b/cmd/authctl/user/user_test.go @@ -0,0 +1,67 @@ +package user_test + +import ( + "fmt" + "os" + "os/exec" + "strings" + "testing" + + "github.com/ubuntu/authd/internal/testutils" + "github.com/ubuntu/authd/internal/testutils/golden" +) + +var authctlPath string + +func TestUserCommand(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{"user"}, tc.args...)...) + t.Logf("Running command: %s", strings.Join(cmd.Args, " ")) + outputBytes, err := cmd.CombinedOutput() + output := string(outputBytes) + exitCode := cmd.ProcessState.ExitCode() + + if tc.expectedExitCode == 0 && err != nil { + t.Logf("Command output:\n%s", output) + t.Errorf("Expected no error, but got: %v", err) + } + + if exitCode != tc.expectedExitCode { + t.Logf("Command output:\n%s", output) + t.Errorf("Expected exit code %d, got %d", tc.expectedExitCode, exitCode) + } + + golden.CheckOrUpdate(t, output) + }) + } +} + +func TestMain(m *testing.M) { + var cleanup func() + var err error + authctlPath, cleanup, err = testutils.BuildAuthctl() + if err != nil { + fmt.Fprintf(os.Stderr, "Setup: %v\n", err) + os.Exit(1) + } + defer cleanup() + + m.Run() +} From 8e37541af2c01b6fd78ba9593a6f7631d51dceac Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Fri, 20 Jun 2025 15:39:05 +0200 Subject: [PATCH 33/55] refactor: Rename RunDaemon -> StartDaemon I would expect a function called RunDaemon to start the daemon and block until it has finished running. StartDaemon makes it clear that the function returns once the daemon was started. --- internal/testutils/daemon.go | 4 ++-- nss/integration-tests/integration_test.go | 4 ++-- pam/integration-tests/helpers_test.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/testutils/daemon.go b/internal/testutils/daemon.go index 24620cb73e..c760e9a3af 100644 --- a/internal/testutils/daemon.go +++ b/internal/testutils/daemon.go @@ -113,9 +113,9 @@ func WithGroupFileOutput(groupFile string) DaemonOption { } } -// RunDaemon runs the daemon in a separate process and returns the socket path and a channel that will be closed when +// StartDaemon runs the daemon in a separate process and returns the socket path and a channel that will be closed when // the daemon stops. -func RunDaemon(ctx context.Context, t *testing.T, execPath string, args ...DaemonOption) (socketPath string, stopped chan struct{}) { +func StartDaemon(ctx context.Context, t *testing.T, execPath string, args ...DaemonOption) (socketPath string, stopped chan struct{}) { t.Helper() opts := &daemonOptions{} diff --git a/nss/integration-tests/integration_test.go b/nss/integration-tests/integration_test.go index 22a2ac62a3..d7b99d1b48 100644 --- a/nss/integration-tests/integration_test.go +++ b/nss/integration-tests/integration_test.go @@ -30,7 +30,7 @@ func TestIntegration(t *testing.T) { defaultGroupsFilePath := filepath.Join(filepath.Join("testdata", "empty.group")) ctx, cancel := context.WithCancel(context.Background()) - _, stopped := testutils.RunDaemon(ctx, t, daemonPath, + _, stopped := testutils.StartDaemon(ctx, t, daemonPath, testutils.WithSocketPath(defaultSocket), testutils.WithPreviousDBState(defaultDbState), testutils.WithGroupFile(defaultGroupsFilePath), @@ -115,7 +115,7 @@ func TestIntegration(t *testing.T) { // Run a specific new daemon for special test cases. var daemonStopped chan struct{} ctx, cancel := context.WithCancel(context.Background()) - socketPath, daemonStopped = testutils.RunDaemon(ctx, t, daemonPath, + socketPath, daemonStopped = testutils.StartDaemon(ctx, t, daemonPath, testutils.WithPreviousDBState(tc.dbState), testutils.WithGroupFile(defaultGroupsFilePath), ) diff --git a/pam/integration-tests/helpers_test.go b/pam/integration-tests/helpers_test.go index 2f22577f6b..a010588569 100644 --- a/pam/integration-tests/helpers_test.go +++ b/pam/integration-tests/helpers_test.go @@ -80,7 +80,7 @@ func runAuthdForTesting(t *testing.T, currentUserAsRoot bool, isSharedDaemon boo args = append(args, testutils.WithDBPath(filepath.Dir(database))) } - socketPath, stopped := testutils.RunDaemon(ctx, t, daemonPath, args...) + socketPath, stopped := testutils.StartDaemon(ctx, t, daemonPath, args...) saveArtifactsForDebugOnCleanup(t, []string{outputFile}) return socketPath, func() { cancel() From 6f7525b63fd65aae5418fede43c38b8ce5c126df Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Mon, 23 Jun 2025 13:00:47 +0200 Subject: [PATCH 34/55] Improve error message --- cmd/authctl/user/user.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/authctl/user/user.go b/cmd/authctl/user/user.go index c6ea554413..d81366a0d6 100644 --- a/cmd/authctl/user/user.go +++ b/cmd/authctl/user/user.go @@ -2,6 +2,7 @@ package user import ( + "fmt" "os" "github.com/spf13/cobra" @@ -28,7 +29,7 @@ func NewUserServiceClient() (authd.UserServiceClient, error) { conn, err := grpc.NewClient(authdSocket, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to connect to authd: %w", err) } client := authd.NewUserServiceClient(conn) From f9a85f226ff132639ccf09f434e8124ad846965c Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Mon, 23 Jun 2025 13:06:36 +0200 Subject: [PATCH 35/55] authctl: Add integration test for `authctl user lock` --- .../testdata/db/one_user_and_group.db.yaml | 17 +++++ cmd/authctl/user/testdata/empty.group | 0 .../Error_locking_invalid_user | 2 + .../TestUserLockCommand/Lock_user_success | 1 + cmd/authctl/user/user_test.go | 70 ++++++++++++++++++- 5 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 cmd/authctl/user/testdata/db/one_user_and_group.db.yaml create mode 100644 cmd/authctl/user/testdata/empty.group create mode 100644 cmd/authctl/user/testdata/golden/TestUserLockCommand/Error_locking_invalid_user create mode 100644 cmd/authctl/user/testdata/golden/TestUserLockCommand/Lock_user_success diff --git a/cmd/authctl/user/testdata/db/one_user_and_group.db.yaml b/cmd/authctl/user/testdata/db/one_user_and_group.db.yaml new file mode 100644 index 0000000000..77567897ae --- /dev/null +++ b/cmd/authctl/user/testdata/db/one_user_and_group.db.yaml @@ -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 diff --git a/cmd/authctl/user/testdata/empty.group b/cmd/authctl/user/testdata/empty.group new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/authctl/user/testdata/golden/TestUserLockCommand/Error_locking_invalid_user b/cmd/authctl/user/testdata/golden/TestUserLockCommand/Error_locking_invalid_user new file mode 100644 index 0000000000..6040e88661 --- /dev/null +++ b/cmd/authctl/user/testdata/golden/TestUserLockCommand/Error_locking_invalid_user @@ -0,0 +1,2 @@ +Locking user "invaliduser"... +Error: user "invaliduser" not found diff --git a/cmd/authctl/user/testdata/golden/TestUserLockCommand/Lock_user_success b/cmd/authctl/user/testdata/golden/TestUserLockCommand/Lock_user_success new file mode 100644 index 0000000000..780ae7ba1f --- /dev/null +++ b/cmd/authctl/user/testdata/golden/TestUserLockCommand/Lock_user_success @@ -0,0 +1 @@ +Locking user "user1"... diff --git a/cmd/authctl/user/user_test.go b/cmd/authctl/user/user_test.go index 9af0b3f268..35c475844a 100644 --- a/cmd/authctl/user/user_test.go +++ b/cmd/authctl/user/user_test.go @@ -1,17 +1,22 @@ package user_test import ( + "context" "fmt" "os" "os/exec" + "path/filepath" "strings" "testing" + "github.com/stretchr/testify/require" "github.com/ubuntu/authd/internal/testutils" "github.com/ubuntu/authd/internal/testutils/golden" + "google.golang.org/grpc/codes" ) var authctlPath string +var daemonPath string func TestUserCommand(t *testing.T) { t.Parallel() @@ -53,15 +58,74 @@ func TestUserCommand(t *testing.T) { } } +func TestUserLockCommand(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + daemonSocket, stopped := testutils.StartDaemon(ctx, t, daemonPath, + testutils.WithGroupFile(filepath.Join("testdata", "empty.group")), + testutils.WithPreviousDBState("one_user_and_group"), + testutils.WithEnvironment("AUTHD_INTEGRATIONTESTS_CURRENT_USER_AS_ROOT=1"), + ) + + t.Cleanup(func() { + t.Log("Stopping daemon...") + cancel() + <-stopped + }) + + err := os.Setenv("AUTHD_SOCKET", "unix://"+daemonSocket) + require.NoError(t, err, "Failed to set AUTHD_SOCKET environment variable") + + tests := map[string]struct { + args []string + expectedExitCode int + }{ + "Lock_user_success": {args: []string{"lock", "user1"}, expectedExitCode: 0}, + + "Error_locking_invalid_user": {args: []string{"lock", "invaliduser"}, expectedExitCode: int(codes.NotFound)}, + } + + 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{"user"}, tc.args...)...) + t.Logf("Running command: %s", strings.Join(cmd.Args, " ")) + outputBytes, err := cmd.CombinedOutput() + output := string(outputBytes) + exitCode := cmd.ProcessState.ExitCode() + + t.Logf("Command output:\n%s", output) + + if tc.expectedExitCode == 0 { + require.NoError(t, err) + } + require.Equal(t, tc.expectedExitCode, exitCode, "Expected exit code does not match actual exit code") + + golden.CheckOrUpdate(t, output) + }) + } +} + func TestMain(m *testing.M) { - var cleanup func() + var authctlCleanup func() var err error - authctlPath, cleanup, err = testutils.BuildAuthctl() + 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.BuildDaemon("-tags=withexamplebroker,integrationtests") if err != nil { fmt.Fprintf(os.Stderr, "Setup: %v\n", err) os.Exit(1) } - defer cleanup() + defer daemonCleanup() m.Run() } From fcbe525c310a6df69dbf89b4fdd92050ff0d38b1 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Fri, 20 Jun 2025 16:23:49 +0200 Subject: [PATCH 36/55] refactor: Inline extra args in BuildDaemon() The BuildDaemon() function is only called to build the daemon for integration tests with the example broker. Lets avoid passing the same arguments everywhere. Also renames the function to BuildDaemonWithExampleBroker. --- cmd/authctl/user/user_test.go | 2 +- internal/testutils/daemon.go | 6 +++--- nss/integration-tests/integration_test.go | 5 +++-- pam/integration-tests/integration_test.go | 9 +++++---- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/cmd/authctl/user/user_test.go b/cmd/authctl/user/user_test.go index 35c475844a..9dbc75ce64 100644 --- a/cmd/authctl/user/user_test.go +++ b/cmd/authctl/user/user_test.go @@ -120,7 +120,7 @@ func TestMain(m *testing.M) { defer authctlCleanup() var daemonCleanup func() - daemonPath, daemonCleanup, err = testutils.BuildDaemon("-tags=withexamplebroker,integrationtests") + daemonPath, daemonCleanup, err = testutils.BuildDaemonWithExampleBroker() if err != nil { fmt.Fprintf(os.Stderr, "Setup: %v\n", err) os.Exit(1) diff --git a/internal/testutils/daemon.go b/internal/testutils/daemon.go index c760e9a3af..ab56993f71 100644 --- a/internal/testutils/daemon.go +++ b/internal/testutils/daemon.go @@ -249,8 +249,8 @@ paths: return opts.socketPath, stopped } -// BuildDaemon builds the daemon executable and returns the binary path. -func BuildDaemon(extraArgs ...string) (execPath string, cleanup func(), err error) { +// BuildDaemonWithExampleBroker builds the daemon executable and returns the binary path. +func BuildDaemonWithExampleBroker() (execPath string, cleanup func(), err error) { projectRoot := ProjectRoot() tempDir, err := os.MkdirTemp("", "authd-tests-daemon") @@ -273,7 +273,7 @@ func BuildDaemon(extraArgs ...string) (execPath string, cleanup func(), err erro cmd.Args = append(cmd.Args, "-race") } cmd.Args = append(cmd.Args, "-gcflags=all=-N -l") - cmd.Args = append(cmd.Args, extraArgs...) + cmd.Args = append(cmd.Args, "-tags=withexamplebroker,integrationtests") cmd.Args = append(cmd.Args, "-o", execPath, "./cmd/authd") if out, err := cmd.CombinedOutput(); err != nil { diff --git a/nss/integration-tests/integration_test.go b/nss/integration-tests/integration_test.go index d7b99d1b48..c53a1381e0 100644 --- a/nss/integration-tests/integration_test.go +++ b/nss/integration-tests/integration_test.go @@ -167,13 +167,14 @@ func TestIntegration(t *testing.T) { } func TestMain(m *testing.M) { - execPath, cleanup, err := testutils.BuildDaemon("-tags=withexamplebroker,integrationtests") + var cleanup func() + var err error + daemonPath, cleanup, err = testutils.BuildDaemonWithExampleBroker() if err != nil { log.Printf("Setup: failed to build daemon: %v", err) os.Exit(1) } defer cleanup() - daemonPath = execPath m.Run() } diff --git a/pam/integration-tests/integration_test.go b/pam/integration-tests/integration_test.go index 7b0fd25e92..6cddb606b5 100644 --- a/pam/integration-tests/integration_test.go +++ b/pam/integration-tests/integration_test.go @@ -13,13 +13,14 @@ const authdCurrentUserRootEnvVariableContent = "AUTHD_INTEGRATIONTESTS_CURRENT_U var daemonPath string func TestMain(m *testing.M) { - execPath, daemonCleanup, err := testutils.BuildDaemon("-tags=withexamplebroker,integrationtests") + var cleanup func() + var err error + daemonPath, cleanup, err = testutils.BuildDaemonWithExampleBroker() if err != nil { - log.Printf("Setup: Failed to build authd daemon: %v", err) + log.Printf("Setup: failed to build daemon: %v", err) os.Exit(1) } - defer daemonCleanup() - daemonPath = execPath + defer cleanup() m.Run() } From 0feb3880a22dca61b1f8d1c936a2997c4d6f7347 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Mon, 23 Jun 2025 13:43:27 +0200 Subject: [PATCH 37/55] refactor: Add WithCurrentUserAsRoot option for testutils.StartDaemon() --- cmd/authctl/user/user_test.go | 2 +- internal/testutils/daemon.go | 7 +++++++ nss/integration-tests/integration_test.go | 2 +- pam/integration-tests/helpers_test.go | 10 ++++------ pam/integration-tests/integration_test.go | 2 -- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/cmd/authctl/user/user_test.go b/cmd/authctl/user/user_test.go index 9dbc75ce64..3c60691d51 100644 --- a/cmd/authctl/user/user_test.go +++ b/cmd/authctl/user/user_test.go @@ -65,7 +65,7 @@ func TestUserLockCommand(t *testing.T) { daemonSocket, stopped := testutils.StartDaemon(ctx, t, daemonPath, testutils.WithGroupFile(filepath.Join("testdata", "empty.group")), testutils.WithPreviousDBState("one_user_and_group"), - testutils.WithEnvironment("AUTHD_INTEGRATIONTESTS_CURRENT_USER_AS_ROOT=1"), + testutils.WithCurrentUserAsRoot, ) t.Cleanup(func() { diff --git a/internal/testutils/daemon.go b/internal/testutils/daemon.go index ab56993f71..b544a95713 100644 --- a/internal/testutils/daemon.go +++ b/internal/testutils/daemon.go @@ -113,6 +113,13 @@ func WithGroupFileOutput(groupFile string) DaemonOption { } } +// WithCurrentUserAsRoot configures the daemon to accept the current user as root when checking permissions. +// This is useful for integration tests where the current user is not root, but we want to +// test the behavior as if it were root. +var WithCurrentUserAsRoot DaemonOption = func(o *daemonOptions) { + o.env = append(o.env, "AUTHD_INTEGRATIONTESTS_CURRENT_USER_AS_ROOT=1") +} + // StartDaemon runs the daemon in a separate process and returns the socket path and a channel that will be closed when // the daemon stops. func StartDaemon(ctx context.Context, t *testing.T, execPath string, args ...DaemonOption) (socketPath string, stopped chan struct{}) { diff --git a/nss/integration-tests/integration_test.go b/nss/integration-tests/integration_test.go index c53a1381e0..45530d5982 100644 --- a/nss/integration-tests/integration_test.go +++ b/nss/integration-tests/integration_test.go @@ -34,7 +34,7 @@ func TestIntegration(t *testing.T) { testutils.WithSocketPath(defaultSocket), testutils.WithPreviousDBState(defaultDbState), testutils.WithGroupFile(defaultGroupsFilePath), - testutils.WithEnvironment("AUTHD_INTEGRATIONTESTS_CURRENT_USER_AS_ROOT=1"), + testutils.WithCurrentUserAsRoot, ) t.Cleanup(func() { diff --git a/pam/integration-tests/helpers_test.go b/pam/integration-tests/helpers_test.go index a010588569..c93534a1b7 100644 --- a/pam/integration-tests/helpers_test.go +++ b/pam/integration-tests/helpers_test.go @@ -56,12 +56,6 @@ func runAuthdForTesting(t *testing.T, currentUserAsRoot bool, isSharedDaemon boo ctx, cancel := context.WithCancel(context.Background()) - var env []string - if currentUserAsRoot { - env = append(env, authdCurrentUserRootEnvVariableContent) - } - args = append(args, testutils.WithEnvironment(env...)) - outputFile := filepath.Join(t.TempDir(), "authd.log") args = append(args, testutils.WithOutputFile(outputFile)) @@ -70,6 +64,10 @@ func runAuthdForTesting(t *testing.T, currentUserAsRoot bool, isSharedDaemon boo require.NoError(t, err, "Setup: Creating home base dir %q", homeBaseDir) args = append(args, testutils.WithHomeBaseDir(homeBaseDir)) + if currentUserAsRoot { + args = append(args, testutils.WithCurrentUserAsRoot) + } + if !isSharedDaemon { database := filepath.Join(t.TempDir(), "db", consts.DefaultDatabaseFileName) args = append(args, testutils.WithDBPath(filepath.Dir(database))) diff --git a/pam/integration-tests/integration_test.go b/pam/integration-tests/integration_test.go index 6cddb606b5..930eb3bf1a 100644 --- a/pam/integration-tests/integration_test.go +++ b/pam/integration-tests/integration_test.go @@ -8,8 +8,6 @@ import ( "github.com/ubuntu/authd/internal/testutils" ) -const authdCurrentUserRootEnvVariableContent = "AUTHD_INTEGRATIONTESTS_CURRENT_USER_AS_ROOT=1" - var daemonPath string func TestMain(m *testing.M) { From 7d7c7d9f5b4b40a2e05e5c8fdd2f675ca2eb9d75 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Mon, 23 Jun 2025 16:03:57 +0200 Subject: [PATCH 38/55] refactor: Register cleanup of daemon process as part of StartDaemon() Everywhere we called StartDaemon(), we registered the cleanup of the daemon process via t.Cleanup(). Let's avoid the duplicate code by inlining the cleanup registration into StartDaemon(). --- cmd/authctl/user/user_test.go | 10 +-------- internal/testutils/daemon.go | 26 +++++++++++++++------- nss/integration-tests/integration_test.go | 17 ++------------ pam/integration-tests/helpers_test.go | 27 +++++++++++------------ 4 files changed, 34 insertions(+), 46 deletions(-) diff --git a/cmd/authctl/user/user_test.go b/cmd/authctl/user/user_test.go index 3c60691d51..412511730f 100644 --- a/cmd/authctl/user/user_test.go +++ b/cmd/authctl/user/user_test.go @@ -1,7 +1,6 @@ package user_test import ( - "context" "fmt" "os" "os/exec" @@ -61,19 +60,12 @@ func TestUserCommand(t *testing.T) { func TestUserLockCommand(t *testing.T) { t.Parallel() - ctx, cancel := context.WithCancel(context.Background()) - daemonSocket, stopped := testutils.StartDaemon(ctx, t, daemonPath, + daemonSocket := testutils.StartDaemon(t, daemonPath, testutils.WithGroupFile(filepath.Join("testdata", "empty.group")), testutils.WithPreviousDBState("one_user_and_group"), testutils.WithCurrentUserAsRoot, ) - t.Cleanup(func() { - t.Log("Stopping daemon...") - cancel() - <-stopped - }) - err := os.Setenv("AUTHD_SOCKET", "unix://"+daemonSocket) require.NoError(t, err, "Failed to set AUTHD_SOCKET environment variable") diff --git a/internal/testutils/daemon.go b/internal/testutils/daemon.go index b544a95713..22bdd23b58 100644 --- a/internal/testutils/daemon.go +++ b/internal/testutils/daemon.go @@ -120,9 +120,17 @@ var WithCurrentUserAsRoot DaemonOption = func(o *daemonOptions) { o.env = append(o.env, "AUTHD_INTEGRATIONTESTS_CURRENT_USER_AS_ROOT=1") } -// StartDaemon runs the daemon in a separate process and returns the socket path and a channel that will be closed when -// the daemon stops. -func StartDaemon(ctx context.Context, t *testing.T, execPath string, args ...DaemonOption) (socketPath string, stopped chan struct{}) { +// StartDaemon starts the daemon in a separate process and returns the socket path. +func StartDaemon(t *testing.T, execPath string, args ...DaemonOption) (socketPath string) { + t.Helper() + + socketPath, cancelFunc := StartDaemonWithCancel(t, execPath, args...) + t.Cleanup(cancelFunc) + return socketPath +} + +// StartDaemonWithCancel starts the daemon in a separate process and returns the socket path and a cancel function. +func StartDaemonWithCancel(t *testing.T, execPath string, args ...DaemonOption) (socketPath string, cancelFunc func()) { t.Helper() opts := &daemonOptions{} @@ -158,9 +166,12 @@ paths: configPath := filepath.Join(tempDir, "testconfig.yaml") require.NoError(t, os.WriteFile(configPath, []byte(config), 0600), "Setup: failed to create config file for tests") - var cancel context.CancelCauseFunc - if opts.pidFile != "" { - ctx, cancel = context.WithCancelCause(ctx) + stopped := make(chan struct{}) + ctx, cancel := context.WithCancelCause(context.Background()) + cancelFunc = func() { + t.Log("Stopping daemon...") + cancel(nil) + <-stopped } // #nosec:G204 - we control the command arguments in tests @@ -176,7 +187,6 @@ paths: } // Start the daemon - stopped = make(chan struct{}) processPid := make(chan int) go func() { defer close(stopped) @@ -253,7 +263,7 @@ paths: }() } - return opts.socketPath, stopped + return opts.socketPath, cancelFunc } // BuildDaemonWithExampleBroker builds the daemon executable and returns the binary path. diff --git a/nss/integration-tests/integration_test.go b/nss/integration-tests/integration_test.go index 45530d5982..d5a87bfa12 100644 --- a/nss/integration-tests/integration_test.go +++ b/nss/integration-tests/integration_test.go @@ -1,7 +1,6 @@ package nss_test import ( - "context" "log" "os" "path/filepath" @@ -29,19 +28,13 @@ func TestIntegration(t *testing.T) { defaultDbState := "multiple_users_and_groups" defaultGroupsFilePath := filepath.Join(filepath.Join("testdata", "empty.group")) - ctx, cancel := context.WithCancel(context.Background()) - _, stopped := testutils.StartDaemon(ctx, t, daemonPath, + testutils.StartDaemon(t, daemonPath, testutils.WithSocketPath(defaultSocket), testutils.WithPreviousDBState(defaultDbState), testutils.WithGroupFile(defaultGroupsFilePath), testutils.WithCurrentUserAsRoot, ) - t.Cleanup(func() { - cancel() - <-stopped - }) - tests := map[string]struct { getentDB string key string @@ -113,16 +106,10 @@ func TestIntegration(t *testing.T) { if useAlternativeDaemon { // Run a specific new daemon for special test cases. - var daemonStopped chan struct{} - ctx, cancel := context.WithCancel(context.Background()) - socketPath, daemonStopped = testutils.StartDaemon(ctx, t, daemonPath, + socketPath = testutils.StartDaemon(t, daemonPath, testutils.WithPreviousDBState(tc.dbState), testutils.WithGroupFile(defaultGroupsFilePath), ) - t.Cleanup(func() { - cancel() - <-daemonStopped - }) } cmds := []string{tc.getentDB} diff --git a/pam/integration-tests/helpers_test.go b/pam/integration-tests/helpers_test.go index c93534a1b7..55f460bec3 100644 --- a/pam/integration-tests/helpers_test.go +++ b/pam/integration-tests/helpers_test.go @@ -50,11 +50,16 @@ var ( sharedAuthdInstance = authdInstance{} ) -func runAuthdForTesting(t *testing.T, currentUserAsRoot bool, isSharedDaemon bool, args ...testutils.DaemonOption) ( - socketPath string, waitFunc func()) { +func runAuthdForTesting(t *testing.T, currentUserAsRoot bool, isSharedDaemon bool, args ...testutils.DaemonOption) (socketPath string) { t.Helper() - ctx, cancel := context.WithCancel(context.Background()) + socketPath, cancelFunc := runAuthdForTestingWithCancel(t, currentUserAsRoot, isSharedDaemon, args...) + t.Cleanup(cancelFunc) + return socketPath +} + +func runAuthdForTestingWithCancel(t *testing.T, currentUserAsRoot bool, isSharedDaemon bool, args ...testutils.DaemonOption) (socketPath string, cancelFunc func()) { + t.Helper() outputFile := filepath.Join(t.TempDir(), "authd.log") args = append(args, testutils.WithOutputFile(outputFile)) @@ -78,20 +83,15 @@ func runAuthdForTesting(t *testing.T, currentUserAsRoot bool, isSharedDaemon boo args = append(args, testutils.WithDBPath(filepath.Dir(database))) } - socketPath, stopped := testutils.StartDaemon(ctx, t, daemonPath, args...) + socketPath, cancelFunc = testutils.StartDaemonWithCancel(t, daemonPath, args...) saveArtifactsForDebugOnCleanup(t, []string{outputFile}) - return socketPath, func() { - cancel() - <-stopped - } + return socketPath, cancelFunc } func runAuthd(t *testing.T, currentUserAsRoot bool, args ...testutils.DaemonOption) string { t.Helper() - socketPath, waitFunc := runAuthdForTesting(t, currentUserAsRoot, false, args...) - t.Cleanup(waitFunc) - return socketPath + return runAuthdForTesting(t, currentUserAsRoot, false, args...) } func sharedAuthd(t *testing.T, args ...testutils.DaemonOption) (socketPath string, groupFile string) { @@ -108,8 +108,7 @@ func sharedAuthd(t *testing.T, args ...testutils.DaemonOption) (socketPath strin args = append(args, testutils.WithGroupFile(groups), testutils.WithGroupFileOutput(groupOutput)) - socket, cleanup := runAuthdForTesting(t, true, useSharedInstance, args...) - t.Cleanup(cleanup) + socket := runAuthdForTesting(t, true, useSharedInstance, args...) return socket, groupOutput } @@ -150,7 +149,7 @@ func sharedAuthd(t *testing.T, args ...testutils.DaemonOption) (socketPath strin args = append(args, testutils.WithGroupFile(sa.groupsFile)) sa.groupsOutputPath = filepath.Join(t.TempDir(), "groups") args = append(args, testutils.WithGroupFileOutput(sa.groupsOutputPath)) - sa.socketPath, sa.cleanup = runAuthdForTesting(t, true, useSharedInstance, args...) + sa.socketPath, sa.cleanup = runAuthdForTestingWithCancel(t, true, useSharedInstance, args...) return sa.socketPath, sa.groupsOutputPath } From 564bdd08f672aed0f1f5eda2b520c0a22acc3dbb Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Mon, 23 Jun 2025 17:55:53 +0200 Subject: [PATCH 39/55] authctl: Print no output on success Lets be more consistent with other command-line tools which control system services, like systemctl and machinectl, and not print any output if the command succeeds (for example like `systemctl start`/`systemctl stop` or `machinectl kill`). --- cmd/authctl/user/lock.go | 3 --- .../golden/TestUserLockCommand/Error_locking_invalid_user | 1 - .../user/testdata/golden/TestUserLockCommand/Lock_user_success | 1 - cmd/authctl/user/unlock.go | 3 --- 4 files changed, 8 deletions(-) diff --git a/cmd/authctl/user/lock.go b/cmd/authctl/user/lock.go index 9673e6ffbc..6f83b4e8b2 100644 --- a/cmd/authctl/user/lock.go +++ b/cmd/authctl/user/lock.go @@ -2,7 +2,6 @@ package user import ( "context" - "fmt" "github.com/spf13/cobra" "github.com/ubuntu/authd/internal/proto/authd" @@ -14,8 +13,6 @@ var lockCmd = &cobra.Command{ Short: "Lock (disable) a user managed by authd", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - fmt.Printf("Locking user %q...\n", args[0]) - client, err := NewUserServiceClient() if err != nil { return err diff --git a/cmd/authctl/user/testdata/golden/TestUserLockCommand/Error_locking_invalid_user b/cmd/authctl/user/testdata/golden/TestUserLockCommand/Error_locking_invalid_user index 6040e88661..93dd7dd5ff 100644 --- a/cmd/authctl/user/testdata/golden/TestUserLockCommand/Error_locking_invalid_user +++ b/cmd/authctl/user/testdata/golden/TestUserLockCommand/Error_locking_invalid_user @@ -1,2 +1 @@ -Locking user "invaliduser"... Error: user "invaliduser" not found diff --git a/cmd/authctl/user/testdata/golden/TestUserLockCommand/Lock_user_success b/cmd/authctl/user/testdata/golden/TestUserLockCommand/Lock_user_success index 780ae7ba1f..e69de29bb2 100644 --- a/cmd/authctl/user/testdata/golden/TestUserLockCommand/Lock_user_success +++ b/cmd/authctl/user/testdata/golden/TestUserLockCommand/Lock_user_success @@ -1 +0,0 @@ -Locking user "user1"... diff --git a/cmd/authctl/user/unlock.go b/cmd/authctl/user/unlock.go index d32714f237..4480eba584 100644 --- a/cmd/authctl/user/unlock.go +++ b/cmd/authctl/user/unlock.go @@ -2,7 +2,6 @@ package user import ( "context" - "fmt" "github.com/spf13/cobra" "github.com/ubuntu/authd/internal/proto/authd" @@ -14,8 +13,6 @@ var unlockCmd = &cobra.Command{ Short: "Unlock (enable) a user managed by authd", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - fmt.Printf("Unlocking user %q...\n", args[0]) - client, err := NewUserServiceClient() if err != nil { return err From df03efdd3a697e3735616b63ad4e246f15d3afc9 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Mon, 23 Jun 2025 18:24:41 +0200 Subject: [PATCH 40/55] Improve error message When trying to log in with a locked user, the following error message is printed: $ su test@ubudev1.onmicrosoft.com can't select broker: error PermissionDenied from server: can't start authentication transaction: rpc error: code = PermissionDenied desc = user test@ubudev1.onmicrosoft.com is locked The "can't start authentication transaction" part doesn't add any valuable information to the error message, which is already too long. By omitting it, we also avoid that the gRPC status is formatted to a string containing the error code, which would duplicate information which the caller adds to the error message. With this commit, the error message is simplified to: can't select broker: error PermissionDenied from server: user test@ubudev1.onmicrosoft.com is locked --- internal/services/pam/pam.go | 2 -- pam/integration-tests/gdm_test.go | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/services/pam/pam.go b/internal/services/pam/pam.go index 59596eb919..1f46537d3f 100644 --- a/internal/services/pam/pam.go +++ b/internal/services/pam/pam.go @@ -123,8 +123,6 @@ func (s Service) GetPreviousBroker(ctx context.Context, req *authd.GPBRequest) ( // SelectBroker starts a new session and selects the requested broker for the user. func (s Service) SelectBroker(ctx context.Context, req *authd.SBRequest) (resp *authd.SBResponse, err error) { - defer decorate.OnError(&err, "can't start authentication transaction") - username := req.GetUsername() brokerID := req.GetBrokerId() lang := req.GetLang() diff --git a/pam/integration-tests/gdm_test.go b/pam/integration-tests/gdm_test.go index 911b343cee..12c6662998 100644 --- a/pam/integration-tests/gdm_test.go +++ b/pam/integration-tests/gdm_test.go @@ -805,7 +805,7 @@ func TestGdmModule(t *testing.T) { }, }, wantPamErrorMessages: []string{ - "can't select broker: error InvalidArgument from server: can't start authentication transaction: rpc error: code = InvalidArgument desc = no user name provided", + "can't select broker: error InvalidArgument from server: no user name provided", }, wantError: pam.ErrSystem, wantAcctMgmtErr: pam_test.ErrIgnore, From 3dbded10ff372f5639b3a67d799988db51039429 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Mon, 23 Jun 2025 18:29:45 +0200 Subject: [PATCH 41/55] Further improve error message Following up on the error message printed when trying to log in as a locked user, we can further improve the message: $ su test@ubudev1.onmicrosoft.com can't select broker: error PermissionDenied from server: user test@ubudev1.onmicrosoft.com is locked by omitting the "can't select broker:" part, which doesn't add any useful information. With this commit, the error message is simplified to: error PermissionDenied from server: user test@ubudev1.onmicrosoft.com is locked --- pam/integration-tests/gdm_test.go | 2 +- .../Deny_authentication_if_user_does_not_exist | 2 +- .../Prevent_change_password_if_user_does_not_exist | 2 +- .../Deny_authentication_if_user_does_not_exist | 2 +- .../Prevent_change_password_if_user_does_not_exist | 2 +- pam/internal/adapter/commands.go | 2 +- pam/internal/adapter/gdmmodel_test.go | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pam/integration-tests/gdm_test.go b/pam/integration-tests/gdm_test.go index 12c6662998..cfd51b0fd0 100644 --- a/pam/integration-tests/gdm_test.go +++ b/pam/integration-tests/gdm_test.go @@ -805,7 +805,7 @@ func TestGdmModule(t *testing.T) { }, }, wantPamErrorMessages: []string{ - "can't select broker: error InvalidArgument from server: no user name provided", + "error InvalidArgument from server: no user name provided", }, wantError: pam.ErrSystem, wantAcctMgmtErr: pam_test.ErrIgnore, diff --git a/pam/integration-tests/testdata/golden/TestCLIAuthenticate/Deny_authentication_if_user_does_not_exist b/pam/integration-tests/testdata/golden/TestCLIAuthenticate/Deny_authentication_if_user_does_not_exist index 58714a3771..83ff5abf28 100644 --- a/pam/integration-tests/testdata/golden/TestCLIAuthenticate/Deny_authentication_if_user_does_not_exist +++ b/pam/integration-tests/testdata/golden/TestCLIAuthenticate/Deny_authentication_if_user_does_not_exist @@ -18,7 +18,7 @@ Username: user-unexistent 1. local > 2. ExampleBroker -PAM Error Message: can't select broker: user "user-unexistent" does not exist +PAM Error Message: user "user-unexistent" does not exist PAM Authenticate() User: "user-unexistent" Result: error: PAM exit code: 4 diff --git a/pam/integration-tests/testdata/golden/TestCLIChangeAuthTok/Prevent_change_password_if_user_does_not_exist b/pam/integration-tests/testdata/golden/TestCLIChangeAuthTok/Prevent_change_password_if_user_does_not_exist index 725d73f473..4a38d4a8d0 100644 --- a/pam/integration-tests/testdata/golden/TestCLIChangeAuthTok/Prevent_change_password_if_user_does_not_exist +++ b/pam/integration-tests/testdata/golden/TestCLIChangeAuthTok/Prevent_change_password_if_user_does_not_exist @@ -18,7 +18,7 @@ Username: user-unexistent 1. local > 2. ExampleBroker -PAM Error Message: can't select broker: user "user-unexistent" does not exist +PAM Error Message: user "user-unexistent" does not exist PAM ChangeAuthTok() User: "user-unexistent" Result: error: PAM exit code: 4 diff --git a/pam/integration-tests/testdata/golden/TestNativeAuthenticate/Deny_authentication_if_user_does_not_exist b/pam/integration-tests/testdata/golden/TestNativeAuthenticate/Deny_authentication_if_user_does_not_exist index b7ce250a7f..c7d2b094a2 100644 --- a/pam/integration-tests/testdata/golden/TestNativeAuthenticate/Deny_authentication_if_user_does_not_exist +++ b/pam/integration-tests/testdata/golden/TestNativeAuthenticate/Deny_authentication_if_user_does_not_exist @@ -18,7 +18,7 @@ Choose your provider: 2. ExampleBroker Choose your provider: > 2 -PAM Error Message: can't select broker: user "user-unexistent" does not exist +PAM Error Message: user "user-unexistent" does not exist PAM Authenticate() User: "user-unexistent" Result: error: PAM exit code: 4 diff --git a/pam/integration-tests/testdata/golden/TestNativeChangeAuthTok/Prevent_change_password_if_user_does_not_exist b/pam/integration-tests/testdata/golden/TestNativeChangeAuthTok/Prevent_change_password_if_user_does_not_exist index b7c9d98836..7c7e24b4ad 100644 --- a/pam/integration-tests/testdata/golden/TestNativeChangeAuthTok/Prevent_change_password_if_user_does_not_exist +++ b/pam/integration-tests/testdata/golden/TestNativeChangeAuthTok/Prevent_change_password_if_user_does_not_exist @@ -27,7 +27,7 @@ Username: user-unexistent Or enter 'r' to go back to user selection Choose your provider: > 2 -PAM Error Message: can't select broker: user "user-unexistent" does not exist +PAM Error Message: user "user-unexistent" does not exist PAM ChangeAuthTok() User: "user-unexistent" Result: error: PAM exit code: 4 diff --git a/pam/internal/adapter/commands.go b/pam/internal/adapter/commands.go index 0c8b798096..c0dfe15567 100644 --- a/pam/internal/adapter/commands.go +++ b/pam/internal/adapter/commands.go @@ -47,7 +47,7 @@ func startBrokerSession(client authd.PAMClient, brokerID, username string, mode sbResp, err := client.SelectBroker(context.TODO(), sbReq) if err != nil { - return pamError{status: pam.ErrSystem, msg: fmt.Sprintf("can't select broker: %v", err)} + return pamError{status: pam.ErrSystem, msg: err.Error()} } sessionID := sbResp.GetSessionId() diff --git a/pam/internal/adapter/gdmmodel_test.go b/pam/internal/adapter/gdmmodel_test.go index ede22f98bd..1cc32f3a6b 100644 --- a/pam/internal/adapter/gdmmodel_test.go +++ b/pam/internal/adapter/gdmmodel_test.go @@ -1775,7 +1775,7 @@ func TestGdmModel(t *testing.T) { }, wantExitStatus: pamError{ status: pam.ErrSystem, - msg: "can't select broker: error during broker selection", + msg: "error during broker selection", }, }, "Error_during_broker_selection_if_session_ID_is_empty": { From 14cafa28a08b9072ef2de32adc973b10d4d3caf0 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Mon, 23 Jun 2025 18:36:26 +0200 Subject: [PATCH 42/55] Further improve error message Further improve the error message printed when trying to log in with a locked user from: error PermissionDenied from server: user test@ubudev1.onmicrosoft.com is locked to permission denied: user test@ubudev1.onmicrosoft.com is locked --- internal/services/errmessages/redactor.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/services/errmessages/redactor.go b/internal/services/errmessages/redactor.go index c0881458f1..3fa37dd059 100644 --- a/internal/services/errmessages/redactor.go +++ b/internal/services/errmessages/redactor.go @@ -52,6 +52,9 @@ func FormatErrorMessage(ctx context.Context, method string, req, reply any, cc * // likely means that IsAuthenticated got cancelled, so we need to keep the error intact case codes.Canceled: break + case codes.PermissionDenied: + // permission denied, just format it + err = fmt.Errorf("permission denied: %v", st.Message()) // grpc error, just format it default: err = fmt.Errorf("error %s from server: %v", st.Code(), st.Message()) From e2e558eabc5b6779b84800f3d52957d50ef33402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Trevisan=20=28Trevi=C3=B1o=29?= Date: Mon, 7 Jul 2025 19:34:12 +0200 Subject: [PATCH 43/55] pam/integration-tests/ssh: Add a simple lock/unlock test via SSH Co-Authored-By: Adrian Dombeck --- pam/integration-tests/ssh_test.go | 10 ++ .../Authenticate_user_locks_and_unlocks_it | 128 ++++++++++++++++++ ...e_user_locks_and_unlocks_it_on_shared_SSHd | 128 ++++++++++++++++++ .../tapes/ssh/simple_auth_locks_unlocks.tape | 83 ++++++++++++ 4 files changed, 349 insertions(+) create mode 100644 pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_locks_and_unlocks_it create mode 100644 pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_locks_and_unlocks_it_on_shared_SSHd create mode 100644 pam/integration-tests/testdata/tapes/ssh/simple_auth_locks_unlocks.tape diff --git a/pam/integration-tests/ssh_test.go b/pam/integration-tests/ssh_test.go index f677e7b0c4..d9db698ac1 100644 --- a/pam/integration-tests/ssh_test.go +++ b/pam/integration-tests/ssh_test.go @@ -134,6 +134,10 @@ func testSSHAuthenticate(t *testing.T, sharedSSHd bool) { sshEnvVariablesRegex = regexp.MustCompile(`(?m) (PATH|HOME|PWD|SSH_[A-Z]+)=.*(\n*)($[^ ]{2}.*)?$`) sshHostPortRegex = regexp.MustCompile(`([\d\.:]+) port ([\d:]+)`) + authctlPath, authctlCleanup, err := testutils.BuildAuthctl() + require.NoError(t, err) + t.Cleanup(authctlCleanup) + tests := map[string]struct { tape string tapeSettings []tapeSetting @@ -288,6 +292,10 @@ func testSSHAuthenticate(t *testing.T, sharedSSHd bool) { vhsCommandFinalAuthWaitVariable: `Wait /Password:/`, }, }, + "Authenticate_user_locks_and_unlocks_it": { + tape: "simple_auth_locks_unlocks", + daemonizeSSHd: true, + }, "Deny_authentication_if_max_attempts_reached": { tape: "max_attempts", @@ -462,6 +470,8 @@ Wait@%dms`, sshDefaultFinalWaitTimeout), td.Command = tapeCommand td.Env[pam_test.RunnerEnvSupportsConversation] = "1" td.Env[pamSSHUserEnv] = user + td.Env["AUTHD_SOCKET"] = "unix://" + socketPath + td.Env["AUTHCTL_PATH"] = authctlPath td.Env["AUTHD_PAM_SSH_ARGS"] = strings.Join([]string{ "-p", sshdPort, "-F", os.DevNull, diff --git a/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_locks_and_unlocks_it b/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_locks_and_unlocks_it new file mode 100644 index 0000000000..cd21f3e3a7 --- /dev/null +++ b/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_locks_and_unlocks_it @@ -0,0 +1,128 @@ +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Provider selection == + 1. local + 2. ExampleBroker +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it@localhost) Choose your provider: +> +──────────────────────────────────────────────────────────────────────────────── +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Provider selection == + 1. local + 2. ExampleBroker +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it@localhost) Choose your provider: +> 2 +──────────────────────────────────────────────────────────────────────────────── +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Provider selection == + 1. local + 2. ExampleBroker +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it@localhost) Choose your provider: +> 2 +== Password authentication == +Enter 'r' to cancel the request and go back to select the authentication method +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it@localhost) Gimme your password: +> +──────────────────────────────────────────────────────────────────────────────── +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Provider selection == + 1. local + 2. ExampleBroker +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it@localhost) Choose your provider: +> 2 +== Password authentication == +Enter 'r' to cancel the request and go back to select the authentication method +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it@localhost) Gimme your password: +> +PAM Authenticate() finished for user 'user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it' +PAM AcctMgmt() finished for user 'user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it' + SSHD: Connected to ssh via authd module! [TestSSHAuthenticate/Authenticate_user_locks_and_unlocks_it] + HOME=${AUTHD_TEST_HOME} + LOGNAME=user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it + PATH=${AUTHD_TEST_PATH} + PWD=${AUTHD_TEST_PWD} + SHELL=/bin/sh + SSH_CLIENT=${AUTHD_TEST_SSH_CLIENT} + SSH_CONNECTION=${AUTHD_TEST_SSH_CONNECTION} + SSH_TTY=${AUTHD_TEST_SSH_TTY} + TERM=xterm-256color + USER=user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it +Connection to localhost closed. +> +──────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────── +> ${AUTHCTL_PATH} --help +authctl is a command-line tool to interact with the authd service for user and group management. + +Usage: + authctl [flags] + authctl [command] + +Available Commands: + user Commands related to users + help Help about any command + +Flags: + -h, --help help for authctl + +Use "authctl [command] --help" for more information about a command. +> +──────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────── +> echo $? +0 +> +──────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────── +> ${AUTHCTL_PATH} user lock ${AUTHD_PAM_SSH_USER} +> +──────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────── +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +permission denied: user user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it is locked +Received disconnect from ${SSH_HOST} port ${SSH_PORT} Too many authentication failures +Disconnected from ${SSH_HOST} port ${SSH_PORT} +> +──────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────── +> ${AUTHCTL_PATH} user lock ${AUTHD_PAM_SSH_USER} +> +──────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────── +> ${AUTHCTL_PATH} user unlock ${AUTHD_PAM_SSH_USER} +> +──────────────────────────────────────────────────────────────────────────────── +> ${AUTHCTL_PATH} user unlock ${AUTHD_PAM_SSH_USER} +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Password authentication == +Enter 'r' to cancel the request and go back to select the authentication method +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it@localhost) Gimme your password: +> +──────────────────────────────────────────────────────────────────────────────── +> ${AUTHCTL_PATH} user unlock ${AUTHD_PAM_SSH_USER} +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Password authentication == +Enter 'r' to cancel the request and go back to select the authentication method +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it@localhost) Gimme your password: +> +PAM Authenticate() finished for user 'user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it' +PAM AcctMgmt() finished for user 'user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it' + SSHD: Connected to ssh via authd module! [TestSSHAuthenticate/Authenticate_user_locks_and_unlocks_it] + HOME=${AUTHD_TEST_HOME} + LOGNAME=user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it + PATH=${AUTHD_TEST_PATH} + PWD=${AUTHD_TEST_PWD} + SHELL=/bin/sh + SSH_CLIENT=${AUTHD_TEST_SSH_CLIENT} + SSH_CONNECTION=${AUTHD_TEST_SSH_CONNECTION} + SSH_TTY=${AUTHD_TEST_SSH_TTY} + TERM=xterm-256color + USER=user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it +Connection to localhost closed. +> +──────────────────────────────────────────────────────────────────────────────── diff --git a/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_locks_and_unlocks_it_on_shared_SSHd b/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_locks_and_unlocks_it_on_shared_SSHd new file mode 100644 index 0000000000..1bbbe5d521 --- /dev/null +++ b/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_locks_and_unlocks_it_on_shared_SSHd @@ -0,0 +1,128 @@ +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Provider selection == + 1. local + 2. ExampleBroker +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd@localhost) Choose your provider: +> +──────────────────────────────────────────────────────────────────────────────── +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Provider selection == + 1. local + 2. ExampleBroker +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd@localhost) Choose your provider: +> 2 +──────────────────────────────────────────────────────────────────────────────── +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Provider selection == + 1. local + 2. ExampleBroker +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd@localhost) Choose your provider: +> 2 +== Password authentication == +Enter 'r' to cancel the request and go back to select the authentication method +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd@localhost) Gimme your password: +> +──────────────────────────────────────────────────────────────────────────────── +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Provider selection == + 1. local + 2. ExampleBroker +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd@localhost) Choose your provider: +> 2 +== Password authentication == +Enter 'r' to cancel the request and go back to select the authentication method +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd@localhost) Gimme your password: +> +PAM Authenticate() finished for user 'user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd' +PAM AcctMgmt() finished for user 'user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd' + SSHD: Connected to ssh via authd module! [TestSSHAuthenticate] + HOME=${AUTHD_TEST_HOME} + LOGNAME=user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd + PATH=${AUTHD_TEST_PATH} + PWD=${AUTHD_TEST_PWD} + SHELL=/bin/sh + SSH_CLIENT=${AUTHD_TEST_SSH_CLIENT} + SSH_CONNECTION=${AUTHD_TEST_SSH_CONNECTION} + SSH_TTY=${AUTHD_TEST_SSH_TTY} + TERM=xterm-256color + USER=user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd +Connection to localhost closed. +> +──────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────── +> ${AUTHCTL_PATH} --help +authctl is a command-line tool to interact with the authd service for user and group management. + +Usage: + authctl [flags] + authctl [command] + +Available Commands: + user Commands related to users + help Help about any command + +Flags: + -h, --help help for authctl + +Use "authctl [command] --help" for more information about a command. +> +──────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────── +> echo $? +0 +> +──────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────── +> ${AUTHCTL_PATH} user lock ${AUTHD_PAM_SSH_USER} +> +──────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────── +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +permission denied: user user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd is locked +Received disconnect from ${SSH_HOST} port ${SSH_PORT} Too many authentication failures +Disconnected from ${SSH_HOST} port ${SSH_PORT} +> +──────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────── +> ${AUTHCTL_PATH} user lock ${AUTHD_PAM_SSH_USER} +> +──────────────────────────────────────────────────────────────────────────────── +> +──────────────────────────────────────────────────────────────────────────────── +> ${AUTHCTL_PATH} user unlock ${AUTHD_PAM_SSH_USER} +> +──────────────────────────────────────────────────────────────────────────────── +> ${AUTHCTL_PATH} user unlock ${AUTHD_PAM_SSH_USER} +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Password authentication == +Enter 'r' to cancel the request and go back to select the authentication method +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd@localhost) Gimme your password: +> +──────────────────────────────────────────────────────────────────────────────── +> ${AUTHCTL_PATH} user unlock ${AUTHD_PAM_SSH_USER} +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Password authentication == +Enter 'r' to cancel the request and go back to select the authentication method +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd@localhost) Gimme your password: +> +PAM Authenticate() finished for user 'user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd' +PAM AcctMgmt() finished for user 'user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd' + SSHD: Connected to ssh via authd module! [TestSSHAuthenticate] + HOME=${AUTHD_TEST_HOME} + LOGNAME=user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd + PATH=${AUTHD_TEST_PATH} + PWD=${AUTHD_TEST_PWD} + SHELL=/bin/sh + SSH_CLIENT=${AUTHD_TEST_SSH_CLIENT} + SSH_CONNECTION=${AUTHD_TEST_SSH_CONNECTION} + SSH_TTY=${AUTHD_TEST_SSH_TTY} + TERM=xterm-256color + USER=user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd +Connection to localhost closed. +> +──────────────────────────────────────────────────────────────────────────────── diff --git a/pam/integration-tests/testdata/tapes/ssh/simple_auth_locks_unlocks.tape b/pam/integration-tests/testdata/tapes/ssh/simple_auth_locks_unlocks.tape new file mode 100644 index 0000000000..0d56bd202d --- /dev/null +++ b/pam/integration-tests/testdata/tapes/ssh/simple_auth_locks_unlocks.tape @@ -0,0 +1,83 @@ +Hide +TypeInPrompt+Shell "${AUTHD_TEST_TAPE_COMMAND}" +Enter +Wait+Prompt /Choose your provider/ +Show + +Hide +TypeInPrompt "2" +Show + +Hide +Enter +Wait+Prompt /Gimme your password/ +Show + +Hide +Type "goodpass" +Enter +${AUTHD_TEST_TAPE_COMMAND_AUTH_FINAL_WAIT} +Show + +ClearTerminal + +Hide +TypeInPrompt+Shell "${AUTHCTL_PATH} --help" +Enter +Wait +Show + +ClearTerminal + +Hide +TypeInPrompt+Shell "echo $?" +Enter +Wait +Show + +ClearTerminal + +Hide +TypeInPrompt+Shell "${AUTHCTL_PATH} user lock ${AUTHD_PAM_SSH_USER}" +Enter +Wait +Show + +ClearTerminal + +Hide +TypeInPrompt+Shell "${AUTHD_TEST_TAPE_COMMAND}" +Enter +Wait /permission denied: user .* is locked/ +Wait +Show + +ClearTerminal + +# lock again... + +Hide +TypeInPrompt+Shell "${AUTHCTL_PATH} user lock ${AUTHD_PAM_SSH_USER}" +Enter +Wait +Show + +ClearTerminal + +Hide +TypeInPrompt+Shell "${AUTHCTL_PATH} user unlock ${AUTHD_PAM_SSH_USER}" +Enter +Wait +Show + +Hide +TypeInPrompt+Shell "${AUTHD_TEST_TAPE_COMMAND}" +Enter +Wait+Prompt /Gimme your password/ +Show + +Hide +Type "goodpass" +Enter +${AUTHD_TEST_TAPE_COMMAND_AUTH_FINAL_WAIT} +Show From bac069950d29057b507e049280ec5e445a47e4be Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 8 Jul 2025 17:55:41 +0200 Subject: [PATCH 44/55] Don't leak to unauthenticated users whether a user account is locked Same as pam_unix, we now avoid leaking to unauthenticated users whether a user account is locked. That's achieved by only checking if the user is locked after they successfully authenticated to the broker, aborting login in that case. That has the side effect that locked users can still refresh the token and user info which the broker stores on disk. This commit also implements the same improvements to the "permission denied: user is locked" error message which were already implemented in the SelectBroker method. I'm not reverting the changes to the SelectBroker method because I think they are still improvements, even if they are not relevant for this specific error message anymore. --- internal/services/pam/pam.go | 29 ++++++++++++------- internal/services/pam/pam_test.go | 2 +- .../cache-with-locked-user.db | 2 +- .../IsAuthenticated | 2 +- .../IsAuthenticated | 2 +- .../IsAuthenticated | 2 +- .../IsAuthenticated | 2 +- .../IsAuthenticated | 2 +- .../IsAuthenticated | 2 +- .../IsAuthenticated | 2 +- .../Error_when_user_is_locked/IsAuthenticated | 4 +++ .../Error_when_user_is_locked/cache.db | 17 +++++++++++ .../Authenticate_user_locks_and_unlocks_it | 10 +++++++ ...e_user_locks_and_unlocks_it_on_shared_SSHd | 10 +++++++ .../tapes/ssh/simple_auth_locks_unlocks.tape | 6 ++++ pam/internal/adapter/authentication.go | 2 +- pam/internal/adapter/gdmmodel_test.go | 2 +- 17 files changed, 76 insertions(+), 22 deletions(-) rename internal/services/pam/testdata/{TestSelectBroker => TestIsAuthenticated}/cache-with-locked-user.db (78%) create mode 100644 internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_user_is_locked/IsAuthenticated create mode 100644 internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_user_is_locked/cache.db diff --git a/internal/services/pam/pam.go b/internal/services/pam/pam.go index 1f46537d3f..088c6b5dd6 100644 --- a/internal/services/pam/pam.go +++ b/internal/services/pam/pam.go @@ -142,15 +142,6 @@ func (s Service) SelectBroker(ctx context.Context, req *authd.SBRequest) (resp * lang = "C" } - userIsLocked, err := s.userManager.IsUserLocked(username) - if err != nil && !errors.Is(err, users.NoDataFoundError{}) { - return nil, fmt.Errorf("could not check if user %q is locked: %w", username, err) - } - // Throw an error if the user trying to authenticate already exists in the database and is locked - if err == nil && userIsLocked { - return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("user %s is locked", username)) - } - var mode string switch req.GetMode() { case authd.SessionMode_LOGIN: @@ -255,8 +246,6 @@ func (s Service) SelectAuthenticationMode(ctx context.Context, req *authd.SAMReq // IsAuthenticated returns broker answer to authentication request. func (s Service) IsAuthenticated(ctx context.Context, req *authd.IARequest) (resp *authd.IAResponse, err error) { - defer decorate.OnError(&err, "can't check authentication") - sessionID := req.GetSessionId() if sessionID == "" { log.Errorf(ctx, "IsAuthenticated: No session ID provided") @@ -296,6 +285,24 @@ func (s Service) IsAuthenticated(ctx context.Context, req *authd.IARequest) (res return nil, fmt.Errorf("user data from broker invalid: %v", err) } + // authd uses lowercase usernames + uInfo.Name = strings.ToLower(uInfo.Name) + + // Check if the user is locked. We can only do this after the broker has granted access, because we want to avoid + // leaking whether a user exists or not to unauthenticated users. + // TODO: We might want to let the broker know whether the user is locked or not, so that it can avoid storing any + // updated tokens or user info on disk. + userIsLocked, err := s.userManager.IsUserLocked(uInfo.Name) + if err != nil && !errors.Is(err, users.NoDataFoundError{}) { + log.Errorf(ctx, "IsAuthenticated: Could not check if user %q is locked: %v", uInfo.Name, err) + return nil, fmt.Errorf("could not check if user %q is locked: %w", uInfo.Name, err) + } + // Throw an error if the user trying to authenticate already exists in the database and is locked + if err == nil && userIsLocked { + log.Noticef(ctx, "Authentication failure: user %q is locked", uInfo.Name) + return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("user %s is locked", uInfo.Name)) + } + // Update database and local groups on granted auth. if err := s.userManager.UpdateUser(uInfo); err != nil { log.Errorf(ctx, "IsAuthenticated: Could not update user %q in database: %v", uInfo.Name, err) diff --git a/internal/services/pam/pam_test.go b/internal/services/pam/pam_test.go index 1495687204..5fdb1b163d 100644 --- a/internal/services/pam/pam_test.go +++ b/internal/services/pam/pam_test.go @@ -210,7 +210,6 @@ func TestSelectBroker(t *testing.T) { "Error_when_broker_does_not_exist": {username: "no broker", brokerID: "does not exist", wantErr: true}, "Error_when_broker_does_not_provide_a_session_ID": {username: "ns_no_id", wantErr: true}, "Error_when_starting_the_session": {username: "ns_error", wantErr: true}, - "Error_when_user_is_locked": {username: "locked", wantErr: true, existingDB: "cache-with-locked-user.db"}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { @@ -446,6 +445,7 @@ func TestIsAuthenticated(t *testing.T) { "Error_when_not_root": {username: "success", currentUserNotRoot: true}, "Error_when_sessionID_is_empty": {sessionID: "-"}, "Error_when_there_is_no_broker": {sessionID: "invalid-session"}, + "Error_when_user_is_locked": {username: "locked", existingDB: "cache-with-locked-user.db"}, // broker errors "Error_when_authenticating": {username: "ia_error"}, diff --git a/internal/services/pam/testdata/TestSelectBroker/cache-with-locked-user.db b/internal/services/pam/testdata/TestIsAuthenticated/cache-with-locked-user.db similarity index 78% rename from internal/services/pam/testdata/TestSelectBroker/cache-with-locked-user.db rename to internal/services/pam/testdata/TestIsAuthenticated/cache-with-locked-user.db index 05bf68dab2..cf3be9e829 100644 --- a/internal/services/pam/testdata/TestSelectBroker/cache-with-locked-user.db +++ b/internal/services/pam/testdata/TestIsAuthenticated/cache-with-locked-user.db @@ -1,5 +1,5 @@ users: - - name: testselectbroker/error_when_user_is_locked_separator_locked + - name: testisauthenticated/error_when_user_is_locked_separator_locked uid: 1111 gid: 11111 gecos: gecos for other user diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_on_empty_data_even_if_granted/IsAuthenticated b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_on_empty_data_even_if_granted/IsAuthenticated index 91c579daab..9077010ef2 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_on_empty_data_even_if_granted/IsAuthenticated +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_on_empty_data_even_if_granted/IsAuthenticated @@ -1,4 +1,4 @@ FIRST CALL: access: msg: - err: can't check authentication: missing key "userinfo" in returned message, got: {} + err: missing key "userinfo" in returned message, got: {} diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_on_updating_local_groups_with_unexisting_file/IsAuthenticated b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_on_updating_local_groups_with_unexisting_file/IsAuthenticated index 77877bd5de..a30ec99d72 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_on_updating_local_groups_with_unexisting_file/IsAuthenticated +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_on_updating_local_groups_with_unexisting_file/IsAuthenticated @@ -1,4 +1,4 @@ FIRST CALL: access: msg: - err: can't check authentication: failed to update user "testisauthenticated/error_on_updating_local_groups_with_unexisting_file_separator_success_with_local_groups": could not update local groups for user "testisauthenticated/error_on_updating_local_groups_with_unexisting_file_separator_success_with_local_groups": could not fetch existing local group: open : no such file or directory + err: failed to update user "testisauthenticated/error_on_updating_local_groups_with_unexisting_file_separator_success_with_local_groups": could not update local groups for user "testisauthenticated/error_on_updating_local_groups_with_unexisting_file_separator_success_with_local_groups": could not fetch existing local group: open : no such file or directory diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_access/IsAuthenticated b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_access/IsAuthenticated index 49f1b2961e..92f4beafcd 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_access/IsAuthenticated +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_access/IsAuthenticated @@ -1,4 +1,4 @@ FIRST CALL: access: msg: - err: can't check authentication: invalid access authentication key: invalid + err: invalid access authentication key: invalid diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_data/IsAuthenticated b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_data/IsAuthenticated index 2a70683dd0..bd72dfbe47 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_data/IsAuthenticated +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_data/IsAuthenticated @@ -1,5 +1,5 @@ FIRST CALL: access: msg: - err: can't check authentication: response returned by the broker is not a valid json: invalid character 'i' looking for beginning of value + err: response returned by the broker is not a valid json: invalid character 'i' looking for beginning of value Broker returned: invalid diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_userinfo/IsAuthenticated b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_userinfo/IsAuthenticated index b917b0921c..b071345de3 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_userinfo/IsAuthenticated +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_userinfo/IsAuthenticated @@ -1,4 +1,4 @@ FIRST CALL: access: msg: - err: can't check authentication: message is not JSON formatted: json: cannot unmarshal string into Go value of type types.UserInfo + err: message is not JSON formatted: json: cannot unmarshal string into Go value of type types.UserInfo diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_sessionID_is_empty/IsAuthenticated b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_sessionID_is_empty/IsAuthenticated index 6ec3830d89..96ea6d8d5e 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_sessionID_is_empty/IsAuthenticated +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_sessionID_is_empty/IsAuthenticated @@ -1,4 +1,4 @@ FIRST CALL: access: msg: - err: error InvalidArgument from server: can't check authentication: rpc error: code = InvalidArgument desc = no session ID provided + err: error InvalidArgument from server: no session ID provided diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_there_is_no_broker/IsAuthenticated b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_there_is_no_broker/IsAuthenticated index 5137dc5e62..10d7fab744 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_there_is_no_broker/IsAuthenticated +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_there_is_no_broker/IsAuthenticated @@ -1,4 +1,4 @@ FIRST CALL: access: msg: - err: can't check authentication: no broker found for session "invalid-session" + err: no broker found for session "invalid-session" diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_user_is_locked/IsAuthenticated b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_user_is_locked/IsAuthenticated new file mode 100644 index 0000000000..15055a4978 --- /dev/null +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_user_is_locked/IsAuthenticated @@ -0,0 +1,4 @@ +FIRST CALL: + access: + msg: + err: permission denied: user testisauthenticated/error_when_user_is_locked_separator_locked is locked diff --git a/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_user_is_locked/cache.db b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_user_is_locked/cache.db new file mode 100644 index 0000000000..f0770123eb --- /dev/null +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_user_is_locked/cache.db @@ -0,0 +1,17 @@ +users: + - name: testisauthenticated/error_when_user_is_locked_separator_locked + uid: 1111 + gid: 11111 + gecos: gecos for other user + dir: /home/locked + shell: /bin/bash + broker_id: broker-id + locked: true +groups: + - name: group1 + gid: 11111 + ugid: ugid +users_to_groups: + - uid: 1111 + gid: 11111 +schema_version: 2 diff --git a/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_locks_and_unlocks_it b/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_locks_and_unlocks_it index cd21f3e3a7..87139b9940 100644 --- a/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_locks_and_unlocks_it +++ b/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_locks_and_unlocks_it @@ -82,6 +82,16 @@ Use "authctl [command] --help" for more information about a command. > ──────────────────────────────────────────────────────────────────────────────── > ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Password authentication == +Enter 'r' to cancel the request and go back to select the authentication method +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it@localhost) Gimme your password: +> +──────────────────────────────────────────────────────────────────────────────── +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Password authentication == +Enter 'r' to cancel the request and go back to select the authentication method +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it@localhost) Gimme your password: +> permission denied: user user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it is locked Received disconnect from ${SSH_HOST} port ${SSH_PORT} Too many authentication failures Disconnected from ${SSH_HOST} port ${SSH_PORT} diff --git a/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_locks_and_unlocks_it_on_shared_SSHd b/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_locks_and_unlocks_it_on_shared_SSHd index 1bbbe5d521..c1927d02ea 100644 --- a/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_locks_and_unlocks_it_on_shared_SSHd +++ b/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_locks_and_unlocks_it_on_shared_SSHd @@ -82,6 +82,16 @@ Use "authctl [command] --help" for more information about a command. > ──────────────────────────────────────────────────────────────────────────────── > ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Password authentication == +Enter 'r' to cancel the request and go back to select the authentication method +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd@localhost) Gimme your password: +> +──────────────────────────────────────────────────────────────────────────────── +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Password authentication == +Enter 'r' to cancel the request and go back to select the authentication method +(user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd@localhost) Gimme your password: +> permission denied: user user-integration-pre-check-ssh-authenticate-user-locks-and-unlocks-it-on-shared-sshd is locked Received disconnect from ${SSH_HOST} port ${SSH_PORT} Too many authentication failures Disconnected from ${SSH_HOST} port ${SSH_PORT} diff --git a/pam/integration-tests/testdata/tapes/ssh/simple_auth_locks_unlocks.tape b/pam/integration-tests/testdata/tapes/ssh/simple_auth_locks_unlocks.tape index 0d56bd202d..c736c096f2 100644 --- a/pam/integration-tests/testdata/tapes/ssh/simple_auth_locks_unlocks.tape +++ b/pam/integration-tests/testdata/tapes/ssh/simple_auth_locks_unlocks.tape @@ -48,6 +48,12 @@ ClearTerminal Hide TypeInPrompt+Shell "${AUTHD_TEST_TAPE_COMMAND}" Enter +Wait+Prompt /Gimme your password/ +Show + +Hide +Type "goodpass" +Enter Wait /permission denied: user .* is locked/ Wait Show diff --git a/pam/internal/adapter/authentication.go b/pam/internal/adapter/authentication.go index ef1bf2baa6..8d50e1120e 100644 --- a/pam/internal/adapter/authentication.go +++ b/pam/internal/adapter/authentication.go @@ -68,7 +68,7 @@ func sendIsAuthenticated(ctx context.Context, client authd.PAMClient, sessionID } return pamError{ status: pam.ErrSystem, - msg: fmt.Sprintf("authentication status failure: %v", err), + msg: err.Error(), } } diff --git a/pam/internal/adapter/gdmmodel_test.go b/pam/internal/adapter/gdmmodel_test.go index 1cc32f3a6b..c4dfb962d6 100644 --- a/pam/internal/adapter/gdmmodel_test.go +++ b/pam/internal/adapter/gdmmodel_test.go @@ -2027,7 +2027,7 @@ func TestGdmModel(t *testing.T) { wantStage: pam_proto.Stage_challenge, wantExitStatus: pamError{ status: pam.ErrSystem, - msg: "authentication status failure: some authentication error", + msg: "some authentication error", }, }, "Error_on_authentication_client_invalid_message": { From ee4e7d40602f774b8db951e632ecac4e475756d0 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 8 Jul 2025 18:21:53 +0200 Subject: [PATCH 45/55] refactor: Extract GoBuildFlags() --- internal/testutils/args.go | 20 +++++++++++++++++++ internal/testutils/daemon.go | 11 +---------- pam/integration-tests/exec_test.go | 16 +++------------ pam/integration-tests/helpers_test.go | 28 ++++----------------------- 4 files changed, 28 insertions(+), 47 deletions(-) diff --git a/internal/testutils/args.go b/internal/testutils/args.go index 659e8a9dd8..af74211dfe 100644 --- a/internal/testutils/args.go +++ b/internal/testutils/args.go @@ -67,6 +67,26 @@ func IsRace() bool { return isRace } +// GoBuildFlags returns the Go build flags that should be used when building binaries in tests. +// It includes flags for coverage, address sanitizer, and race detection if they are enabled +// in the current test environment. +// +// Note: The flags returned by this function must be the first arguments to the `go build` command, +// because -cover is a "positional flag". +func GoBuildFlags() []string { + var flags []string + if CoverDirForTests() != "" { + flags = append(flags, "-cover") + } + if IsAsan() { + flags = append(flags, "-asan") + } + if IsRace() { + flags = append(flags, "-race") + } + return flags +} + // SleepMultiplier returns the sleep multiplier to be used in tests. func SleepMultiplier() float64 { sleepMultiplierOnce.Do(func() { diff --git a/internal/testutils/daemon.go b/internal/testutils/daemon.go index 22bdd23b58..f58031b5cf 100644 --- a/internal/testutils/daemon.go +++ b/internal/testutils/daemon.go @@ -279,16 +279,7 @@ func BuildDaemonWithExampleBroker() (execPath string, cleanup func(), err error) execPath = filepath.Join(tempDir, "authd") cmd := exec.Command("go", "build") cmd.Dir = projectRoot - if CoverDirForTests() != "" { - // -cover is a "positional flag", so it needs to come right after the "build" command. - cmd.Args = append(cmd.Args, "-cover") - } - if IsAsan() { - cmd.Args = append(cmd.Args, "-asan") - } - if IsRace() { - cmd.Args = append(cmd.Args, "-race") - } + cmd.Args = append(cmd.Args, GoBuildFlags()...) cmd.Args = append(cmd.Args, "-gcflags=all=-N -l") cmd.Args = append(cmd.Args, "-tags=withexamplebroker,integrationtests") cmd.Args = append(cmd.Args, "-o", execPath, "./cmd/authd") diff --git a/pam/integration-tests/exec_test.go b/pam/integration-tests/exec_test.go index 4e9ec28a1f..d9ed145a6c 100644 --- a/pam/integration-tests/exec_test.go +++ b/pam/integration-tests/exec_test.go @@ -973,19 +973,9 @@ func buildExecModuleWithCFlags(t *testing.T, cFlags []string, forPreload bool) s func buildExecClient(t *testing.T) string { t.Helper() - cmd := exec.Command("go", "build", "-C", "cmd/exec-client") - cmd.Dir = filepath.Join(testutils.CurrentDir()) - if testutils.CoverDirForTests() != "" { - // -cover is a "positional flag", so it needs to come right after the "build" command. - cmd.Args = append(cmd.Args, "-cover") - } - if testutils.IsAsan() { - // -asan is a "positional flag", so it needs to come right after the "build" command. - cmd.Args = append(cmd.Args, "-asan") - } - if testutils.IsRace() { - cmd.Args = append(cmd.Args, "-race") - } + cmd := exec.Command("go", "build") + cmd.Dir = filepath.Join(testutils.CurrentDir(), "cmd/exec-client") + cmd.Args = append(cmd.Args, testutils.GoBuildFlags()...) cmd.Args = append(cmd.Args, "-gcflags=all=-N -l") cmd.Args = append(cmd.Args, "-tags=pam_tests_exec_client") cmd.Env = append(os.Environ(), `CGO_CFLAGS=-O0 -g3`) diff --git a/pam/integration-tests/helpers_test.go b/pam/integration-tests/helpers_test.go index 55f460bec3..93804056fa 100644 --- a/pam/integration-tests/helpers_test.go +++ b/pam/integration-tests/helpers_test.go @@ -176,17 +176,7 @@ func preparePamRunnerTest(t *testing.T, clientPath string) []string { func buildPAMRunner(execPath string) (cleanup func(), err error) { cmd := exec.Command("go", "build") cmd.Dir = testutils.ProjectRoot() - if testutils.CoverDirForTests() != "" { - // -cover is a "positional flag", so it needs to come right after the "build" command. - cmd.Args = append(cmd.Args, "-cover") - } - if testutils.IsAsan() { - // -asan is a "positional flag", so it needs to come right after the "build" command. - cmd.Args = append(cmd.Args, "-asan") - } - if testutils.IsRace() { - cmd.Args = append(cmd.Args, "-race") - } + cmd.Args = append(cmd.Args, testutils.GoBuildFlags()...) cmd.Args = append(cmd.Args, "-gcflags=all=-N -l") cmd.Args = append(cmd.Args, "-tags=withpamrunner", "-o", filepath.Join(execPath, "pam_authd"), "./pam/tools/pam-runner") @@ -200,19 +190,9 @@ func buildPAMRunner(execPath string) (cleanup func(), err error) { func buildPAMExecChild(t *testing.T) string { t.Helper() - cmd := exec.Command("go", "build", "-C", "pam") - cmd.Dir = testutils.ProjectRoot() - if testutils.CoverDirForTests() != "" { - // -cover is a "positional flag", so it needs to come right after the "build" command. - cmd.Args = append(cmd.Args, "-cover") - } - if testutils.IsAsan() { - // -asan is a "positional flag", so it needs to come right after the "build" command. - cmd.Args = append(cmd.Args, "-asan") - } - if testutils.IsRace() { - cmd.Args = append(cmd.Args, "-race") - } + cmd := exec.Command("go", "build") + cmd.Dir = filepath.Join(testutils.ProjectRoot(), "pam") + cmd.Args = append(cmd.Args, testutils.GoBuildFlags()...) cmd.Args = append(cmd.Args, "-gcflags=all=-N -l") cmd.Args = append(cmd.Args, "-tags=pam_debug") cmd.Env = append(os.Environ(), `CGO_CFLAGS=-O0 -g3`) From a60b8908eca8d73b8a7f5894f95df50e0df64d48 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 8 Jul 2025 18:22:45 +0200 Subject: [PATCH 46/55] Pass GoBuildFlags to authctl built in tests --- internal/testutils/authctl.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/testutils/authctl.go b/internal/testutils/authctl.go index 4b7aa7c8a8..8ede48db35 100644 --- a/internal/testutils/authctl.go +++ b/internal/testutils/authctl.go @@ -16,7 +16,9 @@ func BuildAuthctl() (binaryPath string, cleanup func(), err error) { cleanup = func() { os.RemoveAll(tempDir) } binaryPath = filepath.Join(tempDir, "authctl") - cmd := exec.Command("go", "build", "-o", binaryPath, "./cmd/authctl/main.go") + cmd := exec.Command("go", "build") + cmd.Args = append(cmd.Args, GoBuildFlags()...) + cmd.Args = append(cmd.Args, "-o", binaryPath, "./cmd/authctl") cmd.Dir = ProjectRoot() fmt.Fprintln(os.Stderr, "Running command:", cmd.String()) From 588e24d5dcc648fdcf6e8cfee99467788a514763 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Fri, 11 Jul 2025 16:34:13 +0200 Subject: [PATCH 47/55] refactor: Use WithCurrentUserAsRoot option for testutils.runAuthd() --- pam/integration-tests/cli_test.go | 13 +++++++---- pam/integration-tests/helpers_test.go | 31 ++++++++++++++------------- pam/integration-tests/native_test.go | 13 +++++++---- pam/integration-tests/ssh_test.go | 3 ++- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/pam/integration-tests/cli_test.go b/pam/integration-tests/cli_test.go index 7f5c10f321..8188847eef 100644 --- a/pam/integration-tests/cli_test.go +++ b/pam/integration-tests/cli_test.go @@ -260,11 +260,17 @@ func TestCLIAuthenticate(t *testing.T) { pidFile = filepath.Join(outDir, "authd.pid") - socketPath = runAuthd(t, !tc.currentUserNotRoot, + args := []testutils.DaemonOption{ testutils.WithGroupFile(groupFile), testutils.WithGroupFileOutput(groupFileOutput), testutils.WithPidFile(pidFile), - testutils.WithEnvironment(useOldDatabaseEnv(t, tc.oldDB)...)) + testutils.WithEnvironment(useOldDatabaseEnv(t, tc.oldDB)...), + } + if !tc.currentUserNotRoot { + args = append(args, testutils.WithCurrentUserAsRoot) + } + + socketPath = runAuthd(t, args...) } else { socketPath, groupFileOutput = sharedAuthd(t) } @@ -379,8 +385,7 @@ func TestCLIChangeAuthTok(t *testing.T) { if tc.currentUserNotRoot { // For the not-root tests authd has to run in a more restricted way. // In the other cases this is not needed, so we can just use a shared authd. - socketPath = runAuthd(t, false, - testutils.WithGroupFile(filepath.Join(t.TempDir(), "group"))) + socketPath = runAuthd(t, testutils.WithGroupFile(filepath.Join(t.TempDir(), "group"))) } else { socketPath, _ = sharedAuthd(t) } diff --git a/pam/integration-tests/helpers_test.go b/pam/integration-tests/helpers_test.go index 93804056fa..11758a0992 100644 --- a/pam/integration-tests/helpers_test.go +++ b/pam/integration-tests/helpers_test.go @@ -50,15 +50,15 @@ var ( sharedAuthdInstance = authdInstance{} ) -func runAuthdForTesting(t *testing.T, currentUserAsRoot bool, isSharedDaemon bool, args ...testutils.DaemonOption) (socketPath string) { +func runAuthdForTesting(t *testing.T, isSharedDaemon bool, args ...testutils.DaemonOption) (socketPath string) { t.Helper() - socketPath, cancelFunc := runAuthdForTestingWithCancel(t, currentUserAsRoot, isSharedDaemon, args...) + socketPath, cancelFunc := runAuthdForTestingWithCancel(t, isSharedDaemon, args...) t.Cleanup(cancelFunc) return socketPath } -func runAuthdForTestingWithCancel(t *testing.T, currentUserAsRoot bool, isSharedDaemon bool, args ...testutils.DaemonOption) (socketPath string, cancelFunc func()) { +func runAuthdForTestingWithCancel(t *testing.T, isSharedDaemon bool, args ...testutils.DaemonOption) (socketPath string, cancelFunc func()) { t.Helper() outputFile := filepath.Join(t.TempDir(), "authd.log") @@ -69,10 +69,6 @@ func runAuthdForTestingWithCancel(t *testing.T, currentUserAsRoot bool, isShared require.NoError(t, err, "Setup: Creating home base dir %q", homeBaseDir) args = append(args, testutils.WithHomeBaseDir(homeBaseDir)) - if currentUserAsRoot { - args = append(args, testutils.WithCurrentUserAsRoot) - } - if !isSharedDaemon { database := filepath.Join(t.TempDir(), "db", consts.DefaultDatabaseFileName) args = append(args, testutils.WithDBPath(filepath.Dir(database))) @@ -88,10 +84,10 @@ func runAuthdForTestingWithCancel(t *testing.T, currentUserAsRoot bool, isShared return socketPath, cancelFunc } -func runAuthd(t *testing.T, currentUserAsRoot bool, args ...testutils.DaemonOption) string { +func runAuthd(t *testing.T, args ...testutils.DaemonOption) string { t.Helper() - return runAuthdForTesting(t, currentUserAsRoot, false, args...) + return runAuthdForTesting(t, false, args...) } func sharedAuthd(t *testing.T, args ...testutils.DaemonOption) (socketPath string, groupFile string) { @@ -107,8 +103,10 @@ func sharedAuthd(t *testing.T, args ...testutils.DaemonOption) (socketPath strin groups := filepath.Join(testutils.TestFamilyPath(t), "groups") args = append(args, testutils.WithGroupFile(groups), - testutils.WithGroupFileOutput(groupOutput)) - socket := runAuthdForTesting(t, true, useSharedInstance, args...) + testutils.WithGroupFileOutput(groupOutput), + testutils.WithCurrentUserAsRoot, + ) + socket := runAuthdForTesting(t, useSharedInstance, args...) return socket, groupOutput } @@ -144,12 +142,15 @@ func sharedAuthd(t *testing.T, args ...testutils.DaemonOption) (socketPath strin return sa.socketPath, sa.groupsOutputPath } - args = append(slices.Clone(args), testutils.WithSharedDaemon(true)) sa.groupsFile = filepath.Join(testutils.TestFamilyPath(t), "groups") - args = append(args, testutils.WithGroupFile(sa.groupsFile)) sa.groupsOutputPath = filepath.Join(t.TempDir(), "groups") - args = append(args, testutils.WithGroupFileOutput(sa.groupsOutputPath)) - sa.socketPath, sa.cleanup = runAuthdForTestingWithCancel(t, true, useSharedInstance, args...) + args = append(slices.Clone(args), + testutils.WithSharedDaemon(true), + testutils.WithCurrentUserAsRoot, + testutils.WithGroupFile(sa.groupsFile), + testutils.WithGroupFileOutput(sa.groupsOutputPath), + ) + sa.socketPath, sa.cleanup = runAuthdForTestingWithCancel(t, useSharedInstance, args...) return sa.socketPath, sa.groupsOutputPath } diff --git a/pam/integration-tests/native_test.go b/pam/integration-tests/native_test.go index a0b42953a0..95bdcdf952 100644 --- a/pam/integration-tests/native_test.go +++ b/pam/integration-tests/native_test.go @@ -419,11 +419,17 @@ func TestNativeAuthenticate(t *testing.T) { pidFile = filepath.Join(outDir, "authd.pid") - socketPath = runAuthd(t, !tc.currentUserNotRoot, + args := []testutils.DaemonOption{ testutils.WithGroupFile(groupFile), testutils.WithGroupFileOutput(groupFileOutput), testutils.WithPidFile(pidFile), - testutils.WithEnvironment(useOldDatabaseEnv(t, tc.oldDB)...)) + testutils.WithEnvironment(useOldDatabaseEnv(t, tc.oldDB)...), + } + if !tc.currentUserNotRoot { + args = append(args, testutils.WithCurrentUserAsRoot) + } + + socketPath = runAuthd(t, args...) } else { socketPath, groupFileOutput = sharedAuthd(t) } @@ -569,8 +575,7 @@ func TestNativeChangeAuthTok(t *testing.T) { if tc.currentUserNotRoot { // For the not-root tests authd has to run in a more restricted way. // In the other cases this is not needed, so we can just use a shared authd. - socketPath = runAuthd(t, false, - testutils.WithGroupFile(filepath.Join(t.TempDir(), "group"))) + socketPath = runAuthd(t, testutils.WithGroupFile(filepath.Join(t.TempDir(), "group"))) } else { socketPath, _ = sharedAuthd(t) } diff --git a/pam/integration-tests/ssh_test.go b/pam/integration-tests/ssh_test.go index d9db698ac1..f00009b97a 100644 --- a/pam/integration-tests/ssh_test.go +++ b/pam/integration-tests/ssh_test.go @@ -391,7 +391,8 @@ Wait@%dms`, sshDefaultFinalWaitTimeout), authdEnv = append(authdEnv, useOldDatabaseEnv(t, tc.oldDB)...) - socketPath = runAuthd(t, true, + socketPath = runAuthd(t, + testutils.WithCurrentUserAsRoot, testutils.WithGroupFile(groupOutput), testutils.WithEnvironment(authdEnv...)) } else if !sharedSSHd { From dd8d66809c5fc5edee334c6b7f2c2c8391ae40a0 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Fri, 22 Aug 2025 12:05:39 +0200 Subject: [PATCH 48/55] Use lowercase username in UpdateLockedFieldForUser --- internal/users/db/db_test.go | 4 ++++ internal/users/db/update.go | 3 +++ 2 files changed, 7 insertions(+) diff --git a/internal/users/db/db_test.go b/internal/users/db/db_test.go index a3cde0bbe2..b26ff85e11 100644 --- a/internal/users/db/db_test.go +++ b/internal/users/db/db_test.go @@ -869,6 +869,10 @@ func TestUpdateLockedFieldForUser(t *testing.T) { err := c.UpdateLockedFieldForUser("user1", true) require.NoError(t, err, "UpdateLockedFieldForUser for an existent user should not return an error") + // Update broker for existent user with different capitalization + err = c.UpdateLockedFieldForUser("USER1", true) + require.NoError(t, err, "UpdateLockedFieldForUser for an existent user with different capitalization should not return an error") + // Error when updating broker for nonexistent user err = c.UpdateLockedFieldForUser("nonexistent", false) require.Error(t, err, "UpdateLockedFieldForUser for a nonexistent user should return an error") diff --git a/internal/users/db/update.go b/internal/users/db/update.go index 9029d2df09..c2bd5a7fe8 100644 --- a/internal/users/db/update.go +++ b/internal/users/db/update.go @@ -185,6 +185,9 @@ func (m *Manager) UpdateBrokerForUser(username, brokerID string) error { // UpdateLockedFieldForUser sets the "locked" field of a user record. func (m *Manager) UpdateLockedFieldForUser(username string, locked bool) error { + // authd uses lowercase usernames + username = strings.ToLower(username) + query := `UPDATE users SET locked = ? WHERE name = ?` res, err := m.db.Exec(query, locked, username) if err != nil { From dc7365c04999762945fea6a32f38e357b385d6a9 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Fri, 22 Aug 2025 12:20:43 +0200 Subject: [PATCH 49/55] Prefix socket URI with "unix://" if no scheme is set. --- cmd/authctl/user/user.go | 7 +++++++ cmd/authctl/user/user_test.go | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cmd/authctl/user/user.go b/cmd/authctl/user/user.go index d81366a0d6..6a307199e2 100644 --- a/cmd/authctl/user/user.go +++ b/cmd/authctl/user/user.go @@ -4,6 +4,7 @@ package user import ( "fmt" "os" + "regexp" "github.com/spf13/cobra" "github.com/ubuntu/authd/internal/consts" @@ -27,6 +28,12 @@ func NewUserServiceClient() (authd.UserServiceClient, error) { 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) diff --git a/cmd/authctl/user/user_test.go b/cmd/authctl/user/user_test.go index 412511730f..9fc181515c 100644 --- a/cmd/authctl/user/user_test.go +++ b/cmd/authctl/user/user_test.go @@ -66,7 +66,7 @@ func TestUserLockCommand(t *testing.T) { testutils.WithCurrentUserAsRoot, ) - err := os.Setenv("AUTHD_SOCKET", "unix://"+daemonSocket) + err := os.Setenv("AUTHD_SOCKET", daemonSocket) require.NoError(t, err, "Failed to set AUTHD_SOCKET environment variable") tests := map[string]struct { From fe8657ea4b6a4ed6047f07c6050e5edc048e0ab5 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Fri, 22 Aug 2025 12:38:56 +0200 Subject: [PATCH 50/55] Ensure that we exit with a status code smaller than 256. --- cmd/authctl/main.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd/authctl/main.go b/cmd/authctl/main.go index 67859d4aa4..5b8c41e051 100644 --- a/cmd/authctl/main.go +++ b/cmd/authctl/main.go @@ -46,14 +46,20 @@ func main() { os.Exit(1) } - // If the error is a gRPC status, we print the message and exit with the appropriate code. + // If the error is a gRPC status, we print the message and exit with the gRPC status code. switch s.Code() { case codes.PermissionDenied: fmt.Fprintln(os.Stderr, "Permission denied:", s.Message()) default: fmt.Fprintln(os.Stderr, "Error:", s.Message()) } + code := int(s.Code()) + if code < 0 || code > 255 { + // We cannot exit with a negative code or a code greater than 255, + // so we map it to 1 in that case. + code = 1 + } - os.Exit(int(s.Code())) + os.Exit(code) } } From b13a67b3d547ed2b8c34a0e413600d2439ef5a39 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 26 Aug 2025 19:26:59 +0200 Subject: [PATCH 51/55] pam/integration-tests: Support loading SQL dump --- cmd/authctl/user/user_test.go | 2 +- internal/testutils/daemon.go | 28 ++++++++++++++++++----- nss/integration-tests/integration_test.go | 4 ++-- pam/integration-tests/ssh_test.go | 17 ++++++++------ 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/cmd/authctl/user/user_test.go b/cmd/authctl/user/user_test.go index 9fc181515c..fd99284e5d 100644 --- a/cmd/authctl/user/user_test.go +++ b/cmd/authctl/user/user_test.go @@ -62,7 +62,7 @@ func TestUserLockCommand(t *testing.T) { daemonSocket := testutils.StartDaemon(t, daemonPath, testutils.WithGroupFile(filepath.Join("testdata", "empty.group")), - testutils.WithPreviousDBState("one_user_and_group"), + testutils.WithDBFromYAML("one_user_and_group"), testutils.WithCurrentUserAsRoot, ) diff --git a/internal/testutils/daemon.go b/internal/testutils/daemon.go index f58031b5cf..d1a2c06824 100644 --- a/internal/testutils/daemon.go +++ b/internal/testutils/daemon.go @@ -25,7 +25,8 @@ import ( type daemonOptions struct { dbPath string - existentDB string + dbFromYAML string + dbFromDump string socketPath string pidFile string outputFile string @@ -43,10 +44,17 @@ func WithDBPath(path string) DaemonOption { } } -// WithPreviousDBState initializes the database of the daemon with a preexistent database. -func WithPreviousDBState(db string) DaemonOption { +// WithDBFromYAML initializes the database of the daemon with a preexistent database. +func WithDBFromYAML(db string) DaemonOption { return func(o *daemonOptions) { - o.existentDB = db + o.dbFromYAML = db + } +} + +// WithDBFromDump initializes the database of the daemon with a preexistent database dump. +func WithDBFromDump(db string) DaemonOption { + return func(o *daemonOptions) { + o.dbFromDump = db } } @@ -146,12 +154,20 @@ func StartDaemonWithCancel(t *testing.T, execPath string, args ...DaemonOption) opts.dbPath = filepath.Join(tempDir, "db") } - if opts.existentDB != "" { + require.False(t, opts.dbFromYAML != "" && opts.dbFromDump != "", "Setup: cannot use both dbFromYAML and dbFromDump at the same time") + + if opts.dbFromYAML != "" { require.NoError(t, os.MkdirAll(opts.dbPath, 0700), "Setup: failed to create database dir") - err := db.Z_ForTests_CreateDBFromYAML(filepath.Join("testdata", "db", opts.existentDB+".db.yaml"), opts.dbPath) + err := db.Z_ForTests_CreateDBFromYAML(filepath.Join("testdata", "db", opts.dbFromYAML+".db.yaml"), opts.dbPath) require.NoError(t, err, "Setup: could not create database from testdata") } + if opts.dbFromDump != "" { + require.NoError(t, os.MkdirAll(opts.dbPath, 0700), "Setup: failed to create database dir") + err := db.Z_ForTests_CreateDBFromDump(filepath.Join("testdata", "db", opts.dbFromDump+".sql"), opts.dbPath) + require.NoError(t, err, "Setup: could not create database from dump") + } + if opts.socketPath == "" { opts.socketPath = filepath.Join(tempDir, "authd.socket") } diff --git a/nss/integration-tests/integration_test.go b/nss/integration-tests/integration_test.go index d5a87bfa12..bac53ee89b 100644 --- a/nss/integration-tests/integration_test.go +++ b/nss/integration-tests/integration_test.go @@ -30,7 +30,7 @@ func TestIntegration(t *testing.T) { testutils.StartDaemon(t, daemonPath, testutils.WithSocketPath(defaultSocket), - testutils.WithPreviousDBState(defaultDbState), + testutils.WithDBFromYAML(defaultDbState), testutils.WithGroupFile(defaultGroupsFilePath), testutils.WithCurrentUserAsRoot, ) @@ -107,7 +107,7 @@ func TestIntegration(t *testing.T) { if useAlternativeDaemon { // Run a specific new daemon for special test cases. socketPath = testutils.StartDaemon(t, daemonPath, - testutils.WithPreviousDBState(tc.dbState), + testutils.WithDBFromYAML(tc.dbState), testutils.WithGroupFile(defaultGroupsFilePath), ) } diff --git a/pam/integration-tests/ssh_test.go b/pam/integration-tests/ssh_test.go index f00009b97a..7e2cec1a18 100644 --- a/pam/integration-tests/ssh_test.go +++ b/pam/integration-tests/ssh_test.go @@ -150,6 +150,7 @@ func testSSHAuthenticate(t *testing.T, sharedSSHd bool) { socketPath string daemonizeSSHd bool interactiveShell bool + oldBBoltDB string oldDB string wantUserAlreadyExist bool @@ -178,19 +179,19 @@ func testSSHAuthenticate(t *testing.T, sharedSSHd bool) { }, "Authenticate_user_successfully_after_db_migration": { tape: "simple_auth_with_auto_selected_broker", - oldDB: "authd_0.4.1_bbolt_with_mixed_case_users", + oldBBoltDB: "authd_0.4.1_bbolt_with_mixed_case_users", wantUserAlreadyExist: true, user: "user-integration-cached", }, "Authenticate_user_with_upper_case_using_lower_case_after_db_migration": { tape: "simple_auth_with_auto_selected_broker", - oldDB: "authd_0.4.1_bbolt_with_mixed_case_users", + oldBBoltDB: "authd_0.4.1_bbolt_with_mixed_case_users", wantUserAlreadyExist: true, user: "user-integration-upper-case", }, "Authenticate_user_with_mixed_case_after_db_migration": { tape: "simple_auth_with_auto_selected_broker", - oldDB: "authd_0.4.1_bbolt_with_mixed_case_users", + oldBBoltDB: "authd_0.4.1_bbolt_with_mixed_case_users", wantUserAlreadyExist: true, user: "user-integration-WITH-Mixed-CaSe", }, @@ -383,18 +384,20 @@ Wait@%dms`, sshDefaultFinalWaitTimeout), authdEnv = append(authdEnv, nssTestEnv(t, nssLibrary, authdSocketLink)...) } - if tc.wantLocalGroups || tc.oldDB != "" { + if tc.wantLocalGroups || tc.oldDB != "" || tc.oldBBoltDB != "" { // For the local groups tests we need to run authd again so that it has // special environment that saves the updated group file to a writable // location for us to test. _, groupOutput = prepareGroupFiles(t) - authdEnv = append(authdEnv, useOldDatabaseEnv(t, tc.oldDB)...) + authdEnv = append(authdEnv, useOldDatabaseEnv(t, tc.oldBBoltDB)...) socketPath = runAuthd(t, testutils.WithCurrentUserAsRoot, testutils.WithGroupFile(groupOutput), - testutils.WithEnvironment(authdEnv...)) + testutils.WithEnvironment(authdEnv...), + testutils.WithDBFromDump(tc.oldDB), + ) } else if !sharedSSHd { socketPath, groupOutput = sharedAuthd(t, testutils.WithGroupFileOutput(defaultGroupOutput), @@ -437,7 +440,7 @@ Wait@%dms`, sshDefaultFinalWaitTimeout), sshdPort := defaultSSHDPort userHome := defaultUserHome - if !sharedSSHd || tc.wantLocalGroups || tc.oldDB != "" || + if !sharedSSHd || tc.wantLocalGroups || tc.oldBBoltDB != "" || tc.interactiveShell || tc.socketPath != "" { sshdEnv := sshdEnv if nssLibrary != "" { From 7388eab256e1cca6861972dad7a79b67ac7926e6 Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 2 Sep 2025 22:01:46 +0200 Subject: [PATCH 52/55] tests: Improve log message --- pam/integration-tests/sshd_preloader/sshd_preloader.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pam/integration-tests/sshd_preloader/sshd_preloader.c b/pam/integration-tests/sshd_preloader/sshd_preloader.c index 37dcb69e4b..f9e36d7ac9 100644 --- a/pam/integration-tests/sshd_preloader/sshd_preloader.c +++ b/pam/integration-tests/sshd_preloader/sshd_preloader.c @@ -155,7 +155,7 @@ getpwnam (const char *name) */ if (passwd_entity->pw_uid != passwd_entity->pw_gid) { - fprintf (stderr, "sshd_preloader[%d]: User %s has different UID and GID (%d:%d)\n", + fprintf (stderr, "sshd_preloader[%d]: User %s has different UID and GID (%d:%d), aborting!\n", getpid (), name, passwd_entity->pw_uid, passwd_entity->pw_gid); abort(); } From 0bb732e49b8f0b4b620787f368c6602f834780eb Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Wed, 3 Sep 2025 01:10:11 +0200 Subject: [PATCH 53/55] tests: Don't require group backup file to exist I'm adding a test case which doesn't modify the local groups file, so the backup file doesn't exist. --- internal/users/localentries/testutils/localgroups.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/users/localentries/testutils/localgroups.go b/internal/users/localentries/testutils/localgroups.go index 2a5a102490..464bff7ac7 100644 --- a/internal/users/localentries/testutils/localgroups.go +++ b/internal/users/localentries/testutils/localgroups.go @@ -2,6 +2,7 @@ package localgrouptestutils import ( + "errors" "os" "path/filepath" "testing" @@ -65,7 +66,9 @@ func RequireGroupFile(t *testing.T, destGroupFile, goldenGroupPath string) { golden.WithSuffix(groupSuffix)) gotGroupsBackup, err := os.ReadFile(destGroupBackupFile) - require.NoError(t, err, "Teardown: could not read dest group backup file") + if !errors.Is(err, os.ErrNotExist) { + require.NoError(t, err, "Teardown: could not read dest group backup file") + } golden.CheckOrUpdate(t, string(gotGroupsBackup), golden.WithPath(goldenGroupPath), golden.WithSuffix(backupSuffix)) } From 7201b6947af11964709e408a2736d8c612bd024e Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Tue, 26 Aug 2025 19:27:46 +0200 Subject: [PATCH 54/55] tests: Add Authenticate_user_after_db_migration --- pam/integration-tests/ssh_test.go | 6 +++ .../testdata/db/one_user_and_group.sql | 40 +++++++++++++++ .../Authenticate_user_after_db_migration | 51 +++++++++++++++++++ ...Authenticate_user_after_db_migration.group | 1 + ...icate_user_after_db_migration.group.backup | 0 5 files changed, 98 insertions(+) create mode 100644 pam/integration-tests/testdata/db/one_user_and_group.sql create mode 100644 pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_after_db_migration create mode 100644 pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_after_db_migration.group create mode 100644 pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_after_db_migration.group.backup diff --git a/pam/integration-tests/ssh_test.go b/pam/integration-tests/ssh_test.go index 7e2cec1a18..e7b287d37f 100644 --- a/pam/integration-tests/ssh_test.go +++ b/pam/integration-tests/ssh_test.go @@ -297,6 +297,12 @@ func testSSHAuthenticate(t *testing.T, sharedSSHd bool) { tape: "simple_auth_locks_unlocks", daemonizeSSHd: true, }, + "Authenticate_user_after_db_migration": { + tape: "simple_auth", + oldDB: "one_user_and_group", + user: "user1", + wantUserAlreadyExist: true, + }, "Deny_authentication_if_max_attempts_reached": { tape: "max_attempts", diff --git a/pam/integration-tests/testdata/db/one_user_and_group.sql b/pam/integration-tests/testdata/db/one_user_and_group.sql new file mode 100644 index 0000000000..86b730f944 --- /dev/null +++ b/pam/integration-tests/testdata/db/one_user_and_group.sql @@ -0,0 +1,40 @@ +PRAGMA foreign_keys=OFF; +BEGIN TRANSACTION; +CREATE TABLE users ( + name TEXT NOT NULL, -- Uniqueness is enforced by the index below + uid INT PRIMARY KEY, -- Uniqueness and not NULL is enforced by PRIMARY KEY + gid INT NOT NULL, + gecos TEXT DEFAULT "", + dir TEXT DEFAULT "", + shell TEXT DEFAULT "/bin/sh", + broker_id TEXT DEFAULT "" +); +INSERT INTO users VALUES('user1',1111,1111,replace('User1 gecos\nOn multiple lines','\n',char(10)),'/home/user1','/bin/sh','broker-id'); +CREATE TABLE GROUPS ( + name TEXT NOT NULL, -- Uniqueness is enforced by the index below + gid INT PRIMARY KEY, -- Uniqueness and not NULL is enforced by PRIMARY KEY + ugid INT NOT NULL -- Uniqueness is enforced by the index below +); +INSERT INTO "GROUPS" VALUES('group1',1111,'user1'); +CREATE TABLE users_to_groups ( + uid INT NOT NULL, + gid INT NOT NULL, + PRIMARY KEY (uid, gid), + FOREIGN KEY (uid) REFERENCES users (uid) ON DELETE CASCADE, + FOREIGN KEY (gid) REFERENCES GROUPS (gid) ON DELETE CASCADE +); +INSERT INTO users_to_groups VALUES(1111,1111); +CREATE TABLE users_to_local_groups ( + uid INT NOT NULL, + group_name TEXT NOT NULL, + PRIMARY KEY (uid, group_name), + FOREIGN KEY (uid) REFERENCES users (uid) ON DELETE CASCADE +); +CREATE TABLE schema_version ( + version INT PRIMARY KEY +); +INSERT INTO schema_version VALUES(1); +CREATE UNIQUE INDEX "idx_user_name" ON users ("name"); +CREATE UNIQUE INDEX "idx_group_name" ON GROUPS ("name"); +CREATE UNIQUE INDEX "idx_group_ugid" ON GROUPS ("ugid"); +COMMIT; diff --git a/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_after_db_migration b/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_after_db_migration new file mode 100644 index 0000000000..de3ff58491 --- /dev/null +++ b/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_after_db_migration @@ -0,0 +1,51 @@ +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Provider selection == + 1. local + 2. ExampleBroker +(user1@localhost) Choose your provider: +> +──────────────────────────────────────────────────────────────────────────────── +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Provider selection == + 1. local + 2. ExampleBroker +(user1@localhost) Choose your provider: +> 2 +──────────────────────────────────────────────────────────────────────────────── +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Provider selection == + 1. local + 2. ExampleBroker +(user1@localhost) Choose your provider: +> 2 +== Password authentication == +Enter 'r' to cancel the request and go back to select the authentication method +(user1@localhost) Gimme your password: +> +──────────────────────────────────────────────────────────────────────────────── +> ssh ${AUTHD_PAM_SSH_USER}@localhost ${AUTHD_PAM_SSH_ARGS} +== Provider selection == + 1. local + 2. ExampleBroker +(user1@localhost) Choose your provider: +> 2 +== Password authentication == +Enter 'r' to cancel the request and go back to select the authentication method +(user1@localhost) Gimme your password: +> +PAM Authenticate() finished for user 'user1' +PAM AcctMgmt() finished for user 'user1' +Environment: + USER=user1 + LOGNAME=user1 + HOME=${AUTHD_TEST_HOME} + PATH=${AUTHD_TEST_PATH} + SHELL=/bin/sh + TERM=xterm-256color + SSH_CLIENT=${AUTHD_TEST_SSH_CLIENT} + SSH_CONNECTION=${AUTHD_TEST_SSH_CONNECTION} + SSH_TTY=${AUTHD_TEST_SSH_TTY} + SSHD: Connected to ssh via authd module! [TestSSHAuthenticate/Authenticate_user_after_db_migration] +Connection to localhost closed. +> +──────────────────────────────────────────────────────────────────────────────── diff --git a/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_after_db_migration.group b/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_after_db_migration.group new file mode 100644 index 0000000000..b9d8206649 --- /dev/null +++ b/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_after_db_migration.group @@ -0,0 +1 @@ +localgroup:x:41: diff --git a/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_after_db_migration.group.backup b/pam/integration-tests/testdata/golden/TestSSHAuthenticate/Authenticate_user_after_db_migration.group.backup new file mode 100644 index 0000000000..e69de29bb2 From 22bd4f9cf8a57862ac53f16d1d5a924b5313c18a Mon Sep 17 00:00:00 2001 From: Adrian Dombeck Date: Wed, 3 Sep 2025 01:21:48 +0200 Subject: [PATCH 55/55] tests: Add Authenticate_user_after_db_migration_and_being_locked_and_unlocked --- pam/integration-tests/ssh_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pam/integration-tests/ssh_test.go b/pam/integration-tests/ssh_test.go index e7b287d37f..76643c46ca 100644 --- a/pam/integration-tests/ssh_test.go +++ b/pam/integration-tests/ssh_test.go @@ -303,6 +303,12 @@ func testSSHAuthenticate(t *testing.T, sharedSSHd bool) { user: "user1", wantUserAlreadyExist: true, }, + "Authenticate_user_after_db_migration_and_being_locked_and_unlocked": { + tape: "simple_auth_locks_unlocks", + oldDB: "one_user_and_group", + user: "user1", + wantUserAlreadyExist: true, + }, "Deny_authentication_if_max_attempts_reached": { tape: "max_attempts",