diff --git a/cmd/authd/integrationtests.go b/cmd/authd/integrationtests.go index f3728fc5e..3fa90eddd 100644 --- a/cmd/authd/integrationtests.go +++ b/cmd/authd/integrationtests.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/ubuntu/authd/internal/services/permissions" - "github.com/ubuntu/authd/internal/users/localgroups" + "github.com/ubuntu/authd/internal/users/localentries" ) // load any behaviour modifiers from env variable. @@ -21,6 +21,6 @@ func init() { if gpasswdArgs == "" || grpFilePath == "" { panic("AUTHD_INTEGRATIONTESTS_GPASSWD_ARGS and AUTHD_INTEGRATIONTESTS_GPASSWD_GRP_FILE_PATH must be set") } - localgroups.Z_ForTests_SetGpasswdCmd(strings.Split(gpasswdArgs, " ")) - localgroups.Z_ForTests_SetGroupPath(grpFilePath) + localentries.Z_ForTests_SetGpasswdCmd(strings.Split(gpasswdArgs, " ")) + localentries.Z_ForTests_SetGroupPath(grpFilePath) } diff --git a/examplebroker/broker.go b/examplebroker/broker.go index 8bef4109b..8bf57fb8c 100644 --- a/examplebroker/broker.go +++ b/examplebroker/broker.go @@ -930,12 +930,11 @@ func userInfoFromName(name string) string { Groups []groupJSONInfo Gecos string }{ - Name: name, - UUID: "uuid-" + name, - Home: "/home/" + name, - Shell: "/usr/bin/bash", - Groups: []groupJSONInfo{{Name: "group-" + name, UGID: "ugid-" + name}}, - Gecos: "gecos for " + name, + Name: name, + UUID: "uuid-" + name, + Home: "/home/" + name, + Shell: "/usr/bin/bash", + Gecos: "gecos for " + name, } switch name { diff --git a/internal/brokers/broker.go b/internal/brokers/broker.go index f2129dfe6..11c315ed3 100644 --- a/internal/brokers/broker.go +++ b/internal/brokers/broker.go @@ -14,7 +14,7 @@ import ( "github.com/ubuntu/authd/internal/brokers/auth" "github.com/ubuntu/authd/internal/brokers/layouts" "github.com/ubuntu/authd/internal/log" - "github.com/ubuntu/authd/internal/users" + "github.com/ubuntu/authd/internal/users/types" "github.com/ubuntu/decorate" "golang.org/x/exp/slices" ) @@ -318,16 +318,16 @@ func (b Broker) parseSessionID(sessionID string) string { } // unmarshalUserInfo tries to unmarshal the rawMsg into a userinfo. -func unmarshalUserInfo(rawMsg json.RawMessage) (users.UserInfo, error) { - var u users.UserInfo +func unmarshalUserInfo(rawMsg json.RawMessage) (types.UserInfo, error) { + var u types.UserInfo if err := json.Unmarshal(rawMsg, &u); err != nil { - return users.UserInfo{}, fmt.Errorf("message is not JSON formatted: %v", err) + return types.UserInfo{}, fmt.Errorf("message is not JSON formatted: %v", err) } return u, nil } // validateUserInfo checks if the specified userinfo is valid. -func validateUserInfo(uInfo users.UserInfo) (err error) { +func validateUserInfo(uInfo types.UserInfo) (err error) { defer decorate.OnError(&err, "provided userinfo is invalid") // Validate username. We don't want to check here if it matches the username from the request, because it's the diff --git a/internal/brokers/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_userinfo b/internal/brokers/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_userinfo index c10caf453..afd840839 100644 --- a/internal/brokers/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_userinfo +++ b/internal/brokers/testdata/golden/TestIsAuthenticated/Error_when_broker_returns_invalid_userinfo @@ -1,4 +1,4 @@ FIRST CALL: access: data: - err: message is not JSON formatted: json: cannot unmarshal string into Go value of type users.UserInfo + err: message is not JSON formatted: json: cannot unmarshal string into Go value of type types.UserInfo diff --git a/internal/services/nss/nss.go b/internal/services/nss/nss.go index 32efdcec7..992114803 100644 --- a/internal/services/nss/nss.go +++ b/internal/services/nss/nss.go @@ -13,6 +13,7 @@ import ( "github.com/ubuntu/authd/internal/proto/authd" "github.com/ubuntu/authd/internal/services/permissions" "github.com/ubuntu/authd/internal/users" + "github.com/ubuntu/authd/internal/users/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -180,19 +181,23 @@ func (s Service) userPreCheck(ctx context.Context, username string) (pwent *auth return nil, fmt.Errorf("user %q is not known by any broker", username) } - var u users.UserEntry + var u types.UserEntry if err := json.Unmarshal([]byte(userinfo), &u); err != nil { return nil, fmt.Errorf("user data from broker invalid: %v", err) } - // We need to generate the ID for the user, as its business logic is authd responsibility, not the broker's. - u.UID = s.userManager.GenerateUID(u.Name) - u.GID = s.userManager.GenerateGID(u.Name) + + // Register a temporary user with a unique UID. If the user authenticates successfully, the user will be added to + // the database with the same UID. + u.UID, err = s.userManager.RegisterUserPreAuth(u.Name) + if err != nil { + return nil, fmt.Errorf("failed to add temporary record for user %q: %v", username, err) + } return nssPasswdFromUsersPasswd(u), nil } // nssPasswdFromUsersPasswd returns a PasswdEntry from users.UserEntry. -func nssPasswdFromUsersPasswd(u users.UserEntry) *authd.PasswdEntry { +func nssPasswdFromUsersPasswd(u types.UserEntry) *authd.PasswdEntry { return &authd.PasswdEntry{ Name: u.Name, Passwd: "x", @@ -205,17 +210,17 @@ func nssPasswdFromUsersPasswd(u users.UserEntry) *authd.PasswdEntry { } // nssGroupFromUsersGroup returns a GroupEntry from users.GroupEntry. -func nssGroupFromUsersGroup(g users.GroupEntry) *authd.GroupEntry { +func nssGroupFromUsersGroup(g types.GroupEntry) *authd.GroupEntry { return &authd.GroupEntry{ Name: g.Name, - Passwd: "x", + Passwd: g.Passwd, Gid: g.GID, Members: g.Users, } } // nssShadowFromUsersShadow returns a ShadowEntry from users.ShadowEntry. -func nssShadowFromUsersShadow(u users.ShadowEntry) *authd.ShadowEntry { +func nssShadowFromUsersShadow(u types.ShadowEntry) *authd.ShadowEntry { return &authd.ShadowEntry{ Name: u.Name, Passwd: "x", diff --git a/internal/services/nss/nss_test.go b/internal/services/nss/nss_test.go index 58b8ee270..1ff7ac55e 100644 --- a/internal/services/nss/nss_test.go +++ b/internal/services/nss/nss_test.go @@ -19,7 +19,8 @@ import ( "github.com/ubuntu/authd/internal/testutils/golden" "github.com/ubuntu/authd/internal/users" "github.com/ubuntu/authd/internal/users/cache" - localgroupstestutils "github.com/ubuntu/authd/internal/users/localgroups/testutils" + "github.com/ubuntu/authd/internal/users/idgenerator" + localgroupstestutils "github.com/ubuntu/authd/internal/users/localentries/testutils" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" @@ -332,7 +333,14 @@ func newUserManagerForTests(t *testing.T, sourceDB string) *users.Manager { } cache.Z_ForTests_CreateDBFromYAML(t, filepath.Join("testdata", sourceDB), cacheDir) - m, err := users.NewManager(users.DefaultConfig, cacheDir) + managerOpts := []users.Option{ + users.WithIDGenerator(&idgenerator.IDGeneratorMock{ + UIDsToGenerate: []uint32{1234}, + GIDsToGenerate: []uint32{1234}, + }), + } + + m, err := users.NewManager(users.DefaultConfig, cacheDir, managerOpts...) require.NoError(t, err, "Setup: could not create user manager") t.Cleanup(func() { _ = m.Stop() }) diff --git a/internal/services/nss/testdata/golden/TestGetGroupByGID/Return_existing_group b/internal/services/nss/testdata/golden/TestGetGroupByGID/Return_existing_group index 67e5e40ed..3b1067ccf 100644 --- a/internal/services/nss/testdata/golden/TestGetGroupByGID/Return_existing_group +++ b/internal/services/nss/testdata/golden/TestGetGroupByGID/Return_existing_group @@ -1,5 +1,5 @@ name: group1 -passwd: x +passwd: "" gid: 11111 members: - user1 diff --git a/internal/services/nss/testdata/golden/TestGetGroupByName/Return_existing_group b/internal/services/nss/testdata/golden/TestGetGroupByName/Return_existing_group index 67e5e40ed..3b1067ccf 100644 --- a/internal/services/nss/testdata/golden/TestGetGroupByName/Return_existing_group +++ b/internal/services/nss/testdata/golden/TestGetGroupByName/Return_existing_group @@ -1,5 +1,5 @@ name: group1 -passwd: x +passwd: "" gid: 11111 members: - user1 diff --git a/internal/services/nss/testdata/golden/TestGetGroupEntries/Return_all_groups b/internal/services/nss/testdata/golden/TestGetGroupEntries/Return_all_groups index f24c179b7..b259c2a44 100644 --- a/internal/services/nss/testdata/golden/TestGetGroupEntries/Return_all_groups +++ b/internal/services/nss/testdata/golden/TestGetGroupEntries/Return_all_groups @@ -1,20 +1,20 @@ - name: group1 - passwd: x + passwd: "" gid: 11111 members: - user1 - name: group2 - passwd: x + passwd: "" gid: 22222 members: - user2 - name: group3 - passwd: x + passwd: "" gid: 33333 members: - user3 - name: commongroup - passwd: x + passwd: "" gid: 99999 members: - user2 diff --git a/internal/services/nss/testdata/golden/TestGetPasswdByName/Precheck_user_if_not_in_cache b/internal/services/nss/testdata/golden/TestGetPasswdByName/Precheck_user_if_not_in_cache index 08d5ae8cf..b445f7dc5 100644 --- a/internal/services/nss/testdata/golden/TestGetPasswdByName/Precheck_user_if_not_in_cache +++ b/internal/services/nss/testdata/golden/TestGetPasswdByName/Precheck_user_if_not_in_cache @@ -1,7 +1,7 @@ name: user-pre-check passwd: x -uid: 1053432963 -gid: 1053432963 +uid: 1234 +gid: 0 gecos: gecos for user-pre-check homedir: /home/user-pre-check shell: /bin/sh/user-pre-check diff --git a/internal/services/nss/testdata/golden/TestGetPasswdByName/Prechecked_user_with_upper_cases_in_username_has_same_id_as_lower_case b/internal/services/nss/testdata/golden/TestGetPasswdByName/Prechecked_user_with_upper_cases_in_username_has_same_id_as_lower_case index d6f86a50d..d1bc2af56 100644 --- a/internal/services/nss/testdata/golden/TestGetPasswdByName/Prechecked_user_with_upper_cases_in_username_has_same_id_as_lower_case +++ b/internal/services/nss/testdata/golden/TestGetPasswdByName/Prechecked_user_with_upper_cases_in_username_has_same_id_as_lower_case @@ -1,7 +1,7 @@ name: User-Pre-Check passwd: x -uid: 1053432963 -gid: 1053432963 +uid: 1234 +gid: 0 gecos: gecos for User-Pre-Check homedir: /home/User-Pre-Check shell: /bin/sh/User-Pre-Check diff --git a/internal/services/pam/pam.go b/internal/services/pam/pam.go index 52c210d91..e5b3ccd21 100644 --- a/internal/services/pam/pam.go +++ b/internal/services/pam/pam.go @@ -15,6 +15,7 @@ import ( "github.com/ubuntu/authd/internal/proto/authd" "github.com/ubuntu/authd/internal/services/permissions" "github.com/ubuntu/authd/internal/users" + "github.com/ubuntu/authd/internal/users/types" "github.com/ubuntu/decorate" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -262,7 +263,7 @@ func (s Service) IsAuthenticated(ctx context.Context, req *authd.IARequest) (res }, nil } - var uInfo users.UserInfo + var uInfo types.UserInfo if err := json.Unmarshal([]byte(data), &uInfo); err != nil { return nil, fmt.Errorf("user data from broker invalid: %v", err) } diff --git a/internal/services/pam/pam_test.go b/internal/services/pam/pam_test.go index a9ee759ca..9c7f34d5c 100644 --- a/internal/services/pam/pam_test.go +++ b/internal/services/pam/pam_test.go @@ -27,7 +27,8 @@ import ( "github.com/ubuntu/authd/internal/testutils/golden" "github.com/ubuntu/authd/internal/users" "github.com/ubuntu/authd/internal/users/cache" - localgroupstestutils "github.com/ubuntu/authd/internal/users/localgroups/testutils" + "github.com/ubuntu/authd/internal/users/idgenerator" + localgroupstestutils "github.com/ubuntu/authd/internal/users/localentries/testutils" userstestutils "github.com/ubuntu/authd/internal/users/testutils" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -433,11 +434,9 @@ func TestIsAuthenticated(t *testing.T) { "Update local groups": {username: "success_with_local_groups", localGroupsFile: "valid.group"}, // service errors - "Error when not root": {username: "success", currentUserNotRoot: true}, - "Error when UID conflicts with existing different user": {username: "conflicting-uid", existingDB: "cache-with-conflicting-uid.db"}, - "Error when GID conflicts with existing different group": {username: "conflicting-gid", existingDB: "cache-with-conflicting-gid.db"}, - "Error when sessionID is empty": {sessionID: "-"}, - "Error when there is no broker": {sessionID: "invalid-session"}, + "Error when not root": {username: "success", currentUserNotRoot: true}, + "Error when sessionID is empty": {sessionID: "-"}, + "Error when there is no broker": {sessionID: "invalid-session"}, // broker errors "Error when authenticating": {username: "IA_error"}, @@ -466,7 +465,14 @@ func TestIsAuthenticated(t *testing.T) { cache.Z_ForTests_CreateDBFromYAML(t, filepath.Join(testutils.TestFamilyPath(t), tc.existingDB), cacheDir) } - m, err := users.NewManager(users.DefaultConfig, cacheDir) + managerOpts := []users.Option{ + users.WithIDGenerator(&idgenerator.IDGeneratorMock{ + UIDsToGenerate: []uint32{1111}, + GIDsToGenerate: []uint32{1111, 2222}, + }), + } + + m, err := users.NewManager(users.DefaultConfig, cacheDir, managerOpts...) require.NoError(t, err, "Setup: could not create user manager") t.Cleanup(func() { _ = m.Stop() }) pm := newPermissionManager(t, false) // Allow starting the session (current user considered root) @@ -550,13 +556,19 @@ func TestIDGeneration(t *testing.T) { username string }{ "Generate ID": {username: "success"}, - "Generates same ID if user has upper cases in username": {username: "SuCcEsS"}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { t.Parallel() - m, err := users.NewManager(users.DefaultConfig, t.TempDir()) + managerOpts := []users.Option{ + users.WithIDGenerator(&idgenerator.IDGeneratorMock{ + UIDsToGenerate: []uint32{1111}, + GIDsToGenerate: []uint32{1111, 2222}, + }), + } + + m, err := users.NewManager(users.DefaultConfig, t.TempDir(), managerOpts...) require.NoError(t, err, "Setup: could not create user manager") t.Cleanup(func() { _ = m.Stop() }) pm := newPermissionManager(t, false) // Allow starting the session (current user considered root) 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 9a8ad2f55..6a1dc08f6 100644 --- a/internal/services/pam/testdata/golden/TestIDGeneration/Generate_ID/cache.db +++ b/internal/services/pam/testdata/golden/TestIDGeneration/Generate_ID/cache.db @@ -1,21 +1,21 @@ GroupByID: - "1648262143": '{"Name":"TestIDGeneration_separator_success","GID":1648262143,"UGID":"TestIDGeneration_separator_success"}' - "1946747284": '{"Name":"group-success","GID":1946747284,"UGID":"ugid-success"}' + "1111": '{"Name":"TestIDGeneration_separator_success","GID":1111,"UGID":"TestIDGeneration_separator_success"}' + "2222": '{"Name":"group-success","GID":2222,"UGID":"ugid-success"}' GroupByName: - TestIDGeneration_separator_success: '{"Name":"TestIDGeneration_separator_success","GID":1648262143,"UGID":"TestIDGeneration_separator_success"}' - group-success: '{"Name":"group-success","GID":1946747284,"UGID":"ugid-success"}' + TestIDGeneration_separator_success: '{"Name":"TestIDGeneration_separator_success","GID":1111,"UGID":"TestIDGeneration_separator_success"}' + group-success: '{"Name":"group-success","GID":2222,"UGID":"ugid-success"}' GroupByUGID: - TestIDGeneration_separator_success: '{"Name":"TestIDGeneration_separator_success","GID":1648262143,"UGID":"TestIDGeneration_separator_success"}' - ugid-success: '{"Name":"group-success","GID":1946747284,"UGID":"ugid-success"}' + TestIDGeneration_separator_success: '{"Name":"TestIDGeneration_separator_success","GID":1111,"UGID":"TestIDGeneration_separator_success"}' + ugid-success: '{"Name":"group-success","GID":2222,"UGID":"ugid-success"}' GroupToUsers: - "1648262143": '{"GID":1648262143,"UIDs":[1648262143]}' - "1946747284": '{"GID":1946747284,"UIDs":[1648262143]}' + "1111": '{"GID":1111,"UIDs":[1111]}' + "2222": '{"GID":2222,"UIDs":[1111]}' UserByID: - "1648262143": '{"Name":"TestIDGeneration_separator_success","UID":1648262143,"GID":1648262143,"Gecos":"gecos for success","Dir":"/home/success","Shell":"/bin/sh/success","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + "1111": '{"Name":"TestIDGeneration_separator_success","UID":1111,"GID":1111,"Gecos":"gecos for success","Dir":"/home/success","Shell":"/bin/sh/success","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' UserByName: - TestIDGeneration_separator_success: '{"Name":"TestIDGeneration_separator_success","UID":1648262143,"GID":1648262143,"Gecos":"gecos for success","Dir":"/home/success","Shell":"/bin/sh/success","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + TestIDGeneration_separator_success: '{"Name":"TestIDGeneration_separator_success","UID":1111,"GID":1111,"Gecos":"gecos for success","Dir":"/home/success","Shell":"/bin/sh/success","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' UserToBroker: {} UserToGroups: - "1648262143": '{"UID":1648262143,"GIDs":[1648262143,1946747284]}' + "1111": '{"UID":1111,"GIDs":[1111,2222]}' UserToLocalGroups: - "1648262143": "null" + "1111": "null" 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 1b862a8fc..6aa4bc21a 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 @@ -7,4 +7,4 @@ UserByName: {} UserToBroker: {} UserToGroups: {} UserToLocalGroups: - "1042855937": '["localgroup1","localgroup3"]' + "1111": '["localgroup1","localgroup3"]' 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 c8ed669d3..b917b0921 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 users.UserInfo + err: can't check authentication: 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_calling_second_time_without_cancelling/cache.db b/internal/services/pam/testdata/golden/TestIsAuthenticated/Error_when_calling_second_time_without_cancelling/cache.db index 20a373d01..74c72db30 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 @@ -1,21 +1,21 @@ GroupByID: - "1369382419": '{"Name":"group-IA_second_call","GID":1369382419,"UGID":"ugid-IA_second_call"}' - "1556535091": '{"Name":"TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call","GID":1556535091,"UGID":"TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call"}' + "1111": '{"Name":"TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call","GID":1111,"UGID":"TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call"}' + "2222": '{"Name":"group-IA_second_call","GID":2222,"UGID":"ugid-IA_second_call"}' GroupByName: - TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call: '{"Name":"TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call","GID":1556535091,"UGID":"TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call"}' - group-IA_second_call: '{"Name":"group-IA_second_call","GID":1369382419,"UGID":"ugid-IA_second_call"}' + TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call: '{"Name":"TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call","GID":1111,"UGID":"TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call"}' + group-IA_second_call: '{"Name":"group-IA_second_call","GID":2222,"UGID":"ugid-IA_second_call"}' GroupByUGID: - TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call: '{"Name":"TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call","GID":1556535091,"UGID":"TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call"}' - ugid-IA_second_call: '{"Name":"group-IA_second_call","GID":1369382419,"UGID":"ugid-IA_second_call"}' + TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call: '{"Name":"TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call","GID":1111,"UGID":"TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call"}' + ugid-IA_second_call: '{"Name":"group-IA_second_call","GID":2222,"UGID":"ugid-IA_second_call"}' GroupToUsers: - "1369382419": '{"GID":1369382419,"UIDs":[1556535091]}' - "1556535091": '{"GID":1556535091,"UIDs":[1556535091]}' + "1111": '{"GID":1111,"UIDs":[1111]}' + "2222": '{"GID":2222,"UIDs":[1111]}' UserByID: - "1556535091": '{"Name":"TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call","UID":1556535091,"GID":1556535091,"Gecos":"gecos for IA_second_call","Dir":"/home/IA_second_call","Shell":"/bin/sh/IA_second_call","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + "1111": '{"Name":"TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call","UID":1111,"GID":1111,"Gecos":"gecos for IA_second_call","Dir":"/home/IA_second_call","Shell":"/bin/sh/IA_second_call","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' UserByName: - TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call: '{"Name":"TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call","UID":1556535091,"GID":1556535091,"Gecos":"gecos for IA_second_call","Dir":"/home/IA_second_call","Shell":"/bin/sh/IA_second_call","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call: '{"Name":"TestIsAuthenticated/Error_when_calling_second_time_without_cancelling_separator_IA_second_call","UID":1111,"GID":1111,"Gecos":"gecos for IA_second_call","Dir":"/home/IA_second_call","Shell":"/bin/sh/IA_second_call","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' UserToBroker: {} UserToGroups: - "1556535091": '{"UID":1556535091,"GIDs":[1556535091,1369382419]}' + "1111": '{"UID":1111,"GIDs":[1111,2222]}' UserToLocalGroups: - "1556535091": "null" + "1111": "null" 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 ecf0a3721..5ba237713 100644 --- a/internal/services/pam/testdata/golden/TestIsAuthenticated/Successfully_authenticate/cache.db +++ b/internal/services/pam/testdata/golden/TestIsAuthenticated/Successfully_authenticate/cache.db @@ -1,21 +1,21 @@ GroupByID: - "1127066031": '{"Name":"TestIsAuthenticated/Successfully_authenticate_separator_success","GID":1127066031,"UGID":"TestIsAuthenticated/Successfully_authenticate_separator_success"}' - "1946747284": '{"Name":"group-success","GID":1946747284,"UGID":"ugid-success"}' + "1111": '{"Name":"TestIsAuthenticated/Successfully_authenticate_separator_success","GID":1111,"UGID":"TestIsAuthenticated/Successfully_authenticate_separator_success"}' + "2222": '{"Name":"group-success","GID":2222,"UGID":"ugid-success"}' GroupByName: - TestIsAuthenticated/Successfully_authenticate_separator_success: '{"Name":"TestIsAuthenticated/Successfully_authenticate_separator_success","GID":1127066031,"UGID":"TestIsAuthenticated/Successfully_authenticate_separator_success"}' - group-success: '{"Name":"group-success","GID":1946747284,"UGID":"ugid-success"}' + TestIsAuthenticated/Successfully_authenticate_separator_success: '{"Name":"TestIsAuthenticated/Successfully_authenticate_separator_success","GID":1111,"UGID":"TestIsAuthenticated/Successfully_authenticate_separator_success"}' + group-success: '{"Name":"group-success","GID":2222,"UGID":"ugid-success"}' GroupByUGID: - TestIsAuthenticated/Successfully_authenticate_separator_success: '{"Name":"TestIsAuthenticated/Successfully_authenticate_separator_success","GID":1127066031,"UGID":"TestIsAuthenticated/Successfully_authenticate_separator_success"}' - ugid-success: '{"Name":"group-success","GID":1946747284,"UGID":"ugid-success"}' + TestIsAuthenticated/Successfully_authenticate_separator_success: '{"Name":"TestIsAuthenticated/Successfully_authenticate_separator_success","GID":1111,"UGID":"TestIsAuthenticated/Successfully_authenticate_separator_success"}' + ugid-success: '{"Name":"group-success","GID":2222,"UGID":"ugid-success"}' GroupToUsers: - "1127066031": '{"GID":1127066031,"UIDs":[1127066031]}' - "1946747284": '{"GID":1946747284,"UIDs":[1127066031]}' + "1111": '{"GID":1111,"UIDs":[1111]}' + "2222": '{"GID":2222,"UIDs":[1111]}' UserByID: - "1127066031": '{"Name":"TestIsAuthenticated/Successfully_authenticate_separator_success","UID":1127066031,"GID":1127066031,"Gecos":"gecos for success","Dir":"/home/success","Shell":"/bin/sh/success","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + "1111": '{"Name":"TestIsAuthenticated/Successfully_authenticate_separator_success","UID":1111,"GID":1111,"Gecos":"gecos for success","Dir":"/home/success","Shell":"/bin/sh/success","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' UserByName: - TestIsAuthenticated/Successfully_authenticate_separator_success: '{"Name":"TestIsAuthenticated/Successfully_authenticate_separator_success","UID":1127066031,"GID":1127066031,"Gecos":"gecos for success","Dir":"/home/success","Shell":"/bin/sh/success","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + TestIsAuthenticated/Successfully_authenticate_separator_success: '{"Name":"TestIsAuthenticated/Successfully_authenticate_separator_success","UID":1111,"GID":1111,"Gecos":"gecos for success","Dir":"/home/success","Shell":"/bin/sh/success","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' UserToBroker: {} UserToGroups: - "1127066031": '{"UID":1127066031,"GIDs":[1127066031,1946747284]}' + "1111": '{"UID":1111,"GIDs":[1111,2222]}' UserToLocalGroups: - "1127066031": "null" + "1111": "null" 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 f382e8110..3e70cc6fe 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 @@ -1,21 +1,21 @@ GroupByID: - "1369382419": '{"Name":"group-IA_second_call","GID":1369382419,"UGID":"ugid-IA_second_call"}' - "1569396774": '{"Name":"TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call","GID":1569396774,"UGID":"TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call"}' + "1111": '{"Name":"TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call","GID":1111,"UGID":"TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call"}' + "2222": '{"Name":"group-IA_second_call","GID":2222,"UGID":"ugid-IA_second_call"}' GroupByName: - TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call: '{"Name":"TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call","GID":1569396774,"UGID":"TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call"}' - group-IA_second_call: '{"Name":"group-IA_second_call","GID":1369382419,"UGID":"ugid-IA_second_call"}' + TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call: '{"Name":"TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call","GID":1111,"UGID":"TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call"}' + group-IA_second_call: '{"Name":"group-IA_second_call","GID":2222,"UGID":"ugid-IA_second_call"}' GroupByUGID: - TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call: '{"Name":"TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call","GID":1569396774,"UGID":"TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call"}' - ugid-IA_second_call: '{"Name":"group-IA_second_call","GID":1369382419,"UGID":"ugid-IA_second_call"}' + TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call: '{"Name":"TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call","GID":1111,"UGID":"TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call"}' + ugid-IA_second_call: '{"Name":"group-IA_second_call","GID":2222,"UGID":"ugid-IA_second_call"}' GroupToUsers: - "1369382419": '{"GID":1369382419,"UIDs":[1569396774]}' - "1569396774": '{"GID":1569396774,"UIDs":[1569396774]}' + "1111": '{"GID":1111,"UIDs":[1111]}' + "2222": '{"GID":2222,"UIDs":[1111]}' UserByID: - "1569396774": '{"Name":"TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call","UID":1569396774,"GID":1569396774,"Gecos":"gecos for IA_second_call","Dir":"/home/IA_second_call","Shell":"/bin/sh/IA_second_call","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + "1111": '{"Name":"TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call","UID":1111,"GID":1111,"Gecos":"gecos for IA_second_call","Dir":"/home/IA_second_call","Shell":"/bin/sh/IA_second_call","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' UserByName: - TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call: '{"Name":"TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call","UID":1569396774,"GID":1569396774,"Gecos":"gecos for IA_second_call","Dir":"/home/IA_second_call","Shell":"/bin/sh/IA_second_call","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call: '{"Name":"TestIsAuthenticated/Successfully_authenticate_if_first_call_is_canceled_separator_IA_second_call","UID":1111,"GID":1111,"Gecos":"gecos for IA_second_call","Dir":"/home/IA_second_call","Shell":"/bin/sh/IA_second_call","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' UserToBroker: {} UserToGroups: - "1569396774": '{"UID":1569396774,"GIDs":[1569396774,1369382419]}' + "1111": '{"UID":1111,"GIDs":[1111,2222]}' UserToLocalGroups: - "1569396774": "null" + "1111": "null" 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 ced287cf0..f050c6063 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 @@ -1,27 +1,27 @@ GroupByID: + "1111": '{"Name":"TestIsAuthenticated/Update_existing_DB_on_success_separator_success","GID":1111,"UGID":"TestIsAuthenticated/Update_existing_DB_on_success_separator_success"}' + "2222": '{"Name":"group-success","GID":2222,"UGID":"ugid-success"}' "88888": '{"Name":"group-success","GID":88888,"UGID":"group-success"}' - "1714308795": '{"Name":"TestIsAuthenticated/Update_existing_DB_on_success_separator_success","GID":1714308795,"UGID":"TestIsAuthenticated/Update_existing_DB_on_success_separator_success"}' - "1946747284": '{"Name":"group-success","GID":1946747284,"UGID":"ugid-success"}' GroupByName: - TestIsAuthenticated/Update_existing_DB_on_success_separator_success: '{"Name":"TestIsAuthenticated/Update_existing_DB_on_success_separator_success","GID":1714308795,"UGID":"TestIsAuthenticated/Update_existing_DB_on_success_separator_success"}' - group-success: '{"Name":"group-success","GID":1946747284,"UGID":"ugid-success"}' + TestIsAuthenticated/Update_existing_DB_on_success_separator_success: '{"Name":"TestIsAuthenticated/Update_existing_DB_on_success_separator_success","GID":1111,"UGID":"TestIsAuthenticated/Update_existing_DB_on_success_separator_success"}' + group-success: '{"Name":"group-success","GID":2222,"UGID":"ugid-success"}' GroupByUGID: - TestIsAuthenticated/Update_existing_DB_on_success_separator_success: '{"Name":"TestIsAuthenticated/Update_existing_DB_on_success_separator_success","GID":1714308795,"UGID":"TestIsAuthenticated/Update_existing_DB_on_success_separator_success"}' - ugid-success: '{"Name":"group-success","GID":1946747284,"UGID":"ugid-success"}' + TestIsAuthenticated/Update_existing_DB_on_success_separator_success: '{"Name":"TestIsAuthenticated/Update_existing_DB_on_success_separator_success","GID":1111,"UGID":"TestIsAuthenticated/Update_existing_DB_on_success_separator_success"}' + ugid-success: '{"Name":"group-success","GID":2222,"UGID":"ugid-success"}' GroupToUsers: + "1111": '{"GID":1111,"UIDs":[1111]}' + "2222": '{"GID":2222,"UIDs":[1111]}' "88888": '{"GID":88888,"UIDs":[77777]}' - "1714308795": '{"GID":1714308795,"UIDs":[1714308795]}' - "1946747284": '{"GID":1946747284,"UIDs":[1714308795]}' UserByID: + "1111": '{"Name":"TestIsAuthenticated/Update_existing_DB_on_success_separator_success","UID":1111,"GID":1111,"Gecos":"gecos for success","Dir":"/home/success","Shell":"/bin/sh/success","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' "77777": '{"Name":"otheruser","UID":77777,"GID":88888,"Gecos":"gecos for other user","Dir":"/home/otheruser","Shell":"/bin/sh/otheruser","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"AAAAATIME"}' - "1714308795": '{"Name":"TestIsAuthenticated/Update_existing_DB_on_success_separator_success","UID":1714308795,"GID":1714308795,"Gecos":"gecos for success","Dir":"/home/success","Shell":"/bin/sh/success","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' UserByName: - TestIsAuthenticated/Update_existing_DB_on_success_separator_success: '{"Name":"TestIsAuthenticated/Update_existing_DB_on_success_separator_success","UID":1714308795,"GID":1714308795,"Gecos":"gecos for success","Dir":"/home/success","Shell":"/bin/sh/success","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + TestIsAuthenticated/Update_existing_DB_on_success_separator_success: '{"Name":"TestIsAuthenticated/Update_existing_DB_on_success_separator_success","UID":1111,"GID":1111,"Gecos":"gecos for success","Dir":"/home/success","Shell":"/bin/sh/success","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' otheruser: '{"Name":"otheruser","UID":77777,"GID":88888,"Gecos":"gecos for other user","Dir":"/home/otheruser","Shell":"/bin/sh/otheruser","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"AAAAATIME"}' UserToBroker: "77777": '"broker-id"' UserToGroups: + "1111": '{"UID":1111,"GIDs":[1111,2222]}' "77777": '{"UID":77777,"GIDs":[88888]}' - "1714308795": '{"UID":1714308795,"GIDs":[1714308795,1946747284]}' UserToLocalGroups: - "1714308795": "null" + "1111": "null" 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 bf5d50fd5..181d52fc9 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 @@ -1,21 +1,21 @@ GroupByID: - "1370830640": '{"Name":"TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups","GID":1370830640,"UGID":"TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups"}' - "1602050681": '{"Name":"group-success_with_local_groups","GID":1602050681,"UGID":"ugid-success_with_local_groups"}' + "1111": '{"Name":"TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups","GID":1111,"UGID":"TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups"}' + "2222": '{"Name":"group-success_with_local_groups","GID":2222,"UGID":"ugid-success_with_local_groups"}' GroupByName: - TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups: '{"Name":"TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups","GID":1370830640,"UGID":"TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups"}' - group-success_with_local_groups: '{"Name":"group-success_with_local_groups","GID":1602050681,"UGID":"ugid-success_with_local_groups"}' + TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups: '{"Name":"TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups","GID":1111,"UGID":"TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups"}' + group-success_with_local_groups: '{"Name":"group-success_with_local_groups","GID":2222,"UGID":"ugid-success_with_local_groups"}' GroupByUGID: - TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups: '{"Name":"TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups","GID":1370830640,"UGID":"TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups"}' - ugid-success_with_local_groups: '{"Name":"group-success_with_local_groups","GID":1602050681,"UGID":"ugid-success_with_local_groups"}' + TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups: '{"Name":"TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups","GID":1111,"UGID":"TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups"}' + ugid-success_with_local_groups: '{"Name":"group-success_with_local_groups","GID":2222,"UGID":"ugid-success_with_local_groups"}' GroupToUsers: - "1370830640": '{"GID":1370830640,"UIDs":[1370830640]}' - "1602050681": '{"GID":1602050681,"UIDs":[1370830640]}' + "1111": '{"GID":1111,"UIDs":[1111]}' + "2222": '{"GID":2222,"UIDs":[1111]}' UserByID: - "1370830640": '{"Name":"TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups","UID":1370830640,"GID":1370830640,"Gecos":"gecos for success_with_local_groups","Dir":"/home/success_with_local_groups","Shell":"/bin/sh/success_with_local_groups","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + "1111": '{"Name":"TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups","UID":1111,"GID":1111,"Gecos":"gecos for success_with_local_groups","Dir":"/home/success_with_local_groups","Shell":"/bin/sh/success_with_local_groups","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' UserByName: - TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups: '{"Name":"TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups","UID":1370830640,"GID":1370830640,"Gecos":"gecos for success_with_local_groups","Dir":"/home/success_with_local_groups","Shell":"/bin/sh/success_with_local_groups","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups: '{"Name":"TestIsAuthenticated/Update_local_groups_separator_success_with_local_groups","UID":1111,"GID":1111,"Gecos":"gecos for success_with_local_groups","Dir":"/home/success_with_local_groups","Shell":"/bin/sh/success_with_local_groups","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' UserToBroker: {} UserToGroups: - "1370830640": '{"UID":1370830640,"GIDs":[1370830640,1602050681]}' + "1111": '{"UID":1111,"GIDs":[1111,2222]}' UserToLocalGroups: - "1370830640": '["localgroup1","localgroup3"]' + "1111": '["localgroup1","localgroup3"]' diff --git a/internal/users/cache/testdata/golden/TestUserGroups/Get_groups_of_existing_user b/internal/users/cache/testdata/golden/TestUserGroups/Get_groups_of_existing_user index 949a41218..aef8ac739 100644 --- a/internal/users/cache/testdata/golden/TestUserGroups/Get_groups_of_existing_user +++ b/internal/users/cache/testdata/golden/TestUserGroups/Get_groups_of_existing_user @@ -1,4 +1,5 @@ - name: group1 gid: 11111 + ugid: "12345678" users: - user1 diff --git a/internal/users/defs.go b/internal/users/defs.go index a48864a0a..704d2c9e5 100644 --- a/internal/users/defs.go +++ b/internal/users/defs.go @@ -2,57 +2,12 @@ package users import ( "github.com/ubuntu/authd/internal/users/cache" + "github.com/ubuntu/authd/internal/users/types" ) -// UserInfo is the user information returned by the broker. -type UserInfo struct { - Name string - UID uint32 - Gecos string - Dir string - Shell string - - Groups []GroupInfo -} - -// GroupInfo is the group information returned by the broker. -type GroupInfo struct { - Name string - GID *uint32 - UGID string -} - -// UserEntry is the user information sent to the NSS service. -type UserEntry struct { - Name string - UID uint32 - GID uint32 - Gecos string - Dir string - Shell string -} - -// ShadowEntry is the shadow information sent to the NSS service. -type ShadowEntry struct { - Name string - LastPwdChange int - MaxPwdAge int - PwdWarnPeriod int - PwdInactivity int - MinPwdAge int - ExpirationDate int -} - -// GroupEntry is the group information sent to the NSS service. -type GroupEntry struct { - Name string - GID uint32 - Users []string -} - // userEntryFromUserDB returns a UserEntry from a UserDB. -func userEntryFromUserDB(u cache.UserDB) UserEntry { - return UserEntry{ +func userEntryFromUserDB(u cache.UserDB) types.UserEntry { + return types.UserEntry{ Name: u.Name, UID: u.UID, GID: u.GID, @@ -63,8 +18,8 @@ func userEntryFromUserDB(u cache.UserDB) UserEntry { } // shadowEntryFromUserDB returns a ShadowEntry from a UserDB. -func shadowEntryFromUserDB(u cache.UserDB) ShadowEntry { - return ShadowEntry{ +func shadowEntryFromUserDB(u cache.UserDB) types.ShadowEntry { + return types.ShadowEntry{ Name: u.Name, LastPwdChange: u.LastPwdChange, MaxPwdAge: u.MaxPwdAge, @@ -76,8 +31,8 @@ func shadowEntryFromUserDB(u cache.UserDB) ShadowEntry { } // groupEntryFromGroupDB returns a GroupEntry from a GroupDB. -func groupEntryFromGroupDB(g cache.GroupDB) GroupEntry { - return GroupEntry{ +func groupEntryFromGroupDB(g cache.GroupDB) types.GroupEntry { + return types.GroupEntry{ Name: g.Name, GID: g.GID, Users: g.Users, diff --git a/internal/users/export_test.go b/internal/users/export_test.go new file mode 100644 index 000000000..387f7418d --- /dev/null +++ b/internal/users/export_test.go @@ -0,0 +1,9 @@ +package users + +import ( + "github.com/ubuntu/authd/internal/users/tempentries" +) + +func (m *Manager) TemporaryRecords() *tempentries.TemporaryRecords { + return m.temporaryRecords +} diff --git a/internal/users/idgenerator/idgenerator.go b/internal/users/idgenerator/idgenerator.go new file mode 100644 index 000000000..f95dd605b --- /dev/null +++ b/internal/users/idgenerator/idgenerator.go @@ -0,0 +1,38 @@ +// Package idgenerator provides an ID generator that generates UIDs and GIDs in a specific range. +package idgenerator + +import ( + "crypto/rand" + "math/big" +) + +// IDGenerator is an ID generator that generates UIDs and GIDs in a specific range. +type IDGenerator struct { + UIDMin uint32 + UIDMax uint32 + GIDMin uint32 + GIDMax uint32 +} + +// GenerateUID generates a random UID in the configured range. +func (g *IDGenerator) GenerateUID() (uint32, error) { + return generateID(g.UIDMin, g.UIDMax) +} + +// GenerateGID generates a random GID in the configured range. +func (g *IDGenerator) GenerateGID() (uint32, error) { + return generateID(g.GIDMin, g.GIDMax) +} + +func generateID(minID, maxID uint32) (uint32, error) { + diff := int64(maxID - minID) + // Generate a cryptographically secure random number between 0 and diff + nBig, err := rand.Int(rand.Reader, big.NewInt(diff+1)) + if err != nil { + return 0, err + } + + // Add minID to get a number in the desired range + //nolint:gosec // This conversion is safe because we only generate UIDs which are positive and smaller than uint32. + return uint32(nBig.Int64()) + minID, nil +} diff --git a/internal/users/idgenerator/idgenerator_test.go b/internal/users/idgenerator/idgenerator_test.go new file mode 100644 index 000000000..e926d8014 --- /dev/null +++ b/internal/users/idgenerator/idgenerator_test.go @@ -0,0 +1,31 @@ +package idgenerator + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGenerateID(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + input string + idMin uint32 + idMax uint32 + }{ + "Generated ID is within the defined range": {input: "test", idMin: 1000, idMax: 2000}, + "Generate ID with minimum ID equal to maximum ID": {input: "test", idMin: 1000, idMax: 1000}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + id, err := generateID(tc.idMin, tc.idMax) + require.NoError(t, err, "GenerateID should not have failed") + + require.GreaterOrEqual(t, id, tc.idMin, "GenerateID should return an ID greater or equal to the minimum") + require.LessOrEqual(t, id, tc.idMax, "GenerateID should return an ID less or equal to the maximum") + }) + } +} diff --git a/internal/users/idgenerator/testutils.go b/internal/users/idgenerator/testutils.go new file mode 100644 index 000000000..81375cb28 --- /dev/null +++ b/internal/users/idgenerator/testutils.go @@ -0,0 +1,30 @@ +package idgenerator + +import "fmt" + +// IDGeneratorMock is a mock implementation of the IDGenerator interface. +// revive:disable-next-line:exported // We don't want to call this type just "Mock" +type IDGeneratorMock struct { + UIDsToGenerate []uint32 + GIDsToGenerate []uint32 +} + +// GenerateUID generates a UID. +func (g *IDGeneratorMock) GenerateUID() (uint32, error) { + if len(g.UIDsToGenerate) == 0 { + return 0, fmt.Errorf("no more UIDs to generate") + } + uid := g.UIDsToGenerate[0] + g.UIDsToGenerate = g.UIDsToGenerate[1:] + return uid, nil +} + +// GenerateGID generates a GID. +func (g *IDGeneratorMock) GenerateGID() (uint32, error) { + if len(g.GIDsToGenerate) == 0 { + return 0, fmt.Errorf("no more GIDs to generate") + } + gid := g.GIDsToGenerate[0] + g.GIDsToGenerate = g.GIDsToGenerate[1:] + return gid, nil +} diff --git a/internal/users/internal_test.go b/internal/users/internal_test.go deleted file mode 100644 index 9f07b8a6a..000000000 --- a/internal/users/internal_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package users - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestGenerateID(t *testing.T) { - t.Parallel() - - tests := map[string]struct { - input string - idMin uint32 - idMax uint32 - - wantID string - }{ - "Generate ID from input": {input: "test", wantID: "1190748311"}, - "Generate ID from empty input": {input: "", wantID: "1820012610"}, - "Generate ID from input with upper case characters": {input: "TeSt", wantID: "1190748311"}, - "Generated ID is within the defined range": {input: "test", idMin: 1000, idMax: 2000, wantID: "1008"}, - "Generate ID with minimum ID equal to maximum ID": {input: "test", idMin: 1000, idMax: 1000, wantID: "1000"}, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - t.Parallel() - - if tc.idMin == 0 { - tc.idMin = DefaultConfig.UIDMin - } - if tc.idMax == 0 { - tc.idMax = DefaultConfig.UIDMax - } - - require.Equal(t, tc.wantID, fmt.Sprint(generateID(tc.input, tc.idMin, tc.idMax)), "GenerateID did not return expected value") - }) - } -} diff --git a/internal/users/localentries/errno.go b/internal/users/localentries/errno.go new file mode 100644 index 000000000..8f345f3c7 --- /dev/null +++ b/internal/users/localentries/errno.go @@ -0,0 +1,5 @@ +package localentries + +import "sync" + +var errnoMutex sync.Mutex diff --git a/internal/users/localgroups/export_test.go b/internal/users/localentries/export_test.go similarity index 83% rename from internal/users/localgroups/export_test.go rename to internal/users/localentries/export_test.go index 0a3a2c3e9..45fdd3812 100644 --- a/internal/users/localgroups/export_test.go +++ b/internal/users/localentries/export_test.go @@ -1,4 +1,4 @@ -package localgroups +package localentries // WithGroupPath overrides the default /etc/group path for tests. func WithGroupPath(p string) Option { @@ -15,7 +15,7 @@ func WithGpasswdCmd(cmds []string) Option { } // WithGetUsersFunc overrides the getusers func with a custom one for tests. -func WithGetUsersFunc(getUsersFunc func() []string) Option { +func WithGetUsersFunc(getUsersFunc func() ([]string, error)) Option { return func(o *options) { o.getUsersFunc = getUsersFunc } diff --git a/internal/users/localentries/getgrent_c.go b/internal/users/localentries/getgrent_c.go new file mode 100644 index 000000000..ceb7e93ec --- /dev/null +++ b/internal/users/localentries/getgrent_c.go @@ -0,0 +1,109 @@ +// Package localentries provides functions to access the local user and group database. +// +//nolint:dupl // The tests fail if we remove the duplicate C code. +package localentries + +/* +#include +#include +#include +#include +#include +void unset_errno(void) { + errno = 0; +} + +int get_errno(void) { + return errno; +} +*/ +//#cgo LDFLAGS: -Wl,--allow-multiple-definition +import "C" + +import ( + "errors" + "fmt" + "sync" +) + +// Group represents a group entry. +type Group struct { + Name string + GID uint32 + Passwd string +} + +var getgrentMutex sync.Mutex + +func getGroupEntry() (*C.struct_group, error) { + errnoMutex.Lock() + defer errnoMutex.Unlock() + C.unset_errno() + cGroup := C.getgrent() + if cGroup == nil { + errno := C.get_errno() + // It's not documented in the man page, but apparently getgrent sets errno to ENOENT when there are no more + // entries in the group database. + if errno == C.ENOENT { + return nil, nil + } + if errno != 0 { + return nil, fmt.Errorf("getgrent: %v", C.GoString(C.strerror(errno))) + } + } + return cGroup, nil +} + +// GetGroupEntries returns all group entries. +func GetGroupEntries() ([]Group, error) { + // This function repeatedly calls getgrent, which iterates over the records in the group database. + // Use a mutex to avoid that parallel calls to this function interfere with each other. + getgrentMutex.Lock() + defer getgrentMutex.Unlock() + + C.setgrent() + defer C.endgrent() + + var entries []Group + for { + cGroup, err := getGroupEntry() + if err != nil { + return nil, err + } + if cGroup == nil { + // No more entries in the group database. + break + } + + entries = append(entries, Group{ + Name: C.GoString(cGroup.gr_name), + GID: uint32(cGroup.gr_gid), + Passwd: C.GoString(cGroup.gr_passwd), + }) + } + + return entries, nil +} + +// ErrGroupNotFound is returned when a group is not found. +var ErrGroupNotFound = errors.New("group not found") + +// GetGroupByName returns the group with the given name. +func GetGroupByName(name string) (Group, error) { + C.unset_errno() + cGroup := C.getgrnam(C.CString(name)) + if cGroup == nil { + errno := C.get_errno() + switch errno { + case 0, C.ENOENT, C.ESRCH, C.EBADF, C.EPERM: + return Group{}, ErrGroupNotFound + default: + return Group{}, fmt.Errorf("getgrnam: %v", C.GoString(C.strerror(errno))) + } + } + + return Group{ + Name: C.GoString(cGroup.gr_name), + GID: uint32(cGroup.gr_gid), + }, nil +} diff --git a/internal/users/localentries/getgrent_test.go b/internal/users/localentries/getgrent_test.go new file mode 100644 index 000000000..3c4bec031 --- /dev/null +++ b/internal/users/localentries/getgrent_test.go @@ -0,0 +1,32 @@ +package localentries + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetGroupEntries(t *testing.T) { + t.Parallel() + + got, err := GetGroupEntries() + require.NoError(t, err, "GetGroupEntries should never return an error") + require.NotEmpty(t, got, "GetGroupEntries should never return an empty list") +} + +func TestGetGroupByName(t *testing.T) { + t.Parallel() + + got, err := GetGroupByName("root") + require.NoError(t, err, "GetGroupByName should not return an error") + require.Equal(t, got.Name, "root") + require.Equal(t, got.GID, uint32(0)) +} + +func TestGetGroupByName_NotFound(t *testing.T) { + t.Parallel() + + got, err := GetGroupByName("nonexistent") + require.ErrorIs(t, err, ErrGroupNotFound) + require.Equal(t, got.Name, "") +} diff --git a/internal/users/localentries/getpwent_c.go b/internal/users/localentries/getpwent_c.go new file mode 100644 index 000000000..b5e0f42a6 --- /dev/null +++ b/internal/users/localentries/getpwent_c.go @@ -0,0 +1,109 @@ +// Package localentries provides functions to access local passwd entries. +// +//nolint:dupl // The tests fail if we remove the duplicate C code. +package localentries + +/* #include +#include +#include +#include +#include + +void unset_errno(void) { + errno = 0; +} + +int get_errno(void) { + return errno; +} +*/ +//#cgo LDFLAGS: -Wl,--allow-multiple-definition +import "C" + +import ( + "errors" + "fmt" + "sync" +) + +// Passwd represents a passwd entry. +type Passwd struct { + Name string + UID uint32 + Gecos string +} + +var getpwentMutex sync.Mutex + +func getPasswdEntry() (*C.struct_passwd, error) { + errnoMutex.Lock() + defer errnoMutex.Unlock() + C.unset_errno() + cPasswd := C.getpwent() + if cPasswd == nil { + errno := C.get_errno() + // It's not documented in the man page, but apparently getpwent sets errno to ENOENT when there are no more + // entries in the passwd database. + if errno == C.ENOENT { + return nil, nil + } + if errno != 0 { + return nil, fmt.Errorf("getpwent: %v", C.GoString(C.strerror(errno))) + } + } + return cPasswd, nil +} + +// GetPasswdEntries returns all passwd entries. +func GetPasswdEntries() ([]Passwd, error) { + // This function repeatedly calls getpwent, which iterates over the records in the passwd database. + // Use a mutex to avoid that parallel calls to this function interfere with each other. + getpwentMutex.Lock() + defer getpwentMutex.Unlock() + + C.setpwent() + defer C.endpwent() + + var entries []Passwd + for { + cPasswd, err := getPasswdEntry() + if err != nil { + return nil, err + } + if cPasswd == nil { + // No more entries in the passwd database. + break + } + + entries = append(entries, Passwd{ + Name: C.GoString(cPasswd.pw_name), + UID: uint32(cPasswd.pw_uid), + Gecos: C.GoString(cPasswd.pw_gecos), + }) + } + + return entries, nil +} + +// ErrUserNotFound is returned when a user is not found. +var ErrUserNotFound = errors.New("user not found") + +// GetPasswdByName returns the user with the given name. +func GetPasswdByName(name string) (Passwd, error) { + C.unset_errno() + cPasswd := C.getpwnam(C.CString(name)) + if cPasswd == nil { + errno := C.get_errno() + switch errno { + case 0, C.ENOENT, C.ESRCH, C.EBADF, C.EPERM: + return Passwd{}, ErrUserNotFound + default: + return Passwd{}, fmt.Errorf("getpwnam: %v", C.GoString(C.strerror(errno))) + } + } + + return Passwd{ + Name: C.GoString(cPasswd.pw_name), + UID: uint32(cPasswd.pw_uid), + }, nil +} diff --git a/internal/users/localentries/getpwent_test.go b/internal/users/localentries/getpwent_test.go new file mode 100644 index 000000000..b70bd893f --- /dev/null +++ b/internal/users/localentries/getpwent_test.go @@ -0,0 +1,39 @@ +package localentries + +import ( + "slices" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetPasswdEntries(t *testing.T) { + t.Parallel() + + got, err := GetPasswdEntries() + require.NoError(t, err, "GetPasswdEntries should never return an error") + require.NotEmpty(t, got, "GetPasswdEntries should never return an empty list") + + // Check if the root user is present in the list + rootFound := slices.ContainsFunc(got, func(entry Passwd) bool { + return entry.Name == "root" + }) + require.True(t, rootFound, "GetPasswdEntries should always return root") +} + +func TestGetPasswdByName(t *testing.T) { + t.Parallel() + + got, err := GetPasswdByName("root") + require.NoError(t, err, "GetPasswdByName should not return an error") + require.Equal(t, got.Name, "root") + require.Equal(t, got.UID, uint32(0)) +} + +func TestGetPasswdByName_NotFound(t *testing.T) { + t.Parallel() + + got, err := GetPasswdByName("nonexistent") + require.ErrorIs(t, err, ErrUserNotFound) + require.Equal(t, got.Name, "") +} diff --git a/internal/users/localgroups/localgroups.go b/internal/users/localentries/localgroups.go similarity index 91% rename from internal/users/localgroups/localgroups.go rename to internal/users/localentries/localgroups.go index f9e9b5aee..a0bf6d4a4 100644 --- a/internal/users/localgroups/localgroups.go +++ b/internal/users/localentries/localgroups.go @@ -1,5 +1,5 @@ -// Package localgroups handles the synchronization of local groups the users. -package localgroups +// Package localentries provides functions to retrieve passwd and group entries and to update the groups of a user. +package localentries import ( "bufio" @@ -26,7 +26,7 @@ var defaultOptions = options{ type options struct { groupPath string gpasswdCmd []string - getUsersFunc func() []string + getUsersFunc func() ([]string, error) } // Option represents an optional function to override UpdateLocalGroups default values. @@ -76,6 +76,20 @@ func Update(username string, newGroups []string, oldGroups []string, args ...Opt return nil } +// getPasswdUsernames gets the passwd entries and returns their usernames. +func getPasswdUsernames() ([]string, error) { + var usernames []string + entries, err := GetPasswdEntries() + if err != nil { + return nil, err + } + for _, e := range entries { + usernames = append(usernames, e.Name) + } + + return usernames, nil +} + // existingLocalGroups returns which groups from groupPath the user is part of. func existingLocalGroups(user, groupPath string) (groups []string, err error) { defer decorate.OnError(&err, "could not fetch existing local group") @@ -159,7 +173,11 @@ func Clean(args ...Option) (err error) { // Add the existingUsers to a map to speed up search existingUsers := make(map[string]struct{}) - for _, username := range opts.getUsersFunc() { + usernames, err := opts.getUsersFunc() + if err != nil { + return err + } + for _, username := range usernames { existingUsers[username] = struct{}{} } // If no username was returned, something probably went wrong during the getpwent call and we should stop, diff --git a/internal/users/localgroups/localgroups_test.go b/internal/users/localentries/localgroups_test.go similarity index 80% rename from internal/users/localgroups/localgroups_test.go rename to internal/users/localentries/localgroups_test.go index 9236bbf68..0bab732d2 100644 --- a/internal/users/localgroups/localgroups_test.go +++ b/internal/users/localentries/localgroups_test.go @@ -1,4 +1,4 @@ -package localgroups_test +package localentries_test import ( "os" @@ -7,11 +7,11 @@ import ( "github.com/stretchr/testify/require" "github.com/ubuntu/authd/internal/testutils/golden" - "github.com/ubuntu/authd/internal/users/localgroups" - localgroupstestutils "github.com/ubuntu/authd/internal/users/localgroups/testutils" + "github.com/ubuntu/authd/internal/users/localentries" + localentriestestutils "github.com/ubuntu/authd/internal/users/localentries/testutils" ) -func TestUpdateLocalGroups(t *testing.T) { +func TestUpdatelocalentries(t *testing.T) { t.Parallel() tests := map[string]struct { @@ -79,19 +79,19 @@ func TestUpdateLocalGroups(t *testing.T) { groupFilePath, destCmdsFile, } - err := localgroups.Update(tc.username, tc.newGroups, tc.oldGroups, localgroups.WithGroupPath(groupFilePath), localgroups.WithGpasswdCmd(cmdArgs)) + err := localentries.Update(tc.username, tc.newGroups, tc.oldGroups, localentries.WithGroupPath(groupFilePath), localentries.WithGpasswdCmd(cmdArgs)) if tc.wantErr { - require.Error(t, err, "UpdateLocalGroups should have failed") + require.Error(t, err, "Updatelocalentries should have failed") } else { - require.NoError(t, err, "UpdateLocalGroups should not have failed") + require.NoError(t, err, "Updatelocalentries should not have failed") } - localgroupstestutils.RequireGPasswdOutput(t, destCmdsFile, golden.Path(t)) + localentriestestutils.RequireGPasswdOutput(t, destCmdsFile, golden.Path(t)) }) } } -func TestCleanLocalGroups(t *testing.T) { +func TestCleanlocalentries(t *testing.T) { t.Parallel() tests := map[string]struct { @@ -127,24 +127,24 @@ func TestCleanLocalGroups(t *testing.T) { tc.getUsersReturn = []string{"myuser", "otheruser", "otheruser2", "otheruser3", "otheruser4"} } - cleanupOptions := []localgroups.Option{ - localgroups.WithGpasswdCmd(gpasswdCmd), - localgroups.WithGroupPath(groupFilePath), - localgroups.WithGetUsersFunc(func() []string { return tc.getUsersReturn }), + cleanupOptions := []localentries.Option{ + localentries.WithGpasswdCmd(gpasswdCmd), + localentries.WithGroupPath(groupFilePath), + localentries.WithGetUsersFunc(func() ([]string, error) { return tc.getUsersReturn, nil }), } - err := localgroups.Clean(cleanupOptions...) + err := localentries.Clean(cleanupOptions...) if tc.wantErr { - require.Error(t, err, "CleanupLocalGroups should have failed") + require.Error(t, err, "Cleanuplocalentries should have failed") } else { - require.NoError(t, err, "CleanupLocalGroups should not have failed") + require.NoError(t, err, "Cleanuplocalentries should not have failed") } - localgroupstestutils.RequireGPasswdOutput(t, destCmdsFile, golden.Path(t)) + localentriestestutils.RequireGPasswdOutput(t, destCmdsFile, golden.Path(t)) }) } } -func TestCleanUserFromLocalGroups(t *testing.T) { +func TestCleanUserFromlocalentries(t *testing.T) { t.Parallel() tests := map[string]struct { @@ -184,22 +184,22 @@ func TestCleanUserFromLocalGroups(t *testing.T) { gpasswdCmd = append(gpasswdCmd, "gpasswdfail") } - cleanupOptions := []localgroups.Option{ - localgroups.WithGpasswdCmd(gpasswdCmd), - localgroups.WithGroupPath(groupFilePath), + cleanupOptions := []localentries.Option{ + localentries.WithGpasswdCmd(gpasswdCmd), + localentries.WithGroupPath(groupFilePath), } - err := localgroups.CleanUser(tc.username, cleanupOptions...) + err := localentries.CleanUser(tc.username, cleanupOptions...) if tc.wantErr { - require.Error(t, err, "CleanUserFromLocalGroups should have failed") + require.Error(t, err, "CleanUserFromlocalentries should have failed") } else { - require.NoError(t, err, "CleanUserFromLocalGroups should not have failed") + require.NoError(t, err, "CleanUserFromlocalentries should not have failed") } - localgroupstestutils.RequireGPasswdOutput(t, destCmdsFile, golden.Path(t)) + localentriestestutils.RequireGPasswdOutput(t, destCmdsFile, golden.Path(t)) }) } } func TestMockgpasswd(t *testing.T) { - localgroupstestutils.Mockgpasswd(t) + localentriestestutils.Mockgpasswd(t) } diff --git a/internal/users/localgroups/testdata/empty_line.group b/internal/users/localentries/testdata/empty_line.group similarity index 100% rename from internal/users/localgroups/testdata/empty_line.group rename to internal/users/localentries/testdata/empty_line.group diff --git a/internal/users/localentries/testdata/golden/TestCleanUserFromlocalentries/Cleans_up_user_from_group b/internal/users/localentries/testdata/golden/TestCleanUserFromlocalentries/Cleans_up_user_from_group new file mode 100644 index 000000000..cf402a20b --- /dev/null +++ b/internal/users/localentries/testdata/golden/TestCleanUserFromlocalentries/Cleans_up_user_from_group @@ -0,0 +1 @@ +--delete myuser localgroup1 \ No newline at end of file diff --git a/internal/users/localentries/testdata/golden/TestCleanUserFromlocalentries/Cleans_up_user_from_multiple_groups b/internal/users/localentries/testdata/golden/TestCleanUserFromlocalentries/Cleans_up_user_from_multiple_groups new file mode 100644 index 000000000..cf2886bdf --- /dev/null +++ b/internal/users/localentries/testdata/golden/TestCleanUserFromlocalentries/Cleans_up_user_from_multiple_groups @@ -0,0 +1,2 @@ +--delete myuser localgroup1 +--delete myuser localgroup2 \ No newline at end of file diff --git a/internal/users/localentries/testdata/golden/TestCleanlocalentries/Cleans_up_multiple_users_from_group b/internal/users/localentries/testdata/golden/TestCleanlocalentries/Cleans_up_multiple_users_from_group new file mode 100644 index 000000000..541c95018 --- /dev/null +++ b/internal/users/localentries/testdata/golden/TestCleanlocalentries/Cleans_up_multiple_users_from_group @@ -0,0 +1,3 @@ +--delete inactiveuser localgroup1 +--delete inactiveuser2 localgroup1 +--delete inactiveuser3 localgroup1 \ No newline at end of file diff --git a/internal/users/localentries/testdata/golden/TestCleanlocalentries/Cleans_up_multiple_users_from_multiple_groups b/internal/users/localentries/testdata/golden/TestCleanlocalentries/Cleans_up_multiple_users_from_multiple_groups new file mode 100644 index 000000000..3be8a9f0f --- /dev/null +++ b/internal/users/localentries/testdata/golden/TestCleanlocalentries/Cleans_up_multiple_users_from_multiple_groups @@ -0,0 +1,6 @@ +--delete inactiveuser localgroup2 +--delete inactiveuser localgroup3 +--delete inactiveuser2 localgroup1 +--delete inactiveuser2 localgroup3 +--delete inactiveuser3 localgroup1 +--delete inactiveuser3 localgroup2 \ No newline at end of file diff --git a/internal/users/localentries/testdata/golden/TestCleanlocalentries/Cleans_up_user_from_group b/internal/users/localentries/testdata/golden/TestCleanlocalentries/Cleans_up_user_from_group new file mode 100644 index 000000000..1cb5f6df8 --- /dev/null +++ b/internal/users/localentries/testdata/golden/TestCleanlocalentries/Cleans_up_user_from_group @@ -0,0 +1 @@ +--delete inactiveuser localgroup1 \ No newline at end of file diff --git a/internal/users/localentries/testdata/golden/TestCleanlocalentries/Cleans_up_user_from_multiple_groups b/internal/users/localentries/testdata/golden/TestCleanlocalentries/Cleans_up_user_from_multiple_groups new file mode 100644 index 000000000..f792ff028 --- /dev/null +++ b/internal/users/localentries/testdata/golden/TestCleanlocalentries/Cleans_up_user_from_multiple_groups @@ -0,0 +1,3 @@ +--delete inactiveuser localgroup1 +--delete inactiveuser localgroup3 +--delete inactiveuser localgroup4 \ No newline at end of file diff --git a/internal/users/localentries/testdata/golden/TestCleanlocalentries/Error_on_any_unignored_delete_gpasswd_error b/internal/users/localentries/testdata/golden/TestCleanlocalentries/Error_on_any_unignored_delete_gpasswd_error new file mode 100644 index 000000000..dac35797d --- /dev/null +++ b/internal/users/localentries/testdata/golden/TestCleanlocalentries/Error_on_any_unignored_delete_gpasswd_error @@ -0,0 +1,5 @@ +--delete cloudgroup1 +--delete cloudgroup2 +--delete localgroup1 +--delete localgroup3 +--delete localgroup4 \ No newline at end of file diff --git a/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Add_and_remove_user_from_multiple_groups_with_one_remaining b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Add_and_remove_user_from_multiple_groups_with_one_remaining new file mode 100644 index 000000000..80dd17cde --- /dev/null +++ b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Add_and_remove_user_from_multiple_groups_with_one_remaining @@ -0,0 +1 @@ +--add myuser localgroup3 \ No newline at end of file diff --git a/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Group_file_with_empty_line_is_ignored b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Group_file_with_empty_line_is_ignored new file mode 100644 index 000000000..48073e4af --- /dev/null +++ b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Group_file_with_empty_line_is_ignored @@ -0,0 +1,2 @@ +--add myuser localgroup1 +--add myuser localgroup3 \ No newline at end of file diff --git a/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Insert_new_user_in_existing_files_with_multiple_other_users_in_our_group b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Insert_new_user_in_existing_files_with_multiple_other_users_in_our_group new file mode 100644 index 000000000..48073e4af --- /dev/null +++ b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Insert_new_user_in_existing_files_with_multiple_other_users_in_our_group @@ -0,0 +1,2 @@ +--add myuser localgroup1 +--add myuser localgroup3 \ No newline at end of file diff --git a/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Insert_new_user_in_existing_files_with_no_users_in_our_group b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Insert_new_user_in_existing_files_with_no_users_in_our_group new file mode 100644 index 000000000..48073e4af --- /dev/null +++ b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Insert_new_user_in_existing_files_with_no_users_in_our_group @@ -0,0 +1,2 @@ +--add myuser localgroup1 +--add myuser localgroup3 \ No newline at end of file diff --git a/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Insert_new_user_in_existing_files_with_other_users_in_our_group b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Insert_new_user_in_existing_files_with_other_users_in_our_group new file mode 100644 index 000000000..48073e4af --- /dev/null +++ b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Insert_new_user_in_existing_files_with_other_users_in_our_group @@ -0,0 +1,2 @@ +--add myuser localgroup1 +--add myuser localgroup3 \ No newline at end of file diff --git a/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Insert_new_user_when_no_users_in_any_group b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Insert_new_user_when_no_users_in_any_group new file mode 100644 index 000000000..48073e4af --- /dev/null +++ b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Insert_new_user_when_no_users_in_any_group @@ -0,0 +1,2 @@ +--add myuser localgroup1 +--add myuser localgroup3 \ No newline at end of file diff --git a/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Insert_user_in_the_only_local_group_when_not_present b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Insert_user_in_the_only_local_group_when_not_present new file mode 100644 index 000000000..80dd17cde --- /dev/null +++ b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Insert_user_in_the_only_local_group_when_not_present @@ -0,0 +1 @@ +--add myuser localgroup3 \ No newline at end of file diff --git a/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Insert_user_in_the_only_local_group_when_not_present_even_with_multiple b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Insert_user_in_the_only_local_group_when_not_present_even_with_multiple new file mode 100644 index 000000000..80dd17cde --- /dev/null +++ b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Insert_user_in_the_only_local_group_when_not_present_even_with_multiple @@ -0,0 +1 @@ +--add myuser localgroup3 \ No newline at end of file diff --git a/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Missing_group_is_ignored b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Missing_group_is_ignored new file mode 100644 index 000000000..0dd812dc8 --- /dev/null +++ b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/Missing_group_is_ignored @@ -0,0 +1 @@ +--add myuser localgroup1 \ No newline at end of file diff --git a/internal/users/localentries/testdata/golden/TestUpdatelocalentries/User_is_added_to_group_they_were_added_to_before b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/User_is_added_to_group_they_were_added_to_before new file mode 100644 index 000000000..0dd812dc8 --- /dev/null +++ b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/User_is_added_to_group_they_were_added_to_before @@ -0,0 +1 @@ +--add myuser localgroup1 \ No newline at end of file diff --git a/internal/users/localentries/testdata/golden/TestUpdatelocalentries/User_is_removed_from_old_groups_but_not_from_other_groups b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/User_is_removed_from_old_groups_but_not_from_other_groups new file mode 100644 index 000000000..e49d0784f --- /dev/null +++ b/internal/users/localentries/testdata/golden/TestUpdatelocalentries/User_is_removed_from_old_groups_but_not_from_other_groups @@ -0,0 +1 @@ +--delete myuser localgroup3 \ No newline at end of file diff --git a/internal/users/localgroups/testdata/gpasswdfail_in_deleted_group.group b/internal/users/localentries/testdata/gpasswdfail_in_deleted_group.group similarity index 100% rename from internal/users/localgroups/testdata/gpasswdfail_in_deleted_group.group rename to internal/users/localentries/testdata/gpasswdfail_in_deleted_group.group diff --git a/internal/users/localgroups/testdata/inactive_user_in_many_groups.group b/internal/users/localentries/testdata/inactive_user_in_many_groups.group similarity index 100% rename from internal/users/localgroups/testdata/inactive_user_in_many_groups.group rename to internal/users/localentries/testdata/inactive_user_in_many_groups.group diff --git a/internal/users/localgroups/testdata/inactive_user_in_one_group.group b/internal/users/localentries/testdata/inactive_user_in_one_group.group similarity index 100% rename from internal/users/localgroups/testdata/inactive_user_in_one_group.group rename to internal/users/localentries/testdata/inactive_user_in_one_group.group diff --git a/internal/users/localgroups/testdata/inactive_users_in_many_groups.group b/internal/users/localentries/testdata/inactive_users_in_many_groups.group similarity index 100% rename from internal/users/localgroups/testdata/inactive_users_in_many_groups.group rename to internal/users/localentries/testdata/inactive_users_in_many_groups.group diff --git a/internal/users/localgroups/testdata/inactive_users_in_one_group.group b/internal/users/localentries/testdata/inactive_users_in_one_group.group similarity index 100% rename from internal/users/localgroups/testdata/inactive_users_in_one_group.group rename to internal/users/localentries/testdata/inactive_users_in_one_group.group diff --git a/internal/users/localgroups/testdata/malformed_file.group b/internal/users/localentries/testdata/malformed_file.group similarity index 100% rename from internal/users/localgroups/testdata/malformed_file.group rename to internal/users/localentries/testdata/malformed_file.group diff --git a/internal/users/localgroups/testdata/missing_group.group b/internal/users/localentries/testdata/missing_group.group similarity index 100% rename from internal/users/localgroups/testdata/missing_group.group rename to internal/users/localentries/testdata/missing_group.group diff --git a/internal/users/localgroups/testdata/multiple_users_in_our_groups.group b/internal/users/localentries/testdata/multiple_users_in_our_groups.group similarity index 100% rename from internal/users/localgroups/testdata/multiple_users_in_our_groups.group rename to internal/users/localentries/testdata/multiple_users_in_our_groups.group diff --git a/internal/users/localgroups/testdata/no_users.group b/internal/users/localentries/testdata/no_users.group similarity index 100% rename from internal/users/localgroups/testdata/no_users.group rename to internal/users/localentries/testdata/no_users.group diff --git a/internal/users/localgroups/testdata/no_users_in_our_groups.group b/internal/users/localentries/testdata/no_users_in_our_groups.group similarity index 100% rename from internal/users/localgroups/testdata/no_users_in_our_groups.group rename to internal/users/localentries/testdata/no_users_in_our_groups.group diff --git a/internal/users/localgroups/testdata/user_and_others_in_one_groups.group b/internal/users/localentries/testdata/user_and_others_in_one_groups.group similarity index 100% rename from internal/users/localgroups/testdata/user_and_others_in_one_groups.group rename to internal/users/localentries/testdata/user_and_others_in_one_groups.group diff --git a/internal/users/localgroups/testdata/user_in_both_groups.group b/internal/users/localentries/testdata/user_in_both_groups.group similarity index 100% rename from internal/users/localgroups/testdata/user_in_both_groups.group rename to internal/users/localentries/testdata/user_in_both_groups.group diff --git a/internal/users/localgroups/testdata/user_in_many_groups.group b/internal/users/localentries/testdata/user_in_many_groups.group similarity index 100% rename from internal/users/localgroups/testdata/user_in_many_groups.group rename to internal/users/localentries/testdata/user_in_many_groups.group diff --git a/internal/users/localgroups/testdata/user_in_one_group.group b/internal/users/localentries/testdata/user_in_one_group.group similarity index 100% rename from internal/users/localgroups/testdata/user_in_one_group.group rename to internal/users/localentries/testdata/user_in_one_group.group diff --git a/internal/users/localgroups/testdata/user_in_second_local_group.group b/internal/users/localentries/testdata/user_in_second_local_group.group similarity index 100% rename from internal/users/localgroups/testdata/user_in_second_local_group.group rename to internal/users/localentries/testdata/user_in_second_local_group.group diff --git a/internal/users/localgroups/testdata/user_in_second_local_group_with_others.group b/internal/users/localentries/testdata/user_in_second_local_group_with_others.group similarity index 100% rename from internal/users/localgroups/testdata/user_in_second_local_group_with_others.group rename to internal/users/localentries/testdata/user_in_second_local_group_with_others.group diff --git a/internal/users/localgroups/testdata/users_in_our_groups.group b/internal/users/localentries/testdata/users_in_our_groups.group similarity index 100% rename from internal/users/localgroups/testdata/users_in_our_groups.group rename to internal/users/localentries/testdata/users_in_our_groups.group diff --git a/internal/users/localgroups/testutils.go b/internal/users/localentries/testutils.go similarity index 98% rename from internal/users/localgroups/testutils.go rename to internal/users/localentries/testutils.go index 834aabd0e..0ef7edee0 100644 --- a/internal/users/localgroups/testutils.go +++ b/internal/users/localentries/testutils.go @@ -1,4 +1,4 @@ -package localgroups +package localentries import "github.com/ubuntu/authd/internal/testsdetection" diff --git a/internal/users/localgroups/testutils/gpasswd.go b/internal/users/localentries/testutils/gpasswd.go similarity index 94% rename from internal/users/localgroups/testutils/gpasswd.go rename to internal/users/localentries/testutils/gpasswd.go index d205d5d4a..8df17f54c 100644 --- a/internal/users/localgroups/testutils/gpasswd.go +++ b/internal/users/localentries/testutils/gpasswd.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/ubuntu/authd/internal/testutils/golden" - "github.com/ubuntu/authd/internal/users/localgroups" + "github.com/ubuntu/authd/internal/users/localentries" ) // Mockgpasswd is the gpasswd mock. @@ -73,12 +73,12 @@ func Mockgpasswd(_ *testing.T) { func SetupGPasswdMock(t *testing.T, groupsFilePath string) string { t.Helper() - t.Cleanup(localgroups.Z_ForTests_RestoreDefaultOptions) + t.Cleanup(localentries.Z_ForTests_RestoreDefaultOptions) - localgroups.Z_ForTests_SetGroupPath(groupsFilePath) + localentries.Z_ForTests_SetGroupPath(groupsFilePath) destCmdsFile := filepath.Join(t.TempDir(), "gpasswd.output") - localgroups.Z_ForTests_SetGpasswdCmd([]string{"env", "GO_WANT_HELPER_PROCESS=1", + localentries.Z_ForTests_SetGpasswdCmd([]string{"env", "GO_WANT_HELPER_PROCESS=1", os.Args[0], "-test.run=TestMockgpasswd", "--", groupsFilePath, destCmdsFile, }) diff --git a/internal/users/localentries/testutils/localgroups.go b/internal/users/localentries/testutils/localgroups.go new file mode 100644 index 000000000..b63cd27e2 --- /dev/null +++ b/internal/users/localentries/testutils/localgroups.go @@ -0,0 +1,38 @@ +// Package localgrouptestutils export users test functionalities used by other packages to change cmdline and group file. +package localgrouptestutils + +//nolint:gci // We import unsafe as it is needed for go:linkname, but the nolint comment confuses gofmt and it adds +// a blank space between the imports, which creates problems with gci so we need to ignore it. +import ( + + //nolint:revive,nolintlint // needed for go:linkname, but only used in tests. nolintlint as false positive then. + _ "unsafe" + + "github.com/ubuntu/authd/internal/testsdetection" +) + +func init() { + // No import outside of testing environment. + testsdetection.MustBeTesting() +} + +var ( + //go:linkname defaultOptions github.com/ubuntu/authd/internal/users/localentries.defaultOptions + defaultOptions struct { + groupPath string + gpasswdCmd []string + getUsersFunc func() []string + } +) + +// SetGroupPath sets the groupPath for the defaultOptions. +// Tests using this can't be run in parallel. +func SetGroupPath(groupPath string) { + defaultOptions.groupPath = groupPath +} + +// SetGpasswdCmd sets the gpasswdCmd for the defaultOptions. +// Tests using this can't be run in parallel. +func SetGpasswdCmd(gpasswdCmd []string) { + defaultOptions.gpasswdCmd = gpasswdCmd +} diff --git a/internal/users/localgroups/getpwent_c.go b/internal/users/localgroups/getpwent_c.go deleted file mode 100644 index 60a17bb99..000000000 --- a/internal/users/localgroups/getpwent_c.go +++ /dev/null @@ -1,23 +0,0 @@ -package localgroups - -// #include -// #include -import "C" - -// getPasswdUsernames gets the list of users using `getpwent` and returns their usernames. -func getPasswdUsernames() []string { - C.setpwent() - defer C.endpwent() - - var entries []string - for { - cPasswd := C.getpwent() - if cPasswd == nil { - break - } - - entries = append(entries, C.GoString(cPasswd.pw_name)) - } - - return entries -} diff --git a/internal/users/localgroups/getpwent_test.go b/internal/users/localgroups/getpwent_test.go deleted file mode 100644 index 7b671ac08..000000000 --- a/internal/users/localgroups/getpwent_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package localgroups - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestGetPasswdUsernames(t *testing.T) { - t.Parallel() - - got := getPasswdUsernames() - require.NotEmpty(t, got, "GetPasswdUsernames should never return an empty list") - require.Contains(t, got, "root", "GetPasswdUsernames should always return root") -} diff --git a/internal/users/manager.go b/internal/users/manager.go index 35398122e..75f143d4c 100644 --- a/internal/users/manager.go +++ b/internal/users/manager.go @@ -3,17 +3,18 @@ package users import ( "context" - "crypto/sha256" - "encoding/binary" "errors" "fmt" "os" - "strings" + "sync" "syscall" "github.com/ubuntu/authd/internal/log" "github.com/ubuntu/authd/internal/users/cache" - "github.com/ubuntu/authd/internal/users/localgroups" + "github.com/ubuntu/authd/internal/users/idgenerator" + "github.com/ubuntu/authd/internal/users/localentries" + "github.com/ubuntu/authd/internal/users/tempentries" + "github.com/ubuntu/authd/internal/users/types" "github.com/ubuntu/decorate" ) @@ -35,24 +36,62 @@ var DefaultConfig = Config{ // Manager is the manager for any user related operation. type Manager struct { - cache *cache.Cache - config Config + cache *cache.Cache + config Config + temporaryRecords *tempentries.TemporaryRecords + updateUserMu sync.Mutex +} + +type options struct { + idGenerator tempentries.IDGenerator +} + +// Option is a function that allows changing some of the default behaviors of the manager. +type Option func(*options) + +// WithIDGenerator makes the manager use a specific ID generator. +// This option is only useful in tests. +func WithIDGenerator(g tempentries.IDGenerator) Option { + return func(o *options) { + o.idGenerator = g + } } // NewManager creates a new user manager. -func NewManager(config Config, cacheDir string) (m *Manager, err error) { - log.Debugf(context.TODO(), "Creating user manager with config: %+v", config) +func NewManager(config Config, cacheDir string, args ...Option) (m *Manager, err error) { + log.Debugf(context.Background(), "Creating user manager with config: %+v", config) - // Check that the ID ranges are valid. - if config.UIDMin >= config.UIDMax { - return nil, errors.New("UID_MIN must be less than UID_MAX") + opts := &options{} + for _, arg := range args { + arg(opts) } - if config.GIDMin >= config.GIDMax { - return nil, errors.New("GID_MIN must be less than GID_MAX") + + if opts.idGenerator == nil { + // Check that the ID ranges are valid. + if config.UIDMin >= config.UIDMax { + return nil, errors.New("UID_MIN must be less than UID_MAX") + } + if config.GIDMin >= config.GIDMax { + return nil, errors.New("GID_MIN must be less than GID_MAX") + } + // Check that the number of possible UIDs is at least twice the number of possible pre-auth users. + numUIDs := config.UIDMax - config.UIDMin + minNumUIDs := uint32(tempentries.MaxPreAuthUsers * 2) + if numUIDs < minNumUIDs { + return nil, fmt.Errorf("UID range configured via UID_MIN and UID_MAX is too small (%d), must be at least %d", numUIDs, minNumUIDs) + } + + opts.idGenerator = &idgenerator.IDGenerator{ + UIDMin: config.UIDMin, + UIDMax: config.UIDMax, + GIDMin: config.GIDMin, + GIDMax: config.GIDMax, + } } m = &Manager{ - config: config, + config: config, + temporaryRecords: tempentries.NewTemporaryRecords(opts.idGenerator), } c, err := cache.New(cacheDir) @@ -70,40 +109,59 @@ func (m *Manager) Stop() error { } // UpdateUser updates the user information in the cache. -func (m *Manager) UpdateUser(u UserInfo) (err error) { +func (m *Manager) UpdateUser(u types.UserInfo) (err error) { defer decorate.OnError(&err, "failed to update user %q", u.Name) if u.Name == "" { return errors.New("empty username") } + var uid uint32 + + // Prevent a TOCTOU race condition between the check for existence in our database and the registration of the + // temporary user/group records. This does not prevent a race condition where a user is created by some other NSS + // source, but that is handled in the temporaryRecords.RegisterUser and temporaryRecords.RegisterGroup functions. + m.updateUserMu.Lock() + defer m.updateUserMu.Unlock() + // Check if the user already exists in the database oldUser, err := m.cache.UserByName(u.Name) if err != nil && !errors.Is(err, cache.NoDataFoundError{}) { - return err - } - // Keep the old UID if the user already exists in the database, to avoid permission issues with the user's home - // directory and other files. - if !errors.Is(err, cache.NoDataFoundError{}) { - u.UID = oldUser.UID - } + return fmt.Errorf("could not get user %q: %w", u.Name, err) + } + if errors.Is(err, cache.NoDataFoundError{}) { + // The user does not exist, so we generate a unique UID for it. To avoid that a user with the same UID is + // created by some other NSS source, this also registers a temporary user in our NSS handler. We remove that + // temporary user before returning from this function, at which point the user is added to the database (so we + // don't need the temporary user anymore to keep the UID unique). + var cleanup func() error + uid, cleanup, err = m.temporaryRecords.RegisterUser(u.Name) + if err != nil { + return fmt.Errorf("could not register user %q: %w", u.Name, err) + } - // Generate the UID of the user unless a UID is already set. - if u.UID == 0 { - u.UID = m.GenerateUID(u.Name) + defer func() { + if cleanupErr := cleanup(); cleanupErr != nil { + err = errors.Join(err, cleanupErr) + } + }() + } else { + uid = oldUser.UID } // Prepend the user private group - u.Groups = append([]GroupInfo{{Name: u.Name, UGID: u.Name}}, u.Groups...) + u.Groups = append([]types.GroupInfo{{Name: u.Name, UGID: u.Name}}, u.Groups...) - // Generate the GIDs of the user groups + var authdGroups []cache.GroupDB + var localGroups []string for i, g := range u.Groups { if g.Name == "" { return fmt.Errorf("empty group name for user %q", u.Name) } if g.UGID == "" { - // An empty UGID means that the group is a local group, so we don't need to store a GID for it. + // An empty UGID means that the group is local. + localGroups = append(localGroups, g.Name) continue } @@ -119,41 +177,46 @@ func (m *Manager) UpdateUser(u UserInfo) (err error) { u.Groups[i].GID = &oldGroup.GID } - // Generate the GID of the group unless a GID is already set. - if u.Groups[i].GID == nil || *u.Groups[i].GID == 0 { - gidv := m.GenerateGID(u.Groups[i].UGID) - u.Groups[i].GID = &gidv + if g.GID == nil { + // The group does not exist in the database, so we generate a unique GID for it. Similar to the RegisterUser + // call above, this also registers a temporary group in our NSS handler. We remove that temporary group + // before returning from this function, at which point the group is added to the database (so we don't need + // the temporary group anymore to keep the GID unique). + gid, cleanup, err := m.temporaryRecords.RegisterGroup(g.Name) + if err != nil { + return fmt.Errorf("could not generate GID for group %q: %v", g.Name, err) + } + + defer func() { + cleanupErr := cleanup() + if cleanupErr != nil { + err = errors.Join(err, fmt.Errorf("could not remove temporary group %q: %v", g.Name, cleanupErr)) + } + }() + + g.GID = &gid } - } - var authdGroups []cache.GroupDB - var localGroups []string - for _, g := range u.Groups { - // Empty UGID means it is a local group - if g.UGID == "" { - localGroups = append(localGroups, g.Name) - continue - } authdGroups = append(authdGroups, cache.NewGroupDB(g.Name, *g.GID, g.UGID, nil)) } - oldLocalGroups, err := m.cache.UserLocalGroups(u.UID) + oldLocalGroups, err := m.cache.UserLocalGroups(uid) if err != nil && !errors.Is(err, cache.NoDataFoundError{}) { return err } // Update user information in the cache. - userDB := cache.NewUserDB(u.Name, u.UID, *u.Groups[0].GID, u.Gecos, u.Dir, u.Shell) + userDB := cache.NewUserDB(u.Name, uid, authdGroups[0].GID, u.Gecos, u.Dir, u.Shell) if err := m.cache.UpdateUserEntry(userDB, authdGroups, localGroups); err != nil { return err } // Update local groups. - if err := localgroups.Update(u.Name, localGroups, oldLocalGroups); err != nil { - return errors.Join(err, m.cache.DeleteUser(u.UID)) + if err := localentries.Update(u.Name, localGroups, oldLocalGroups); err != nil { + return errors.Join(err, m.cache.DeleteUser(uid)) } - if err = checkHomeDirOwnership(u); err != nil { + if err = checkHomeDirOwnership(userDB.Dir, userDB.UID, userDB.GID); err != nil { return fmt.Errorf("failed to check home directory owner and group: %w", err) } @@ -162,8 +225,8 @@ func (m *Manager) UpdateUser(u UserInfo) (err error) { // checkHomeDirOwnership checks if the home directory of the user is owned by the user and the user's group. // If not, it logs a warning. -func checkHomeDirOwnership(u UserInfo) error { - fileInfo, err := os.Stat(u.Dir) +func checkHomeDirOwnership(home string, uid, gid uint32) error { + fileInfo, err := os.Stat(home) if err != nil && !errors.Is(err, os.ErrNotExist) { return err } @@ -177,14 +240,13 @@ func checkHomeDirOwnership(u UserInfo) error { return errors.New("failed to get file info") } oldUID, oldGID := sys.Uid, sys.Gid - newUID, newGID := u.UID, *u.Groups[0].GID // Check if the home directory is owned by the user. - if oldUID != newUID { - log.Warningf(context.TODO(), "Home directory %q is not owned by UID %d. To fix this, run `sudo chown -R --from=%d %d %s`.", u.Dir, oldUID, oldUID, newUID, u.Dir) + if oldUID != uid { + log.Warningf(context.Background(), "Home directory %q is not owned by UID %d. To fix this, run `sudo chown -R --from=%d %d %s`.", home, oldUID, oldUID, uid, home) } - if oldGID != newGID { - log.Warningf(context.TODO(), "Home directory %q is not owned by GID %d. To fix this, run `sudo chown -R --from=:%d :%d %s`.", u.Dir, oldGID, oldGID, newGID, u.Dir) + if oldGID != gid { + log.Warningf(context.Background(), "Home directory %q is not owned by GID %d. To fix this, run `sudo chown -R --from=:%d :%d %s`.", home, oldGID, oldGID, gid, home) } return nil @@ -213,31 +275,42 @@ func (m *Manager) UpdateBrokerForUser(username, brokerID string) error { } // UserByName returns the user information for the given user name. -func (m *Manager) UserByName(username string) (UserEntry, error) { +func (m *Manager) UserByName(username string) (types.UserEntry, error) { usr, err := m.cache.UserByName(username) + if errors.Is(err, cache.NoDataFoundError{}) { + // Check if the user is a temporary user. + return m.temporaryRecords.UserByName(username) + } if err != nil { - return UserEntry{}, err + return types.UserEntry{}, err } return userEntryFromUserDB(usr), nil } // UserByID returns the user information for the given user ID. -func (m *Manager) UserByID(uid uint32) (UserEntry, error) { +func (m *Manager) UserByID(uid uint32) (types.UserEntry, error) { usr, err := m.cache.UserByID(uid) + if errors.Is(err, cache.NoDataFoundError{}) { + // Check if the user is a temporary user. + return m.temporaryRecords.UserByID(uid) + } if err != nil { - return UserEntry{}, err + return types.UserEntry{}, err } return userEntryFromUserDB(usr), nil } // AllUsers returns all users. -func (m *Manager) AllUsers() ([]UserEntry, error) { +func (m *Manager) AllUsers() ([]types.UserEntry, error) { + // TODO: I'm not sure if we should return temporary users here. On the one hand, they are usually not interesting to + // the user and would clutter the output of `getent passwd`. On the other hand, it might be surprising that some + // users are not returned by `getent passwd` and some apps might rely on all users being returned. usrs, err := m.cache.AllUsers() if err != nil { return nil, err } - var usrEntries []UserEntry + var usrEntries []types.UserEntry for _, usr := range usrs { usrEntries = append(usrEntries, userEntryFromUserDB(usr)) } @@ -245,31 +318,40 @@ func (m *Manager) AllUsers() ([]UserEntry, error) { } // GroupByName returns the group information for the given group name. -func (m *Manager) GroupByName(groupname string) (GroupEntry, error) { +func (m *Manager) GroupByName(groupname string) (types.GroupEntry, error) { grp, err := m.cache.GroupByName(groupname) + if errors.Is(err, cache.NoDataFoundError{}) { + // Check if the group is a temporary group. + return m.temporaryRecords.GroupByName(groupname) + } if err != nil { - return GroupEntry{}, err + return types.GroupEntry{}, err } return groupEntryFromGroupDB(grp), nil } // GroupByID returns the group information for the given group ID. -func (m *Manager) GroupByID(gid uint32) (GroupEntry, error) { +func (m *Manager) GroupByID(gid uint32) (types.GroupEntry, error) { grp, err := m.cache.GroupByID(gid) + if errors.Is(err, cache.NoDataFoundError{}) { + // Check if the group is a temporary group. + return m.temporaryRecords.GroupByID(gid) + } if err != nil { - return GroupEntry{}, err + return types.GroupEntry{}, err } return groupEntryFromGroupDB(grp), nil } // AllGroups returns all groups. -func (m *Manager) AllGroups() ([]GroupEntry, error) { +func (m *Manager) AllGroups() ([]types.GroupEntry, error) { + // TODO: Same as for AllUsers, we might want to return temporary groups here. grps, err := m.cache.AllGroups() if err != nil { return nil, err } - var grpEntries []GroupEntry + var grpEntries []types.GroupEntry for _, grp := range grps { grpEntries = append(grpEntries, groupEntryFromGroupDB(grp)) } @@ -277,55 +359,32 @@ func (m *Manager) AllGroups() ([]GroupEntry, error) { } // ShadowByName returns the shadow information for the given user name. -func (m *Manager) ShadowByName(username string) (ShadowEntry, error) { +func (m *Manager) ShadowByName(username string) (types.ShadowEntry, error) { usr, err := m.cache.UserByName(username) if err != nil { - return ShadowEntry{}, err + return types.ShadowEntry{}, err } return shadowEntryFromUserDB(usr), nil } // AllShadows returns all shadow entries. -func (m *Manager) AllShadows() ([]ShadowEntry, error) { +func (m *Manager) AllShadows() ([]types.ShadowEntry, error) { + // TODO: Even less sure if we should return temporary users here. usrs, err := m.cache.AllUsers() if err != nil { return nil, err } - var shadowEntries []ShadowEntry + var shadowEntries []types.ShadowEntry for _, usr := range usrs { shadowEntries = append(shadowEntries, shadowEntryFromUserDB(usr)) } return shadowEntries, err } -// GenerateUID deterministically generates an ID between from the given string, ignoring case, -// in the range [UIDMin, UIDMax]. The generated ID is *not* guaranteed to be unique. -func (m *Manager) GenerateUID(str string) uint32 { - return generateID(str, m.config.UIDMin, m.config.UIDMax) -} - -// GenerateGID deterministically generates an ID between from the given string, ignoring case, -// in the range [GIDMin, GIDMax]. The generated ID is *not* guaranteed to be unique. -func (m *Manager) GenerateGID(str string) uint32 { - return generateID(str, m.config.GIDMin, m.config.GIDMax) -} - -func generateID(str string, minID, maxID uint32) uint32 { - str = strings.ToLower(str) - - // Create a SHA-256 hash of the input string - hash := sha256.Sum256([]byte(str)) - - // Convert the first 4 bytes of the hash into an integer - number := binary.BigEndian.Uint32(hash[:4]) % (maxID + 1) - - // Repeat hashing until we get a number in the desired range. This ensures that the generated IDs are uniformly - // distributed in the range, opposed to a simple modulo operation. - for number < minID { - hash = sha256.Sum256(hash[:]) - number = binary.BigEndian.Uint32(hash[:4]) % (maxID + 1) - } - - return number +// RegisterUserPreAuth registers a temporary user with a unique UID in our NSS handler (in memory, not in the database). +// +// The temporary user record is removed when UpdateUser is called with the same username. +func (m *Manager) RegisterUserPreAuth(name string) (uint32, error) { + return m.temporaryRecords.RegisterPreAuthUser(name) } diff --git a/internal/users/manager_test.go b/internal/users/manager_test.go index 5bca94f33..3f6f3682b 100644 --- a/internal/users/manager_test.go +++ b/internal/users/manager_test.go @@ -10,8 +10,10 @@ import ( "github.com/ubuntu/authd/internal/testutils/golden" "github.com/ubuntu/authd/internal/users" "github.com/ubuntu/authd/internal/users/cache" - localgroupstestutils "github.com/ubuntu/authd/internal/users/localgroups/testutils" + "github.com/ubuntu/authd/internal/users/idgenerator" + localgroupstestutils "github.com/ubuntu/authd/internal/users/localentries/testutils" userstestutils "github.com/ubuntu/authd/internal/users/testutils" + "github.com/ubuntu/authd/internal/users/types" "go.etcd.io/bbolt" ) @@ -26,7 +28,8 @@ func TestNewManager(t *testing.T) { wantErr bool }{ - "Successfully create a new manager": {}, + "Successfully create manager with default config": {}, + "Successfully create manager with custom config": {uidMin: 10000, uidMax: 20000, gidMin: 10000, gidMax: 20000}, // Corrupted databases "New recreates any missing buckets and delete unknowns": {dbFile: "database_with_unknown_bucket"}, @@ -35,6 +38,7 @@ func TestNewManager(t *testing.T) { "Error if cacheDir does not exist": {dbFile: "-", wantErr: true}, "Error if UID_MIN is equal to UID_MAX": {uidMin: 1000, uidMax: 1000, wantErr: true}, "Error if GID_MIN is equal to GID_MAX": {gidMin: 1000, gidMax: 1000, wantErr: true}, + "Error if UID range is too small": {uidMin: 1000, uidMax: 2000, wantErr: true}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { @@ -97,28 +101,34 @@ func TestStop(t *testing.T) { require.ErrorIs(t, err, bbolt.ErrDatabaseNotOpen, "AllUsers should return an error, but did not") } +type userCase struct { + types.UserInfo + UID uint32 +} + func TestUpdateUser(t *testing.T) { - userCases := map[string]users.UserInfo{ - "user1": {Name: "user1", UID: 1111}, - "user2": {Name: "user2", UID: 2222}, - "same-name-different-uid": {Name: "user1", UID: 3333}, - "different-name-same-uid": {Name: "newuser1", UID: 1111}, + userCases := map[string]userCase{ + "user1": {UserInfo: types.UserInfo{Name: "user1"}, UID: 1111}, + "nameless": {UID: 1111}, + "user2": {UserInfo: types.UserInfo{Name: "user2"}, UID: 2222}, + "same-name-different-uid": {UserInfo: types.UserInfo{Name: "user1"}, UID: 3333}, + "different-name-same-uid": {UserInfo: types.UserInfo{Name: "newuser1"}, UID: 1111}, } - groupsCases := map[string][]users.GroupInfo{ + groupsCases := map[string][]types.GroupInfo{ "cloud-group": {{Name: "group1", GID: ptrUint32(11111), UGID: "1"}}, - "local-group": {{Name: "localgroup1"}}, + "local-group": {{Name: "localgroup1", GID: nil, UGID: ""}}, "mixed-groups-cloud-first": { {Name: "group1", GID: ptrUint32(11111), UGID: "1"}, {Name: "localgroup1", GID: nil, UGID: ""}, }, "mixed-groups-local-first": { - {Name: "localgroup1"}, + {Name: "localgroup1", GID: nil, UGID: ""}, {Name: "group1", GID: ptrUint32(11111), UGID: "1"}, }, "mixed-groups-gpasswd-fail": { {Name: "group1", GID: ptrUint32(11111), UGID: "1"}, - {Name: "gpasswdfail"}, + {Name: "gpasswdfail", GID: nil, UGID: ""}, }, "nameless-group": {{Name: "", GID: ptrUint32(11111), UGID: "1"}}, "different-name-same-gid": {{Name: "newgroup1", GID: ptrUint32(11111), UGID: "1"}}, @@ -141,7 +151,6 @@ func TestUpdateUser(t *testing.T) { "UID does not change if user already exists": {userCase: "same-name-different-uid", dbFile: "one_user_and_group", wantSameUID: true}, "Error if user has no username": {userCase: "nameless", wantErr: true, noOutput: true}, - "Error if user has conflicting uid": {userCase: "different-name-same-uid", dbFile: "one_user_and_group", wantErr: true, noOutput: true}, "Error if group has no name": {groupsCase: "nameless-group", wantErr: true, noOutput: true}, "Error if group has conflicting gid": {groupsCase: "different-name-same-gid", dbFile: "one_user_and_group", wantErr: true, noOutput: true}, @@ -176,7 +185,20 @@ func TestUpdateUser(t *testing.T) { if tc.dbFile != "" { cache.Z_ForTests_CreateDBFromYAML(t, filepath.Join("testdata", "db", tc.dbFile+".db.yaml"), cacheDir) } - m := newManagerForTests(t, cacheDir) + + gids := []uint32{user.UID} + for _, group := range user.Groups { + if group.GID != nil { + gids = append(gids, *group.GID) + } + } + managerOpts := []users.Option{ + users.WithIDGenerator(&idgenerator.IDGeneratorMock{ + UIDsToGenerate: []uint32{user.UID}, + GIDsToGenerate: gids, + }), + } + m := newManagerForTests(t, cacheDir, managerOpts...) var oldUID uint32 if tc.wantSameUID { @@ -185,7 +207,7 @@ func TestUpdateUser(t *testing.T) { oldUID = oldUser.UID } - err := m.UpdateUser(user) + err := m.UpdateUser(user.UserInfo) requireErrorAssertions(t, err, nil, tc.wantErr) if tc.wantErr && tc.noOutput { @@ -291,17 +313,21 @@ func TestUpdateBrokerForUser(t *testing.T) { } } +//nolint:dupl // This is not a duplicate test func TestUserByIDAndName(t *testing.T) { tests := map[string]struct { - uid uint32 - username string - dbFile string + uid uint32 + username string + dbFile string + isTempUser bool wantErr bool wantErrType error }{ - "Successfully get user by ID": {uid: 1111, dbFile: "multiple_users_and_groups"}, - "Successfully get user by name": {username: "user1", dbFile: "multiple_users_and_groups"}, + "Successfully get user by ID": {uid: 1111, dbFile: "multiple_users_and_groups"}, + "Successfully get user by name": {username: "user1", dbFile: "multiple_users_and_groups"}, + "Successfully get temporary user by ID": {dbFile: "multiple_users_and_groups", isTempUser: true}, + "Successfully get temporary user by name": {username: "tempuser1", dbFile: "multiple_users_and_groups", isTempUser: true}, "Error if user does not exist - by ID": {uid: 0, dbFile: "multiple_users_and_groups", wantErrType: cache.NoDataFoundError{}}, "Error if user does not exist - by name": {username: "doesnotexist", dbFile: "multiple_users_and_groups", wantErrType: cache.NoDataFoundError{}}, @@ -318,8 +344,13 @@ func TestUserByIDAndName(t *testing.T) { m := newManagerForTests(t, cacheDir) - var user users.UserEntry var err error + if tc.isTempUser { + tc.uid, _, err = m.TemporaryRecords().RegisterUser("tempuser1") + require.NoError(t, err, "RegisterUser should not return an error, but did") + } + + var user types.UserEntry if tc.username != "" { user, err = m.UserByName(tc.username) } else { @@ -331,6 +362,15 @@ func TestUserByIDAndName(t *testing.T) { return } + // Registering a temporary user creates it with a random UID and random gecos, so we have to make it + // deterministic before comparing it with the golden file + if tc.isTempUser { + require.Equal(t, tc.uid, user.UID) + user.UID = 0 + require.NotEmpty(t, user.Gecos) + user.Gecos = "" + } + golden.CheckOrUpdateYAML(t, user) }) } @@ -369,17 +409,21 @@ func TestAllUsers(t *testing.T) { } } +//nolint:dupl // This is not a duplicate test func TestGroupByIDAndName(t *testing.T) { tests := map[string]struct { - gid uint32 - groupname string - dbFile string + gid uint32 + groupname string + dbFile string + isTempGroup bool wantErr bool wantErrType error }{ - "Successfully get group by ID": {gid: 11111, dbFile: "multiple_users_and_groups"}, - "Successfully get group by name": {groupname: "group1", dbFile: "multiple_users_and_groups"}, + "Successfully get group by ID": {gid: 11111, dbFile: "multiple_users_and_groups"}, + "Successfully get group by name": {groupname: "group1", dbFile: "multiple_users_and_groups"}, + "Successfully get temporary group by ID": {dbFile: "multiple_users_and_groups", isTempGroup: true}, + "Successfully get temporary group by name": {groupname: "tempgroup1", dbFile: "multiple_users_and_groups", isTempGroup: true}, "Error if group does not exist - by ID": {gid: 0, dbFile: "multiple_users_and_groups", wantErrType: cache.NoDataFoundError{}}, "Error if group does not exist - by name": {groupname: "doesnotexist", dbFile: "multiple_users_and_groups", wantErrType: cache.NoDataFoundError{}}, @@ -395,8 +439,13 @@ func TestGroupByIDAndName(t *testing.T) { cache.Z_ForTests_CreateDBFromYAML(t, filepath.Join("testdata", "db", tc.dbFile+".db.yaml"), cacheDir) m := newManagerForTests(t, cacheDir) - var group users.GroupEntry var err error + if tc.isTempGroup { + tc.gid, _, err = m.TemporaryRecords().RegisterGroup("tempgroup1") + require.NoError(t, err, "RegisterGroup should not return an error, but did") + } + + var group types.GroupEntry if tc.groupname != "" { group, err = m.GroupByName(tc.groupname) } else { @@ -408,6 +457,15 @@ func TestGroupByIDAndName(t *testing.T) { return } + // Registering a temporary group creates it with a random GID and random passwd, so we have to make it + // deterministic before comparing it with the golden file + if tc.isTempGroup { + require.Equal(t, tc.gid, group.GID) + group.GID = 0 + require.NotEmpty(t, group.Passwd) + group.Passwd = "" + } + golden.CheckOrUpdateYAML(t, group) }) } @@ -534,10 +592,10 @@ func requireErrorAssertions(t *testing.T, gotErr, wantErrType error, wantErr boo require.NoError(t, gotErr, "Error should not be returned") } -func newManagerForTests(t *testing.T, cacheDir string) *users.Manager { +func newManagerForTests(t *testing.T, cacheDir string, opts ...users.Option) *users.Manager { t.Helper() - m, err := users.NewManager(users.DefaultConfig, cacheDir) + m, err := users.NewManager(users.DefaultConfig, cacheDir, opts...) require.NoError(t, err, "NewManager should not return an error, but did") return m diff --git a/internal/users/tempentries/groups.go b/internal/users/tempentries/groups.go new file mode 100644 index 000000000..ba0947323 --- /dev/null +++ b/internal/users/tempentries/groups.go @@ -0,0 +1,179 @@ +package tempentries + +import ( + "context" + "crypto/rand" + "errors" + "fmt" + "sync" + + "github.com/ubuntu/authd/internal/log" + "github.com/ubuntu/authd/internal/users/localentries" + "github.com/ubuntu/authd/internal/users/types" +) + +type groupRecord struct { + name string + gid uint32 + passwd string +} + +type temporaryGroupRecords struct { + idGenerator IDGenerator + registerMutex sync.Mutex + rwMutex sync.RWMutex + groups map[uint32]groupRecord + gidByName map[string]uint32 +} + +func newTemporaryGroupRecords(idGenerator IDGenerator) *temporaryGroupRecords { + return &temporaryGroupRecords{ + idGenerator: idGenerator, + registerMutex: sync.Mutex{}, + rwMutex: sync.RWMutex{}, + groups: make(map[uint32]groupRecord), + gidByName: make(map[string]uint32), + } +} + +// GroupByID returns the group information for the given group ID. +func (r *temporaryGroupRecords) GroupByID(gid uint32) (types.GroupEntry, error) { + r.rwMutex.RLock() + defer r.rwMutex.RUnlock() + + group, ok := r.groups[gid] + if !ok { + return types.GroupEntry{}, NoDataFoundError{} + } + + return r.groupEntry(group), nil +} + +// GroupByName returns the group information for the given group name. +func (r *temporaryGroupRecords) GroupByName(name string) (types.GroupEntry, error) { + r.rwMutex.RLock() + defer r.rwMutex.RUnlock() + + gid, ok := r.gidByName[name] + if !ok { + return types.GroupEntry{}, NoDataFoundError{} + } + + return r.GroupByID(gid) +} + +func (r *temporaryGroupRecords) groupEntry(group groupRecord) types.GroupEntry { + return types.GroupEntry{Name: group.name, GID: group.gid, Passwd: group.passwd} +} + +// RegisterGroup registers a temporary group with a unique GID in our NSS handler (in memory, not in the database). +// +// Returns the generated GID and a cleanup function that should be called to remove the temporary group once the group +// was added to the database. +func (r *temporaryGroupRecords) RegisterGroup(name string) (gid uint32, cleanup func() error, err error) { + r.registerMutex.Lock() + defer r.registerMutex.Unlock() + + // Check if there is already a temporary group with this name + _, err = r.GroupByName(name) + if err != nil && !errors.Is(err, NoDataFoundError{}) { + return 0, nil, fmt.Errorf("could not check if temporary group %q already exists: %w", name, err) + } + if err == nil { + return 0, nil, fmt.Errorf("group %q already exists", name) + } + + // Generate a GID until we find a unique one + for { + gid, err = r.idGenerator.GenerateGID() + if err != nil { + return 0, nil, err + } + + // To avoid races where a group with this GID is created by some NSS source after we checked, we register this + // GID in our NSS handler and then check if another group with the same GID exists in the system. This way we + // can guarantee that the GID is unique, under the assumption that other NSS sources don't add groups with a GID + // that we already registered (if they do, there's nothing we can do about it). + var tmpID string + tmpID, cleanup, err = r.addTemporaryGroup(gid, name) + if err != nil { + return 0, nil, fmt.Errorf("could not register temporary group: %w", err) + } + + unique, err := r.uniqueNameAndGID(name, gid, tmpID) + if err != nil { + if cleanupErr := cleanup(); cleanupErr != nil { + err = errors.Join(err, cleanupErr) + } + return 0, nil, fmt.Errorf("could not check if GID %d is unique: %w", gid, err) + } + if unique { + break + } + + // If the GID is not unique, remove the temporary group and generate a new one in the next iteration. + if err := cleanup(); err != nil { + return 0, nil, fmt.Errorf("could not remove temporary group %q: %w", name, err) + } + } + + log.Debugf(context.Background(), "Registered group %q with GID %d", name, gid) + return gid, cleanup, nil +} + +func (r *temporaryGroupRecords) uniqueNameAndGID(name string, gid uint32, tmpID string) (bool, error) { + entries, err := localentries.GetGroupEntries() + if err != nil { + return false, err + } + for _, entry := range entries { + if entry.Name == name && entry.Passwd != tmpID { + // A group with the same name already exists, we can't register this temporary group. + log.Debugf(context.Background(), "Name %q already in use by GID %d", name, entry.GID) + return false, fmt.Errorf("group %q already exists", name) + } + + if entry.GID == gid && entry.Passwd != tmpID { + log.Debugf(context.Background(), "GID %d already in use by group %q, generating a new one", gid, entry.Name) + return false, nil + } + } + + return true, nil +} + +func (r *temporaryGroupRecords) addTemporaryGroup(gid uint32, name string) (tmpID string, cleanup func() error, err error) { + r.rwMutex.Lock() + defer r.rwMutex.Unlock() + + // Generate a 64 character (32 bytes in hex) random ID which we store in the passwd field of the temporary group + // record to be able to identify it in isUniqueGID. + bytes := make([]byte, 32) + if _, err := rand.Read(bytes); err != nil { + return "", nil, fmt.Errorf("failed to generate random name: %w", err) + } + tmpID = fmt.Sprintf("authd-temp-group-%x", bytes) + + r.groups[gid] = groupRecord{name: name, gid: gid, passwd: tmpID} + r.gidByName[name] = gid + + cleanup = func() error { return r.deleteTemporaryGroup(gid) } + + return tmpID, cleanup, nil +} + +func (r *temporaryGroupRecords) deleteTemporaryGroup(gid uint32) error { + r.rwMutex.Lock() + defer r.rwMutex.Unlock() + + group, ok := r.groups[gid] + if !ok { + return fmt.Errorf("temporary group with GID %d does not exist", gid) + } + + delete(r.groups, gid) + delete(r.gidByName, group.name) + + log.Debugf(context.Background(), "Removed temporary record for group %q with GID %d", group.name, gid) + return nil +} diff --git a/internal/users/tempentries/groups_test.go b/internal/users/tempentries/groups_test.go new file mode 100644 index 000000000..94c5849ba --- /dev/null +++ b/internal/users/tempentries/groups_test.go @@ -0,0 +1,148 @@ +package tempentries + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/ubuntu/authd/internal/testutils/golden" + "github.com/ubuntu/authd/internal/users/idgenerator" + "github.com/ubuntu/authd/internal/users/types" +) + +func TestRegisterGroup(t *testing.T) { + t.Parallel() + + gidToGenerate := uint32(12345) + + tests := map[string]struct { + groupName string + gidsToGenerate []uint32 + + wantErr bool + }{ + "Successfully register a new group": {}, + "Successfully register a group if the first generated GID is already in use": { + gidsToGenerate: []uint32{0, gidToGenerate}, // GID 0 (root) always exists + }, + + "Error when name is already in use": {groupName: "root", wantErr: true}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + if tc.groupName == "" { + tc.groupName = "authd-temp-groups-test" + } + + if tc.gidsToGenerate == nil { + tc.gidsToGenerate = []uint32{gidToGenerate} + } + + idGeneratorMock := &idgenerator.IDGeneratorMock{GIDsToGenerate: tc.gidsToGenerate} + records := newTemporaryGroupRecords(idGeneratorMock) + + gid, cleanup, err := records.RegisterGroup(tc.groupName) + if tc.wantErr { + require.Error(t, err, "RegisterGroup should return an error, but did not") + return + } + require.NoError(t, err, "RegisterGroup should not return an error, but did") + require.Equal(t, gidToGenerate, gid, "GID should be the one generated by the IDGenerator") + // Check that the temporary group was created + group, err := records.GroupByID(gid) + require.NoError(t, err, "GroupByID should not return an error, but did") + checkGroup(t, group) + + // Delete the temporary group + err = cleanup() + require.NoError(t, err, "Cleanup should not return an error, but did") + + // Check that the temporary group was deleted + _, err = records.GroupByID(gid) + require.Error(t, err, "GroupByID should return an error, but did not") + }) + } +} + +func TestGroupByIDAndName(t *testing.T) { + t.Parallel() + + groupName := "authd-temp-groups-test" + gidToGenerate := uint32(12345) + + tests := map[string]struct { + registerGroup bool + groupAlreadyRemoved bool + byName bool + + wantErr bool + }{ + "Successfully get a group by ID": {registerGroup: true}, + "Successfully get a group by name": {registerGroup: true, byName: true}, + + "Error when group is not registered - GroupByID": {wantErr: true}, + "Error when group is not registered - GroupByName": {byName: true, wantErr: true}, + "Error when group is already removed - GroupByID": { + registerGroup: true, + groupAlreadyRemoved: true, + wantErr: true, + }, + "Error when group is already removed - GroupByName": { + registerGroup: true, + groupAlreadyRemoved: true, + byName: true, + wantErr: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + idGeneratorMock := &idgenerator.IDGeneratorMock{GIDsToGenerate: []uint32{gidToGenerate}} + records := newTemporaryGroupRecords(idGeneratorMock) + + if tc.registerGroup { + gid, cleanup, err := records.RegisterGroup(groupName) + require.NoError(t, err, "RegisterGroup should not return an error, but did") + require.Equal(t, gidToGenerate, gid, "GID should be the one generated by the IDGenerator") + + if tc.groupAlreadyRemoved { + err = cleanup() + require.NoError(t, err, "Cleanup should not return an error, but did") + } else { + defer func() { + err := cleanup() + require.NoError(t, err, "Cleanup should not return an error, but did") + }() + } + } + + var group types.GroupEntry + var err error + if tc.byName { + group, err = records.GroupByID(gidToGenerate) + } else { + group, err = records.GroupByName(groupName) + } + + if tc.wantErr { + require.Error(t, err, "GroupByID should return an error, but did not") + return + } + require.NoError(t, err, "GroupByID should not return an error, but did") + checkGroup(t, group) + }) + } +} + +func checkGroup(t *testing.T, group types.GroupEntry) { + t.Helper() + + // The passwd field is randomly generated, so unset it before comparing the group with the golden file. + require.NotEmpty(t, group.Passwd, "Passwd should not be empty") + group.Passwd = "" + + golden.CheckOrUpdateYAML(t, group) +} diff --git a/internal/users/tempentries/preauth.go b/internal/users/tempentries/preauth.go new file mode 100644 index 000000000..08f4ba4d4 --- /dev/null +++ b/internal/users/tempentries/preauth.go @@ -0,0 +1,226 @@ +package tempentries + +import ( + "context" + "crypto/rand" + "errors" + "fmt" + "sync" + + "github.com/ubuntu/authd/internal/log" + "github.com/ubuntu/authd/internal/users/localentries" + "github.com/ubuntu/authd/internal/users/types" +) + +const ( + // MaxPreAuthUsers is the maximum number of pre-auth users that can be registered. If this limit is reached, + // RegisterPreAuthUser will return an error and disable login for new users via SSH until authd is restarted. + // + // This value must be significantly smaller (less than half) than the number of UIDs which can be generated (as + // defined by UID_MIN and UID_MAX in the config file), otherwise finding a unique UID by trial and error can take + // too long. + MaxPreAuthUsers = 4096 +) + +type preAuthUser struct { + // name is the generated random name of the pre-auth user (which is returned by UserByID). + name string + // loginName is the name of the user who the pre-auth user record is created for. + loginName string + uid uint32 +} + +type preAuthUserRecords struct { + idGenerator IDGenerator + registerMutex sync.Mutex + rwMutex sync.RWMutex + users map[uint32]preAuthUser + uidByName map[string]uint32 + uidByLogin map[string]uint32 + numUsers int +} + +func newPreAuthUserRecords(idGenerator IDGenerator) *preAuthUserRecords { + return &preAuthUserRecords{ + idGenerator: idGenerator, + registerMutex: sync.Mutex{}, + rwMutex: sync.RWMutex{}, + users: make(map[uint32]preAuthUser), + uidByName: make(map[string]uint32), + uidByLogin: make(map[string]uint32), + } +} + +// UserByID returns the user information for the given user ID. +func (r *preAuthUserRecords) userByID(uid uint32) (types.UserEntry, error) { + r.rwMutex.RLock() + defer r.rwMutex.RUnlock() + + user, ok := r.users[uid] + if !ok { + return types.UserEntry{}, NoDataFoundError{} + } + + return preAuthUserEntry(user), nil +} + +// UserByName returns the user information for the given user name. +func (r *preAuthUserRecords) userByName(name string) (types.UserEntry, error) { + r.rwMutex.RLock() + defer r.rwMutex.RUnlock() + + uid, ok := r.uidByName[name] + if !ok { + return types.UserEntry{}, NoDataFoundError{} + } + + return r.userByID(uid) +} + +func (r *preAuthUserRecords) userByLogin(loginName string) (types.UserEntry, error) { + r.rwMutex.RLock() + defer r.rwMutex.RUnlock() + + uid, ok := r.uidByLogin[loginName] + if !ok { + return types.UserEntry{}, NoDataFoundError{} + } + + return r.userByID(uid) +} + +func preAuthUserEntry(user preAuthUser) types.UserEntry { + // TODO: Should we set the GID to something else than 0 (i.e. the GID of the root primary group)? + return types.UserEntry{ + Name: user.name, + UID: user.uid, + Gecos: user.loginName, + Dir: "/nonexistent", + Shell: "/usr/sbin/nologin", + } +} + +// RegisterPreAuthUser registers a temporary user with a unique UID in our NSS handler (in memory, not in the database). +// +// The temporary user record is removed when UpdateUser is called with the same username. +// +// This method is called when a user logs in for the first time via SSH, in which case sshd checks if the user exists on +// the system (before authentication), and denies the login if the user does not exist. We pretend that the user exists +// by creating this temporary user record, which is converted into a permanent user record when UpdateUser is called +// after the user authenticated successfully. +// +// Returns the generated UID. +func (r *preAuthUserRecords) RegisterPreAuthUser(loginName string) (uint32, error) { + // To mitigate DoS attacks, we limit the length of the name to 256 characters. + if len(loginName) > 256 { + return 0, errors.New("username is too long (max 256 characters)") + } + + r.registerMutex.Lock() + defer r.registerMutex.Unlock() + + if r.numUsers >= MaxPreAuthUsers { + return 0, errors.New("maximum number of pre-auth users reached, login for new users via SSH is disabled until authd is restarted") + } + + // Check if there is already a pre-auth user for that name + user, err := r.userByLogin(loginName) + if err != nil && !errors.Is(err, NoDataFoundError{}) { + return 0, fmt.Errorf("could not check if pre-auth user %q already exists: %w", loginName, err) + } + if err == nil { + // A pre-auth user is already registered for this name, so we return the already generated UID. + return user.UID, nil + } + + // Generate a UID until we find a unique one + for { + uid, err := r.idGenerator.GenerateUID() + if err != nil { + return 0, err + } + + // To avoid races where a user with this UID is created by some NSS source after we checked, we register this + // UID in our NSS handler and then check if another user with the same UID exists in the system. This way we + // can guarantee that the UID is unique, under the assumption that other NSS sources don't add users with a UID + // that we already registered (if they do, there's nothing we can do about it). + tmpName, cleanup, err := r.addPreAuthUser(uid, loginName) + if err != nil { + return 0, fmt.Errorf("could not add pre-auth user record: %w", err) + } + + unique, err := r.isUniqueUID(uid, tmpName) + if err != nil { + cleanup() + return 0, fmt.Errorf("could not check if UID %d is unique: %w", uid, err) + } + if unique { + log.Debugf(context.Background(), "Added temporary record for user %q with UID %d", loginName, uid) + return uid, nil + } + + // If the UID is not unique, remove the temporary user and generate a new one in the next iteration. + cleanup() + } +} + +// isUniqueUID returns true if the given UID is unique in the system. It returns false if the UID is already assigned to +// a user by any NSS source (except the given temporary user). +func (r *preAuthUserRecords) isUniqueUID(uid uint32, tmpName string) (bool, error) { + entries, err := localentries.GetPasswdEntries() + if err != nil { + return false, err + } + for _, entry := range entries { + if entry.UID == uid && entry.Name != tmpName { + return false, nil + } + } + return true, nil +} + +// addPreAuthUser adds a temporary user with a random name and the given UID. We use a random name here to avoid +// creating user records with attacker-controlled names. +// +// It returns the generated name and a cleanup function to remove the temporary user record. +func (r *preAuthUserRecords) addPreAuthUser(uid uint32, loginName string) (name string, cleanup func(), err error) { + r.rwMutex.Lock() + defer r.rwMutex.Unlock() + + // Generate a 64 character (32 bytes in hex) random ID which we store in the name field of the temporary user + // record to be able to identify it in isUniqueUID. + bytes := make([]byte, 32) + if _, err := rand.Read(bytes); err != nil { + return "", nil, fmt.Errorf("failed to generate random name: %w", err) + } + name = fmt.Sprintf("authd-pre-auth-user-%x", bytes) + + user := preAuthUser{name: name, uid: uid, loginName: loginName} + r.users[uid] = user + r.uidByName[name] = uid + r.uidByLogin[loginName] = uid + r.numUsers++ + + cleanup = func() { r.deletePreAuthUser(uid) } + + return name, cleanup, nil +} + +// deletePreAuthUser deletes the temporary user with the given UID. +func (r *preAuthUserRecords) deletePreAuthUser(uid uint32) { + r.rwMutex.Lock() + defer r.rwMutex.Unlock() + + user, ok := r.users[uid] + if !ok { + // We ignore the case that the pre-auth user does not exist, because it can happen that the same user is + // registered multiple times (because multiple SSH sessions are opened for the same user) and the cleanup + // function is called multiple times. + return + } + delete(r.users, uid) + delete(r.uidByName, user.name) + delete(r.uidByLogin, user.loginName) + r.numUsers-- + log.Debugf(context.Background(), "Removed temporary record for user %q with UID %d", user.name, uid) +} diff --git a/internal/users/tempentries/preauth_test.go b/internal/users/tempentries/preauth_test.go new file mode 100644 index 000000000..dd2b93146 --- /dev/null +++ b/internal/users/tempentries/preauth_test.go @@ -0,0 +1,146 @@ +package tempentries + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/ubuntu/authd/internal/testutils/golden" + "github.com/ubuntu/authd/internal/users/idgenerator" + "github.com/ubuntu/authd/internal/users/types" +) + +func TestPreAuthUser(t *testing.T) { + t.Parallel() + + loginName := "test" + uidToGenerate := uint32(12345) + + tests := map[string]struct { + maxUsers bool + uidsToGenerate []uint32 + registerTwice bool + + wantErr bool + }{ + "Successfully register a pre-auth user": {}, + "Successfully register a pre-auth user if the first generated UID is already in use": { + uidsToGenerate: []uint32{0, uidToGenerate}, // UID 0 (root) always exists + }, + "No error when registering a pre-auth user with the same name": {registerTwice: true}, + + "Error when maximum number of pre-auth users is reached": {maxUsers: true, wantErr: true}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + if tc.uidsToGenerate == nil { + tc.uidsToGenerate = []uint32{uidToGenerate} + } + + idGeneratorMock := &idgenerator.IDGeneratorMock{UIDsToGenerate: tc.uidsToGenerate} + records := newPreAuthUserRecords(idGeneratorMock) + + if tc.maxUsers { + records.numUsers = MaxPreAuthUsers + } + + uid, err := records.RegisterPreAuthUser(loginName) + if tc.wantErr { + require.Error(t, err, "RegisterPreAuthUser should return an error, but did not") + return + } + require.NoError(t, err, "RegisterPreAuthUser should not return an error, but did") + require.Equal(t, uidToGenerate, uid, "UID should be the one generated by the IDGenerator") + require.Equal(t, records.numUsers, 1, "Number of pre-auth users should be 1") + + if tc.registerTwice { + uid, err = records.RegisterPreAuthUser(loginName) + require.NoError(t, err, "RegisterPreAuthUser should not return an error, but did") + require.Equal(t, uidToGenerate, uid, "UID should be the one generated by the IDGenerator") + require.Equal(t, records.numUsers, 1, "Number of pre-auth users should be 1") + } + + // Check that the user was registered + user, err := records.userByLogin(loginName) + require.NoError(t, err, "UserByID should not return an error, but did") + checkPreAuthUser(t, user) + + // Remove the user + records.deletePreAuthUser(uidToGenerate) + require.Equal(t, records.numUsers, 0, "Number of pre-auth users should be 0") + + // Check that the user was removed + _, err = records.userByLogin(loginName) + require.Error(t, err, "UserByID should return an error, but did not") + }) + } +} + +func TestPreAuthUserByIDAndName(t *testing.T) { + t.Parallel() + + loginName := "test" + uidToGenerate := uint32(12345) + + tests := map[string]struct { + registerUser bool + userAlreadyRemoved bool + + wantErr bool + }{ + "Successfully get a user by ID and name": {registerUser: true}, + + "Error when user is not registered": {wantErr: true}, + "Error when user is already removed": {registerUser: true, userAlreadyRemoved: true, wantErr: true}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + idGeneratorMock := &idgenerator.IDGeneratorMock{UIDsToGenerate: []uint32{uidToGenerate}} + records := newPreAuthUserRecords(idGeneratorMock) + + if tc.registerUser { + uid, err := records.RegisterPreAuthUser(loginName) + require.NoError(t, err, "RegisterPreAuthUser should not return an error, but did") + require.Equal(t, uidToGenerate, uid, "UID should be the one generated by the IDGenerator") + } + + if tc.userAlreadyRemoved { + records.deletePreAuthUser(uidToGenerate) + } else { + defer records.deletePreAuthUser(uidToGenerate) + } + + user, err := records.userByID(uidToGenerate) + + if tc.wantErr { + require.Error(t, err, "UserByID should return an error, but did not") + return + } + require.NoError(t, err, "UserByID should not return an error, but did") + checkPreAuthUser(t, user) + + user, err = records.userByName(user.Name) + if tc.wantErr { + require.Error(t, err, "UserByName should return an error, but did not") + return + } + require.NoError(t, err, "UserByName should not return an error, but did") + checkPreAuthUser(t, user) + }) + } +} + +func checkPreAuthUser(t *testing.T, user types.UserEntry) { + t.Helper() + + // The name field is randomly generated, so unset it before comparing the user with the golden file. + require.NotEmpty(t, user.Name, "Name should not be empty") + user.Name = "" + + golden.CheckOrUpdateYAML(t, user) +} diff --git a/internal/users/tempentries/tempentries.go b/internal/users/tempentries/tempentries.go new file mode 100644 index 000000000..061a8814f --- /dev/null +++ b/internal/users/tempentries/tempentries.go @@ -0,0 +1,151 @@ +// Package tempentries provides a temporary user and group records. +package tempentries + +import ( + "context" + "errors" + "fmt" + + "github.com/ubuntu/authd/internal/log" + "github.com/ubuntu/authd/internal/users/cache" + "github.com/ubuntu/authd/internal/users/types" +) + +// NoDataFoundError is the error returned when no entry is found in the cache. +type NoDataFoundError = cache.NoDataFoundError + +// IDGenerator is the interface that must be implemented by the ID generator. +type IDGenerator interface { + GenerateUID() (uint32, error) + GenerateGID() (uint32, error) +} + +// TemporaryRecords is the in-memory temporary user and group records. +type TemporaryRecords struct { + *temporaryUserRecords + *preAuthUserRecords + *temporaryGroupRecords + + idGenerator IDGenerator +} + +// NewTemporaryRecords creates a new TemporaryRecords. +func NewTemporaryRecords(idGenerator IDGenerator) *TemporaryRecords { + return &TemporaryRecords{ + idGenerator: idGenerator, + temporaryUserRecords: newTemporaryUserRecords(idGenerator), + preAuthUserRecords: newPreAuthUserRecords(idGenerator), + temporaryGroupRecords: newTemporaryGroupRecords(idGenerator), + } +} + +// UserByID returns the user information for the given user ID. +func (r *TemporaryRecords) UserByID(uid uint32) (types.UserEntry, error) { + user, err := r.temporaryUserRecords.userByID(uid) + if errors.Is(err, NoDataFoundError{}) { + user, err = r.preAuthUserRecords.userByID(uid) + } + return user, err +} + +// UserByName returns the user information for the given user name. +func (r *TemporaryRecords) UserByName(name string) (types.UserEntry, error) { + user, err := r.temporaryUserRecords.userByName(name) + if errors.Is(err, NoDataFoundError{}) { + user, err = r.preAuthUserRecords.userByName(name) + } + return user, err +} + +// RegisterUser registers a temporary user with a unique UID in our NSS handler (in memory, not in the database). +// +// Returns the generated UID and a cleanup function that should be called to remove the temporary user once the user was +// added to the database. +func (r *TemporaryRecords) RegisterUser(name string) (uid uint32, cleanup func() error, err error) { + r.temporaryUserRecords.registerMutex.Lock() + defer r.temporaryUserRecords.registerMutex.Unlock() + + // Check if there is a temporary user with the same login name. + _, err = r.temporaryUserRecords.userByName(name) + if err != nil && !errors.Is(err, NoDataFoundError{}) { + return 0, nil, fmt.Errorf("could not check if temporary user %q already exists: %w", name, err) + } + if err == nil { + return 0, nil, fmt.Errorf("user %q already exists", name) + } + + // Check if there is a pre-auth user with the same login name. + user, err := r.preAuthUserRecords.userByLogin(name) + if err != nil && !errors.Is(err, NoDataFoundError{}) { + return 0, nil, fmt.Errorf("could not check if pre-auth user %q already exists: %w", name, err) + } + if err == nil { + // There is a pre-auth user with the same login name. Now that the user authenticated successfully, we can + // replace the pre-auth user with a temporary user. + var tmpID string + tmpID, cleanup, err = r.addTemporaryUser(user.UID, name) + if err != nil { + return 0, nil, fmt.Errorf("could not add temporary user record: %w", err) + } + + // Remove the pre-auth user from the pre-auth user records. + r.deletePreAuthUser(user.UID) + + // Check if the UID and name are unique. + unique, err := r.temporaryUserRecords.uniqueNameAndUID(name, user.UID, tmpID) + if err != nil { + err = fmt.Errorf("checking UID and name uniqueness: %w", err) + if cleanupErr := cleanup(); cleanupErr != nil { + err = errors.Join(err, cleanupErr) + } + return 0, nil, err + } + if !unique { + err = fmt.Errorf("UID (%d) or name (%q) from pre-auth user are not unique", user.UID, name) + if cleanupErr := cleanup(); cleanupErr != nil { + err = errors.Join(err, cleanupErr) + } + return 0, nil, err + } + + return user.UID, cleanup, nil + } + + // Generate a UID until we find a unique one + for { + uid, err = r.idGenerator.GenerateUID() + if err != nil { + return 0, nil, err + } + + // To avoid races where a user with this UID is created by some NSS source after we checked, we register this + // UID in our NSS handler and then check if another user with the same UID exists in the system. This way we + // can guarantee that the UID is unique, under the assumption that other NSS sources don't add users with a UID + // that we already registered (if they do, there's nothing we can do about it). + var tmpID string + tmpID, cleanup, err = r.temporaryUserRecords.addTemporaryUser(uid, name) + if err != nil { + return 0, nil, fmt.Errorf("could not add temporary user record: %w", err) + } + + unique, err := r.temporaryUserRecords.uniqueNameAndUID(name, uid, tmpID) + if err != nil { + err = fmt.Errorf("checking UID and name uniqueness: %w", err) + if cleanupErr := cleanup(); cleanupErr != nil { + err = errors.Join(err, cleanupErr) + } + return 0, nil, err + } + if unique { + break + } + + // If the UID is not unique, remove the temporary user and generate a new one in the next iteration. + if err := cleanup(); err != nil { + return 0, nil, fmt.Errorf("could not remove temporary user %q: %w", name, err) + } + } + + log.Debugf(context.Background(), "Added temporary record for user %q with UID %d", name, uid) + return uid, cleanup, nil +} diff --git a/internal/users/tempentries/tempentries_test.go b/internal/users/tempentries/tempentries_test.go new file mode 100644 index 000000000..199328d8b --- /dev/null +++ b/internal/users/tempentries/tempentries_test.go @@ -0,0 +1,179 @@ +package tempentries + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/ubuntu/authd/internal/testutils/golden" + "github.com/ubuntu/authd/internal/users/idgenerator" + "github.com/ubuntu/authd/internal/users/types" +) + +func TestRegisterUser(t *testing.T) { + t.Parallel() + + uidToGenerate := uint32(12345) + userName := "authd-temp-users-test" + + tests := map[string]struct { + userName string + uidsToGenerate []uint32 + userAlreadyRemoved bool + replacesPreAuthUser bool + preAuthUIDAlreadyExists bool + + wantErr bool + }{ + "Successfully register a new user": {}, + "Successfully register a user if the first generated UID is already in use": { + uidsToGenerate: []uint32{0, uidToGenerate}, // UID 0 (root) always exists + }, + "Successfully register a user if the pre-auth user already exists": { + replacesPreAuthUser: true, + uidsToGenerate: []uint32{}, // No UID generation needed + }, + + "Error when name is already in use": {userName: "root", wantErr: true}, + "Error when pre-auth user already exists and name is not unique": { + replacesPreAuthUser: true, + userName: "root", + wantErr: true, + }, + "Error when pre-auth UID is not unique": { + replacesPreAuthUser: true, + preAuthUIDAlreadyExists: true, + wantErr: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + if tc.userName == "" { + tc.userName = userName + } + + if tc.uidsToGenerate == nil { + tc.uidsToGenerate = []uint32{uidToGenerate} + } + + idGeneratorMock := &idgenerator.IDGeneratorMock{UIDsToGenerate: tc.uidsToGenerate} + records := NewTemporaryRecords(idGeneratorMock) + + var preAuthUID uint32 + if tc.replacesPreAuthUser { + preAuthUID = uidToGenerate + if tc.preAuthUIDAlreadyExists { + preAuthUID = 0 // UID 0 (root) always exists + } + _, _, err := records.preAuthUserRecords.addPreAuthUser(preAuthUID, tc.userName) + require.NoError(t, err, "addPreAuthUser should not return an error, but did") + } + + uid, cleanup, err := records.RegisterUser(tc.userName) + if tc.wantErr { + require.Error(t, err, "RegisterUser should return an error, but did not") + return + } + require.NoError(t, err, "RegisterUser should not return an error, but did") + require.Equal(t, uidToGenerate, uid, "UID should be the one generated by the IDGenerator") + + if tc.replacesPreAuthUser { + // Check that the pre-auth user was removed + _, err = records.preAuthUserRecords.userByID(preAuthUID) + require.Error(t, err, "userByID should return an error, but did not") + } + + user, err := records.UserByID(uid) + require.NoError(t, err, "UserByID should not return an error, but did") + checkUser(t, user) + + err = cleanup() + require.NoError(t, err, "Cleanup should not return an error, but did") + }) + } +} + +func TestUserByIDAndName(t *testing.T) { + t.Parallel() + + userName := "authd-temp-users-test" + uidToGenerate := uint32(12345) + + tests := map[string]struct { + registerUser bool + userAlreadyRemoved bool + byName bool + + wantErr bool + }{ + "Successfully get a user by ID": {registerUser: true}, + "Successfully get a user by name": {registerUser: true, byName: true}, + + "Error when user is not registered - UserByID": {wantErr: true}, + "Error when user is not registered - UserByName": {byName: true, wantErr: true}, + "Error when user is already removed - UserByID": { + registerUser: true, + userAlreadyRemoved: true, + wantErr: true, + }, + "Error when user is already removed - UserByName": { + registerUser: true, + userAlreadyRemoved: true, + byName: true, + wantErr: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + idGeneratorMock := &idgenerator.IDGeneratorMock{UIDsToGenerate: []uint32{uidToGenerate}} + records := NewTemporaryRecords(idGeneratorMock) + userRecords := records.temporaryUserRecords + + if tc.registerUser { + uid, cleanup, err := records.RegisterUser(userName) + require.NoError(t, err, "RegisterUser should not return an error, but did") + require.Equal(t, uidToGenerate, uid, "UID should be the one generated by the IDGenerator") + + if tc.userAlreadyRemoved { + err = cleanup() + require.NoError(t, err, "Cleanup should not return an error, but did") + } else { + defer func() { + err := cleanup() + require.NoError(t, err, "Cleanup should not return an error, but did") + }() + } + } + + var user types.UserEntry + var err error + if tc.byName { + user, err = userRecords.userByName(userName) + } else { + user, err = userRecords.userByID(uidToGenerate) + } + + if tc.wantErr { + require.Error(t, err, "UserByID should return an error, but did not") + return + } + require.NoError(t, err, "UserByID should not return an error, but did") + checkUser(t, user) + }) + } +} + +func checkUser(t *testing.T, user types.UserEntry) { + t.Helper() + + // The gecos field is randomly generated, so unset it before comparing the user with the golden file. + require.NotEmpty(t, user.Gecos, "Gecos should not be empty") + user.Gecos = "" + + golden.CheckOrUpdateYAML(t, user) +} diff --git a/internal/users/tempentries/testdata/golden/TestGroupByIDAndName/Successfully_get_a_group_by_ID b/internal/users/tempentries/testdata/golden/TestGroupByIDAndName/Successfully_get_a_group_by_ID new file mode 100644 index 000000000..e992cfed8 --- /dev/null +++ b/internal/users/tempentries/testdata/golden/TestGroupByIDAndName/Successfully_get_a_group_by_ID @@ -0,0 +1,4 @@ +name: authd-temp-groups-test +gid: 12345 +users: [] +passwd: "" diff --git a/internal/users/tempentries/testdata/golden/TestGroupByIDAndName/Successfully_get_a_group_by_name b/internal/users/tempentries/testdata/golden/TestGroupByIDAndName/Successfully_get_a_group_by_name new file mode 100644 index 000000000..e992cfed8 --- /dev/null +++ b/internal/users/tempentries/testdata/golden/TestGroupByIDAndName/Successfully_get_a_group_by_name @@ -0,0 +1,4 @@ +name: authd-temp-groups-test +gid: 12345 +users: [] +passwd: "" diff --git a/internal/users/tempentries/testdata/golden/TestPreAuthUser/No_error_when_registering_a_pre-auth_user_with_the_same_name b/internal/users/tempentries/testdata/golden/TestPreAuthUser/No_error_when_registering_a_pre-auth_user_with_the_same_name new file mode 100644 index 000000000..916eacf4d --- /dev/null +++ b/internal/users/tempentries/testdata/golden/TestPreAuthUser/No_error_when_registering_a_pre-auth_user_with_the_same_name @@ -0,0 +1,6 @@ +name: "" +uid: 12345 +gid: 0 +gecos: test +dir: /nonexistent +shell: /usr/sbin/nologin diff --git a/internal/users/tempentries/testdata/golden/TestPreAuthUser/Successfully_register_a_pre-auth_user b/internal/users/tempentries/testdata/golden/TestPreAuthUser/Successfully_register_a_pre-auth_user new file mode 100644 index 000000000..916eacf4d --- /dev/null +++ b/internal/users/tempentries/testdata/golden/TestPreAuthUser/Successfully_register_a_pre-auth_user @@ -0,0 +1,6 @@ +name: "" +uid: 12345 +gid: 0 +gecos: test +dir: /nonexistent +shell: /usr/sbin/nologin diff --git a/internal/users/tempentries/testdata/golden/TestPreAuthUser/Successfully_register_a_pre-auth_user_if_the_first_generated_UID_is_already_in_use b/internal/users/tempentries/testdata/golden/TestPreAuthUser/Successfully_register_a_pre-auth_user_if_the_first_generated_UID_is_already_in_use new file mode 100644 index 000000000..916eacf4d --- /dev/null +++ b/internal/users/tempentries/testdata/golden/TestPreAuthUser/Successfully_register_a_pre-auth_user_if_the_first_generated_UID_is_already_in_use @@ -0,0 +1,6 @@ +name: "" +uid: 12345 +gid: 0 +gecos: test +dir: /nonexistent +shell: /usr/sbin/nologin diff --git a/internal/users/tempentries/testdata/golden/TestPreAuthUserByIDAndName/Successfully_get_a_user_by_ID_and_name b/internal/users/tempentries/testdata/golden/TestPreAuthUserByIDAndName/Successfully_get_a_user_by_ID_and_name new file mode 100644 index 000000000..916eacf4d --- /dev/null +++ b/internal/users/tempentries/testdata/golden/TestPreAuthUserByIDAndName/Successfully_get_a_user_by_ID_and_name @@ -0,0 +1,6 @@ +name: "" +uid: 12345 +gid: 0 +gecos: test +dir: /nonexistent +shell: /usr/sbin/nologin diff --git a/internal/users/tempentries/testdata/golden/TestRegisterGroup/Successfully_register_a_group_if_the_first_generated_GID_is_already_in_use b/internal/users/tempentries/testdata/golden/TestRegisterGroup/Successfully_register_a_group_if_the_first_generated_GID_is_already_in_use new file mode 100644 index 000000000..e992cfed8 --- /dev/null +++ b/internal/users/tempentries/testdata/golden/TestRegisterGroup/Successfully_register_a_group_if_the_first_generated_GID_is_already_in_use @@ -0,0 +1,4 @@ +name: authd-temp-groups-test +gid: 12345 +users: [] +passwd: "" diff --git a/internal/users/tempentries/testdata/golden/TestRegisterGroup/Successfully_register_a_new_group b/internal/users/tempentries/testdata/golden/TestRegisterGroup/Successfully_register_a_new_group new file mode 100644 index 000000000..e992cfed8 --- /dev/null +++ b/internal/users/tempentries/testdata/golden/TestRegisterGroup/Successfully_register_a_new_group @@ -0,0 +1,4 @@ +name: authd-temp-groups-test +gid: 12345 +users: [] +passwd: "" diff --git a/internal/users/tempentries/testdata/golden/TestRegisterUser/Successfully_register_a_new_user b/internal/users/tempentries/testdata/golden/TestRegisterUser/Successfully_register_a_new_user new file mode 100644 index 000000000..c6c36066f --- /dev/null +++ b/internal/users/tempentries/testdata/golden/TestRegisterUser/Successfully_register_a_new_user @@ -0,0 +1,6 @@ +name: authd-temp-users-test +uid: 12345 +gid: 0 +gecos: "" +dir: /nonexistent +shell: /usr/sbin/nologin diff --git a/internal/users/tempentries/testdata/golden/TestRegisterUser/Successfully_register_a_user_if_the_first_generated_UID_is_already_in_use b/internal/users/tempentries/testdata/golden/TestRegisterUser/Successfully_register_a_user_if_the_first_generated_UID_is_already_in_use new file mode 100644 index 000000000..c6c36066f --- /dev/null +++ b/internal/users/tempentries/testdata/golden/TestRegisterUser/Successfully_register_a_user_if_the_first_generated_UID_is_already_in_use @@ -0,0 +1,6 @@ +name: authd-temp-users-test +uid: 12345 +gid: 0 +gecos: "" +dir: /nonexistent +shell: /usr/sbin/nologin diff --git a/internal/users/tempentries/testdata/golden/TestRegisterUser/Successfully_register_a_user_if_the_pre-auth_user_already_exists b/internal/users/tempentries/testdata/golden/TestRegisterUser/Successfully_register_a_user_if_the_pre-auth_user_already_exists new file mode 100644 index 000000000..c6c36066f --- /dev/null +++ b/internal/users/tempentries/testdata/golden/TestRegisterUser/Successfully_register_a_user_if_the_pre-auth_user_already_exists @@ -0,0 +1,6 @@ +name: authd-temp-users-test +uid: 12345 +gid: 0 +gecos: "" +dir: /nonexistent +shell: /usr/sbin/nologin diff --git a/internal/users/tempentries/testdata/golden/TestUserByIDAndName/Successfully_get_a_user_by_ID b/internal/users/tempentries/testdata/golden/TestUserByIDAndName/Successfully_get_a_user_by_ID new file mode 100644 index 000000000..c6c36066f --- /dev/null +++ b/internal/users/tempentries/testdata/golden/TestUserByIDAndName/Successfully_get_a_user_by_ID @@ -0,0 +1,6 @@ +name: authd-temp-users-test +uid: 12345 +gid: 0 +gecos: "" +dir: /nonexistent +shell: /usr/sbin/nologin diff --git a/internal/users/tempentries/testdata/golden/TestUserByIDAndName/Successfully_get_a_user_by_name b/internal/users/tempentries/testdata/golden/TestUserByIDAndName/Successfully_get_a_user_by_name new file mode 100644 index 000000000..c6c36066f --- /dev/null +++ b/internal/users/tempentries/testdata/golden/TestUserByIDAndName/Successfully_get_a_user_by_name @@ -0,0 +1,6 @@ +name: authd-temp-users-test +uid: 12345 +gid: 0 +gecos: "" +dir: /nonexistent +shell: /usr/sbin/nologin diff --git a/internal/users/tempentries/users.go b/internal/users/tempentries/users.go new file mode 100644 index 000000000..2db11dee3 --- /dev/null +++ b/internal/users/tempentries/users.go @@ -0,0 +1,133 @@ +package tempentries + +import ( + "context" + "crypto/rand" + "fmt" + "sync" + + "github.com/ubuntu/authd/internal/log" + "github.com/ubuntu/authd/internal/users/localentries" + "github.com/ubuntu/authd/internal/users/types" +) + +type userRecord struct { + name string + uid uint32 + gecos string +} + +type temporaryUserRecords struct { + idGenerator IDGenerator + registerMutex sync.Mutex + rwMutex sync.RWMutex + users map[uint32]userRecord + uidByName map[string]uint32 +} + +func newTemporaryUserRecords(idGenerator IDGenerator) *temporaryUserRecords { + return &temporaryUserRecords{ + idGenerator: idGenerator, + registerMutex: sync.Mutex{}, + rwMutex: sync.RWMutex{}, + users: make(map[uint32]userRecord), + uidByName: make(map[string]uint32), + } +} + +// UserByID returns the user information for the given user ID. +func (r *temporaryUserRecords) userByID(uid uint32) (types.UserEntry, error) { + r.rwMutex.RLock() + defer r.rwMutex.RUnlock() + + user, ok := r.users[uid] + if !ok { + return types.UserEntry{}, NoDataFoundError{} + } + + return r.userEntry(user), nil +} + +// UserByName returns the user information for the given user name. +func (r *temporaryUserRecords) userByName(name string) (types.UserEntry, error) { + r.rwMutex.RLock() + defer r.rwMutex.RUnlock() + + uid, ok := r.uidByName[name] + if !ok { + return types.UserEntry{}, NoDataFoundError{} + } + + return r.userByID(uid) +} + +func (r *temporaryUserRecords) userEntry(user userRecord) types.UserEntry { + // TODO: Should we set the GID to something else than 0 (i.e. the GID of the root primary group)? + return types.UserEntry{ + Name: user.name, + UID: user.uid, + Gecos: user.gecos, + Dir: "/nonexistent", + Shell: "/usr/sbin/nologin", + } +} + +// uniqueNameAndUID returns true if the given UID is unique in the system. It returns false if the UID is already assigned to +// a user by any NSS source (except the given temporary user). +func (r *temporaryUserRecords) uniqueNameAndUID(name string, uid uint32, tmpID string) (bool, error) { + entries, err := localentries.GetPasswdEntries() + if err != nil { + return false, err + } + for _, entry := range entries { + if entry.Name == name && entry.UID != uid { + // A user with the same name already exists, we can't register this temporary user. + log.Debugf(context.Background(), "Name %q already in use by UID %d", name, entry.UID) + return false, fmt.Errorf("user %q already exists", name) + } + + if entry.UID == uid && entry.Gecos != tmpID { + log.Debugf(context.Background(), "UID %d already in use by user %q, generating a new one", uid, entry.Name) + return false, nil + } + } + return true, nil +} + +// addTemporaryUser adds a temporary user with a random name and the given UID. It returns the generated name. +// If the UID is already registered, it returns a errUserAlreadyExists. +func (r *temporaryUserRecords) addTemporaryUser(uid uint32, name string) (tmpID string, cleanup func() error, err error) { + r.rwMutex.Lock() + defer r.rwMutex.Unlock() + + // Generate a 64 character (32 bytes in hex) random ID which we store in the gecos field of the temporary user + // record to be able to identify it in isUniqueUID. + bytes := make([]byte, 32) + if _, err := rand.Read(bytes); err != nil { + return "", nil, fmt.Errorf("failed to generate random name: %w", err) + } + tmpID = fmt.Sprintf("authd-temp-user-%x", bytes) + + r.users[uid] = userRecord{name: name, uid: uid, gecos: tmpID} + r.uidByName[name] = uid + + cleanup = func() error { return r.deleteTemporaryUser(uid) } + + return tmpID, cleanup, nil +} + +// deleteTemporaryUser deletes the temporary user with the given UID. +func (r *temporaryUserRecords) deleteTemporaryUser(uid uint32) error { + r.rwMutex.Lock() + defer r.rwMutex.Unlock() + + user, ok := r.users[uid] + if !ok { + return fmt.Errorf("temporary user with UID %d does not exist", uid) + } + delete(r.users, uid) + delete(r.uidByName, user.name) + + log.Debugf(context.Background(), "Removed temporary record for user %q with UID %d", user.name, uid) + return nil +} diff --git a/internal/users/testdata/golden/TestAllGroups/Successfully_get_all_groups b/internal/users/testdata/golden/TestAllGroups/Successfully_get_all_groups index e9e8c1a25..d44e911cb 100644 --- a/internal/users/testdata/golden/TestAllGroups/Successfully_get_all_groups +++ b/internal/users/testdata/golden/TestAllGroups/Successfully_get_all_groups @@ -2,20 +2,25 @@ gid: 11111 users: - user1 + passwd: "" - name: group2 gid: 22222 users: - user2 + passwd: "" - name: group3 gid: 33333 users: - user3 + passwd: "" - name: group4 gid: 44444 users: - userwithoutbroker + passwd: "" - name: commongroup gid: 99999 users: - user2 - user3 + passwd: "" diff --git a/internal/users/testdata/golden/TestGroupByIDAndName/Successfully_get_group_by_ID b/internal/users/testdata/golden/TestGroupByIDAndName/Successfully_get_group_by_ID index 6af5b8288..deaa6af10 100644 --- a/internal/users/testdata/golden/TestGroupByIDAndName/Successfully_get_group_by_ID +++ b/internal/users/testdata/golden/TestGroupByIDAndName/Successfully_get_group_by_ID @@ -2,3 +2,4 @@ name: group1 gid: 11111 users: - user1 +passwd: "" diff --git a/internal/users/testdata/golden/TestGroupByIDAndName/Successfully_get_group_by_name b/internal/users/testdata/golden/TestGroupByIDAndName/Successfully_get_group_by_name index 6af5b8288..deaa6af10 100644 --- a/internal/users/testdata/golden/TestGroupByIDAndName/Successfully_get_group_by_name +++ b/internal/users/testdata/golden/TestGroupByIDAndName/Successfully_get_group_by_name @@ -2,3 +2,4 @@ name: group1 gid: 11111 users: - user1 +passwd: "" diff --git a/internal/users/testdata/golden/TestGroupByIDAndName/Successfully_get_temporary_group_by_ID b/internal/users/testdata/golden/TestGroupByIDAndName/Successfully_get_temporary_group_by_ID new file mode 100644 index 000000000..6a90d9b2a --- /dev/null +++ b/internal/users/testdata/golden/TestGroupByIDAndName/Successfully_get_temporary_group_by_ID @@ -0,0 +1,4 @@ +name: tempgroup1 +gid: 0 +users: [] +passwd: "" diff --git a/internal/users/testdata/golden/TestGroupByIDAndName/Successfully_get_temporary_group_by_name b/internal/users/testdata/golden/TestGroupByIDAndName/Successfully_get_temporary_group_by_name new file mode 100644 index 000000000..6a90d9b2a --- /dev/null +++ b/internal/users/testdata/golden/TestGroupByIDAndName/Successfully_get_temporary_group_by_name @@ -0,0 +1,4 @@ +name: tempgroup1 +gid: 0 +users: [] +passwd: "" 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 new file mode 100644 index 000000000..a0804f90f --- /dev/null +++ b/internal/users/testdata/golden/TestNewManager/Successfully_create_manager_with_custom_config @@ -0,0 +1,44 @@ +GroupByID: + "11111": '{"Name":"group1","GID":11111,"UGID":"12345678"}' + "22222": '{"Name":"group2","GID":22222,"UGID":"56781234"}' + "33333": '{"Name":"group3","GID":33333,"UGID":"34567812"}' + "44444": '{"Name":"group4","GID":44444,"UGID":"45678123"}' + "99999": '{"Name":"commongroup","GID":99999,"UGID":"87654321"}' +GroupByName: + commongroup: '{"Name":"commongroup","GID":99999,"UGID":"87654321"}' + group1: '{"Name":"group1","GID":11111,"UGID":"12345678"}' + group2: '{"Name":"group2","GID":22222,"UGID":"56781234"}' + group3: '{"Name":"group3","GID":33333,"UGID":"34567812"}' + group4: '{"Name":"group4","GID":44444,"UGID":"45678123"}' +GroupByUGID: + "12345678": '{"Name":"group1","GID":11111,"UGID":"12345678"}' + "34567812": '{"Name":"group3","GID":33333,"UGID":"34567812"}' + "45678123": '{"Name":"group4","GID":44444,"UGID":"45678123"}' + "56781234": '{"Name":"group2","GID":22222,"UGID":"56781234"}' + "87654321": '{"Name":"commongroup","GID":99999,"UGID":"87654321"}' +GroupToUsers: + "11111": '{"GID":11111,"UIDs":[1111]}' + "22222": '{"GID":22222,"UIDs":[2222]}' + "33333": '{"GID":33333,"UIDs":[3333]}' + "44444": '{"GID":33333,"UIDs":[4444]}' + "99999": '{"GID":99999,"UIDs":[2222,3333]}' +UserByID: + "1111": '{"Name":"user1","UID":1111,"GID":11111,"Gecos":"User1 gecos\nOn multiple lines","Dir":"/home/user1","Shell":"/bin/bash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"AAAAATIME"}' + "2222": '{"Name":"user2","UID":2222,"GID":22222,"Gecos":"User2","Dir":"/home/user2","Shell":"/bin/dash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"BBBBBTIME"}' + "3333": '{"Name":"user3","UID":3333,"GID":33333,"Gecos":"User3","Dir":"/home/user3","Shell":"/bin/zsh","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + "4444": '{"Name":"userwithoutbroker","UID":4444,"GID":44444,"Gecos":"userwithoutbroker","Dir":"/home/userwithoutbroker","Shell":"/bin/sh","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' +UserByName: + user1: '{"Name":"user1","UID":1111,"GID":11111,"Gecos":"User1 gecos\nOn multiple lines","Dir":"/home/user1","Shell":"/bin/bash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"AAAAATIME"}' + user2: '{"Name":"user2","UID":2222,"GID":22222,"Gecos":"User2","Dir":"/home/user2","Shell":"/bin/dash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"BBBBBTIME"}' + user3: '{"Name":"user3","UID":3333,"GID":33333,"Gecos":"User3","Dir":"/home/user3","Shell":"/bin/zsh","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + userwithoutbroker: '{"Name":"userwithoutbroker","UID":4444,"GID":44444,"Gecos":"userwithoutbroker","Dir":"/home/userwithoutbroker","Shell":"/bin/sh","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' +UserToBroker: + "1111": '"broker-id"' + "2222": '"broker-id"' + "3333": '"broker-id"' +UserToGroups: + "1111": '{"UID":1111,"GIDs":[11111]}' + "2222": '{"UID":2222,"GIDs":[22222,99999]}' + "3333": '{"UID":3333,"GIDs":[33333,99999]}' + "4444": '{"UID":4444,"GIDs":[44444]}' +UserToLocalGroups: {} 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 new file mode 100644 index 000000000..a0804f90f --- /dev/null +++ b/internal/users/testdata/golden/TestNewManager/Successfully_create_manager_with_default_config @@ -0,0 +1,44 @@ +GroupByID: + "11111": '{"Name":"group1","GID":11111,"UGID":"12345678"}' + "22222": '{"Name":"group2","GID":22222,"UGID":"56781234"}' + "33333": '{"Name":"group3","GID":33333,"UGID":"34567812"}' + "44444": '{"Name":"group4","GID":44444,"UGID":"45678123"}' + "99999": '{"Name":"commongroup","GID":99999,"UGID":"87654321"}' +GroupByName: + commongroup: '{"Name":"commongroup","GID":99999,"UGID":"87654321"}' + group1: '{"Name":"group1","GID":11111,"UGID":"12345678"}' + group2: '{"Name":"group2","GID":22222,"UGID":"56781234"}' + group3: '{"Name":"group3","GID":33333,"UGID":"34567812"}' + group4: '{"Name":"group4","GID":44444,"UGID":"45678123"}' +GroupByUGID: + "12345678": '{"Name":"group1","GID":11111,"UGID":"12345678"}' + "34567812": '{"Name":"group3","GID":33333,"UGID":"34567812"}' + "45678123": '{"Name":"group4","GID":44444,"UGID":"45678123"}' + "56781234": '{"Name":"group2","GID":22222,"UGID":"56781234"}' + "87654321": '{"Name":"commongroup","GID":99999,"UGID":"87654321"}' +GroupToUsers: + "11111": '{"GID":11111,"UIDs":[1111]}' + "22222": '{"GID":22222,"UIDs":[2222]}' + "33333": '{"GID":33333,"UIDs":[3333]}' + "44444": '{"GID":33333,"UIDs":[4444]}' + "99999": '{"GID":99999,"UIDs":[2222,3333]}' +UserByID: + "1111": '{"Name":"user1","UID":1111,"GID":11111,"Gecos":"User1 gecos\nOn multiple lines","Dir":"/home/user1","Shell":"/bin/bash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"AAAAATIME"}' + "2222": '{"Name":"user2","UID":2222,"GID":22222,"Gecos":"User2","Dir":"/home/user2","Shell":"/bin/dash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"BBBBBTIME"}' + "3333": '{"Name":"user3","UID":3333,"GID":33333,"Gecos":"User3","Dir":"/home/user3","Shell":"/bin/zsh","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + "4444": '{"Name":"userwithoutbroker","UID":4444,"GID":44444,"Gecos":"userwithoutbroker","Dir":"/home/userwithoutbroker","Shell":"/bin/sh","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' +UserByName: + user1: '{"Name":"user1","UID":1111,"GID":11111,"Gecos":"User1 gecos\nOn multiple lines","Dir":"/home/user1","Shell":"/bin/bash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"AAAAATIME"}' + user2: '{"Name":"user2","UID":2222,"GID":22222,"Gecos":"User2","Dir":"/home/user2","Shell":"/bin/dash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"BBBBBTIME"}' + user3: '{"Name":"user3","UID":3333,"GID":33333,"Gecos":"User3","Dir":"/home/user3","Shell":"/bin/zsh","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + userwithoutbroker: '{"Name":"userwithoutbroker","UID":4444,"GID":44444,"Gecos":"userwithoutbroker","Dir":"/home/userwithoutbroker","Shell":"/bin/sh","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' +UserToBroker: + "1111": '"broker-id"' + "2222": '"broker-id"' + "3333": '"broker-id"' +UserToGroups: + "1111": '{"UID":1111,"GIDs":[11111]}' + "2222": '{"UID":2222,"GIDs":[22222,99999]}' + "3333": '{"UID":3333,"GIDs":[33333,99999]}' + "4444": '{"UID":4444,"GIDs":[44444]}' +UserToLocalGroups: {} diff --git a/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user b/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user index 19f5d05e1..356a3a901 100644 --- a/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user +++ b/internal/users/testdata/golden/TestUpdateUser/Successfully_update_user @@ -1,22 +1,22 @@ | GroupByID: + "1111": '{"Name":"user1","GID":1111,"UGID":"user1"}' "11111": '{"Name":"group1","GID":11111,"UGID":"1"}' - "1526760316": '{"Name":"user1","GID":1526760316,"UGID":"user1"}' GroupByName: group1: '{"Name":"group1","GID":11111,"UGID":"1"}' - user1: '{"Name":"user1","GID":1526760316,"UGID":"user1"}' + user1: '{"Name":"user1","GID":1111,"UGID":"user1"}' GroupByUGID: "1": '{"Name":"group1","GID":11111,"UGID":"1"}' - user1: '{"Name":"user1","GID":1526760316,"UGID":"user1"}' + user1: '{"Name":"user1","GID":1111,"UGID":"user1"}' GroupToUsers: + "1111": '{"GID":1111,"UIDs":[1111]}' "11111": '{"GID":11111,"UIDs":[1111]}' - "1526760316": '{"GID":1526760316,"UIDs":[1111]}' UserByID: - "1111": '{"Name":"user1","UID":1111,"GID":1526760316,"Gecos":"gecos for user1","Dir":"/home/user1","Shell":"/bin/bash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + "1111": '{"Name":"user1","UID":1111,"GID":1111,"Gecos":"gecos for user1","Dir":"/home/user1","Shell":"/bin/bash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' UserByName: - user1: '{"Name":"user1","UID":1111,"GID":1526760316,"Gecos":"gecos for user1","Dir":"/home/user1","Shell":"/bin/bash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + user1: '{"Name":"user1","UID":1111,"GID":1111,"Gecos":"gecos for user1","Dir":"/home/user1","Shell":"/bin/bash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' UserToBroker: {} UserToGroups: - "1111": '{"UID":1111,"GIDs":[1526760316,11111]}' + "1111": '{"UID":1111,"GIDs":[1111,11111]}' UserToLocalGroups: "1111": "null" 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 fbaf5853f..203f44299 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 @@ -1,22 +1,22 @@ | GroupByID: + "1111": '{"Name":"user1","GID":1111,"UGID":"user1"}' "11111": '{"Name":"group1","GID":11111,"UGID":"1"}' - "1526760316": '{"Name":"user1","GID":1526760316,"UGID":"user1"}' GroupByName: group1: '{"Name":"group1","GID":11111,"UGID":"1"}' - user1: '{"Name":"user1","GID":1526760316,"UGID":"user1"}' + user1: '{"Name":"user1","GID":1111,"UGID":"user1"}' GroupByUGID: "1": '{"Name":"group1","GID":11111,"UGID":"1"}' - user1: '{"Name":"user1","GID":1526760316,"UGID":"user1"}' + user1: '{"Name":"user1","GID":1111,"UGID":"user1"}' GroupToUsers: + "1111": '{"GID":1111,"UIDs":[1111]}' "11111": '{"GID":11111,"UIDs":[1111]}' - "1526760316": '{"GID":1526760316,"UIDs":[1111]}' UserByID: - "1111": '{"Name":"user1","UID":1111,"GID":1526760316,"Gecos":"gecos for user1","Dir":"/home/user1","Shell":"/bin/bash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + "1111": '{"Name":"user1","UID":1111,"GID":1111,"Gecos":"gecos for user1","Dir":"/home/user1","Shell":"/bin/bash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' UserByName: - user1: '{"Name":"user1","UID":1111,"GID":1526760316,"Gecos":"gecos for user1","Dir":"/home/user1","Shell":"/bin/bash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + user1: '{"Name":"user1","UID":1111,"GID":1111,"Gecos":"gecos for user1","Dir":"/home/user1","Shell":"/bin/bash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' UserToBroker: {} UserToGroups: - "1111": '{"UID":1111,"GIDs":[1526760316,11111]}' + "1111": '{"UID":1111,"GIDs":[1111,11111]}' UserToLocalGroups: "1111": '["localgroup1"]' 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 9e04959fd..1c38ff1fd 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 @@ -1,19 +1,19 @@ | GroupByID: - "1526760316": '{"Name":"user1","GID":1526760316,"UGID":"user1"}' + "3333": '{"Name":"user1","GID":3333,"UGID":"user1"}' GroupByName: - user1: '{"Name":"user1","GID":1526760316,"UGID":"user1"}' + user1: '{"Name":"user1","GID":3333,"UGID":"user1"}' GroupByUGID: - user1: '{"Name":"user1","GID":1526760316,"UGID":"user1"}' + user1: '{"Name":"user1","GID":3333,"UGID":"user1"}' GroupToUsers: - "1526760316": '{"GID":1526760316,"UIDs":[1111]}' + "3333": '{"GID":3333,"UIDs":[1111]}' UserByID: - "1111": '{"Name":"user1","UID":1111,"GID":1526760316,"Gecos":"gecos for user1","Dir":"/home/user1","Shell":"/bin/bash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + "1111": '{"Name":"user1","UID":1111,"GID":3333,"Gecos":"gecos for user1","Dir":"/home/user1","Shell":"/bin/bash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' UserByName: - user1: '{"Name":"user1","UID":1111,"GID":1526760316,"Gecos":"gecos for user1","Dir":"/home/user1","Shell":"/bin/bash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' + user1: '{"Name":"user1","UID":1111,"GID":3333,"Gecos":"gecos for user1","Dir":"/home/user1","Shell":"/bin/bash","LastPwdChange":-1,"MaxPwdAge":-1,"PwdWarnPeriod":-1,"PwdInactivity":-1,"MinPwdAge":-1,"ExpirationDate":-1,"LastLogin":"ABCDETIME"}' UserToBroker: "1111": '"broker-id"' UserToGroups: - "1111": '{"UID":1111,"GIDs":[1526760316]}' + "1111": '{"UID":1111,"GIDs":[3333]}' UserToLocalGroups: "1111": "null" diff --git a/internal/users/testdata/golden/TestUserByIDAndName/Successfully_get_temporary_user_by_ID b/internal/users/testdata/golden/TestUserByIDAndName/Successfully_get_temporary_user_by_ID new file mode 100644 index 000000000..8c4eed4c4 --- /dev/null +++ b/internal/users/testdata/golden/TestUserByIDAndName/Successfully_get_temporary_user_by_ID @@ -0,0 +1,6 @@ +name: tempuser1 +uid: 0 +gid: 0 +gecos: "" +dir: /nonexistent +shell: /usr/sbin/nologin diff --git a/internal/users/testdata/golden/TestUserByIDAndName/Successfully_get_temporary_user_by_name b/internal/users/testdata/golden/TestUserByIDAndName/Successfully_get_temporary_user_by_name new file mode 100644 index 000000000..8c4eed4c4 --- /dev/null +++ b/internal/users/testdata/golden/TestUserByIDAndName/Successfully_get_temporary_user_by_name @@ -0,0 +1,6 @@ +name: tempuser1 +uid: 0 +gid: 0 +gecos: "" +dir: /nonexistent +shell: /usr/sbin/nologin diff --git a/internal/users/types/types.go b/internal/users/types/types.go new file mode 100644 index 000000000..a82234ca4 --- /dev/null +++ b/internal/users/types/types.go @@ -0,0 +1,49 @@ +// Package types provides types for the users package. +package types + +// UserInfo is the user information returned by the broker. +type UserInfo struct { + Name string + UID uint32 + Gecos string + Dir string + Shell string + + Groups []GroupInfo +} + +// GroupInfo is the group information returned by the broker. +type GroupInfo struct { + Name string + GID *uint32 + UGID string +} + +// UserEntry is the user information sent to the NSS service. +type UserEntry struct { + Name string + UID uint32 + GID uint32 + Gecos string + Dir string + Shell string +} + +// ShadowEntry is the shadow information sent to the NSS service. +type ShadowEntry struct { + Name string + LastPwdChange int + MaxPwdAge int + PwdWarnPeriod int + PwdInactivity int + MinPwdAge int + ExpirationDate int +} + +// GroupEntry is the group information sent to the NSS service. +type GroupEntry struct { + Name string + GID uint32 + Users []string + Passwd string +} diff --git a/nss/integration-tests/integration_test.go b/nss/integration-tests/integration_test.go index 5cf9a7eb7..121f19805 100644 --- a/nss/integration-tests/integration_test.go +++ b/nss/integration-tests/integration_test.go @@ -5,12 +5,13 @@ import ( "log" "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/require" "github.com/ubuntu/authd/internal/testutils" "github.com/ubuntu/authd/internal/testutils/golden" - localgroupstestutils "github.com/ubuntu/authd/internal/users/localgroups/testutils" + localgroupstestutils "github.com/ubuntu/authd/internal/users/localentries/testutils" ) var daemonPath string @@ -148,6 +149,18 @@ func TestIntegration(t *testing.T) { got, status := getentOutputForLib(t, libPath, socketPath, rustCovEnv, tc.shouldPreCheck, cmds...) require.Equal(t, tc.wantStatus, status, "Expected status %d, but got %d", tc.wantStatus, status) + if tc.shouldPreCheck && tc.db == "passwd" { + // When pre-checking, the `getent passwd` output contains a randomly generated UID. + // To make the test deterministic, we replace the UID with a placeholder. + // The output looks something like this: + // user-pre-check:x:1776689191:0:gecos for user-pre-check:/home/user-pre-check:/usr/bin/bash\n + fields := strings.Split(got, ":") + require.Len(t, fields, 7, "Invalid number of fields in the output: %q", got) + // The UID is the third field. + fields[2] = "{{UID}}" + got = strings.Join(fields, ":") + } + // If the exit status is NotFound, there is no need to create an empty golden file. // But we need to ensure that the output is indeed empty. if tc.wantStatus == codeNotFound { diff --git a/nss/integration-tests/testdata/golden/TestIntegration/Check_user_with_broker_if_not_found_in_cache b/nss/integration-tests/testdata/golden/TestIntegration/Check_user_with_broker_if_not_found_in_cache index 0bfd72201..607538571 100644 --- a/nss/integration-tests/testdata/golden/TestIntegration/Check_user_with_broker_if_not_found_in_cache +++ b/nss/integration-tests/testdata/golden/TestIntegration/Check_user_with_broker_if_not_found_in_cache @@ -1 +1 @@ -user-pre-check:x:1053432963:1053432963:gecos for user-pre-check:/home/user-pre-check:/usr/bin/bash +user-pre-check:x:{{UID}}:0:gecos for user-pre-check:/home/user-pre-check:/usr/bin/bash diff --git a/nss/integration-tests/testdata/golden/TestIntegration/Get_all_entries_from_group b/nss/integration-tests/testdata/golden/TestIntegration/Get_all_entries_from_group index 30341ab54..9c872c190 100644 --- a/nss/integration-tests/testdata/golden/TestIntegration/Get_all_entries_from_group +++ b/nss/integration-tests/testdata/golden/TestIntegration/Get_all_entries_from_group @@ -1,4 +1,4 @@ -group1:x:11111:user1 -group2:x:22222:user2 -group3:x:33333:user3 -commongroup:x:99999:user2,user3 +group1::11111:user1 +group2::22222:user2 +group3::33333:user3 +commongroup::99999:user2,user3 diff --git a/nss/integration-tests/testdata/golden/TestIntegration/Get_entry_from_group_by_id b/nss/integration-tests/testdata/golden/TestIntegration/Get_entry_from_group_by_id index 8d0d48eb2..f7ab6edf8 100644 --- a/nss/integration-tests/testdata/golden/TestIntegration/Get_entry_from_group_by_id +++ b/nss/integration-tests/testdata/golden/TestIntegration/Get_entry_from_group_by_id @@ -1 +1 @@ -group1:x:11111:user1 +group1::11111:user1 diff --git a/nss/integration-tests/testdata/golden/TestIntegration/Get_entry_from_group_by_name b/nss/integration-tests/testdata/golden/TestIntegration/Get_entry_from_group_by_name index 8d0d48eb2..f7ab6edf8 100644 --- a/nss/integration-tests/testdata/golden/TestIntegration/Get_entry_from_group_by_name +++ b/nss/integration-tests/testdata/golden/TestIntegration/Get_entry_from_group_by_name @@ -1 +1 @@ -group1:x:11111:user1 +group1::11111:user1 diff --git a/pam/integration-tests/cli_test.go b/pam/integration-tests/cli_test.go index 6fa89f3f9..cce58c9c0 100644 --- a/pam/integration-tests/cli_test.go +++ b/pam/integration-tests/cli_test.go @@ -13,7 +13,7 @@ import ( "github.com/ubuntu/authd/internal/proto/authd" "github.com/ubuntu/authd/internal/testutils" "github.com/ubuntu/authd/internal/testutils/golden" - localgroupstestutils "github.com/ubuntu/authd/internal/users/localgroups/testutils" + localgroupstestutils "github.com/ubuntu/authd/internal/users/localentries/testutils" "github.com/ubuntu/authd/pam/internal/pam_test" ) diff --git a/pam/integration-tests/helpers_test.go b/pam/integration-tests/helpers_test.go index 111ce2049..82594ebf6 100644 --- a/pam/integration-tests/helpers_test.go +++ b/pam/integration-tests/helpers_test.go @@ -21,7 +21,7 @@ import ( "github.com/ubuntu/authd/internal/services/errmessages" "github.com/ubuntu/authd/internal/testutils" "github.com/ubuntu/authd/internal/testutils/golden" - localgroupstestutils "github.com/ubuntu/authd/internal/users/localgroups/testutils" + localgroupstestutils "github.com/ubuntu/authd/internal/users/localentries/testutils" "github.com/ubuntu/authd/pam/internal/pam_test" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" diff --git a/pam/integration-tests/native_test.go b/pam/integration-tests/native_test.go index c02bf15b5..5106e612e 100644 --- a/pam/integration-tests/native_test.go +++ b/pam/integration-tests/native_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/ubuntu/authd/internal/proto/authd" "github.com/ubuntu/authd/internal/testutils/golden" - localgroupstestutils "github.com/ubuntu/authd/internal/users/localgroups/testutils" + localgroupstestutils "github.com/ubuntu/authd/internal/users/localentries/testutils" "github.com/ubuntu/authd/pam/internal/pam_test" ) diff --git a/pam/integration-tests/ssh_test.go b/pam/integration-tests/ssh_test.go index 596d5b5ba..a0945789a 100644 --- a/pam/integration-tests/ssh_test.go +++ b/pam/integration-tests/ssh_test.go @@ -23,7 +23,7 @@ import ( "github.com/stretchr/testify/require" "github.com/ubuntu/authd/internal/testutils" "github.com/ubuntu/authd/internal/testutils/golden" - localgroupstestutils "github.com/ubuntu/authd/internal/users/localgroups/testutils" + localgroupstestutils "github.com/ubuntu/authd/internal/users/localentries/testutils" "github.com/ubuntu/authd/pam/internal/pam_test" )