package auth_test import ( "github.com/stretchr/testify/require" "heckel.io/ntfy/auth" "path/filepath" "strings" "testing" "time" ) const minBcryptTimingMillis = int64(50) // Ideally should be >100ms, but this should also run on a Raspberry Pi without massive resources func TestSQLiteAuth_FullScenario_Default_DenyAll(t *testing.T) { a := newTestAuth(t, false, false) require.Nil(t, a.AddUser("phil", "phil", auth.RoleAdmin)) require.Nil(t, a.AddUser("ben", "ben", auth.RoleUser)) require.Nil(t, a.AllowAccess("ben", "mytopic", true, true)) require.Nil(t, a.AllowAccess("ben", "readme", true, false)) require.Nil(t, a.AllowAccess("ben", "writeme", false, true)) require.Nil(t, a.AllowAccess("ben", "everyonewrite", false, false)) // How unfair! require.Nil(t, a.AllowAccess(auth.Everyone, "announcements", true, false)) require.Nil(t, a.AllowAccess(auth.Everyone, "everyonewrite", true, true)) require.Nil(t, a.AllowAccess(auth.Everyone, "up*", false, true)) // Everyone can write to /up* phil, err := a.Authenticate("phil", "phil") require.Nil(t, err) require.Equal(t, "phil", phil.Name) require.True(t, strings.HasPrefix(phil.Hash, "$2a$10$")) require.Equal(t, auth.RoleAdmin, phil.Role) require.Equal(t, []auth.Grant{}, phil.Grants) ben, err := a.Authenticate("ben", "ben") require.Nil(t, err) require.Equal(t, "ben", ben.Name) require.True(t, strings.HasPrefix(ben.Hash, "$2a$10$")) require.Equal(t, auth.RoleUser, ben.Role) require.Equal(t, []auth.Grant{ {"mytopic", true, true}, {"readme", true, false}, {"writeme", false, true}, {"everyonewrite", false, false}, }, ben.Grants) notben, err := a.Authenticate("ben", "this is wrong") require.Nil(t, notben) require.Equal(t, auth.ErrUnauthenticated, err) // Admin can do everything require.Nil(t, a.Authorize(phil, "sometopic", auth.PermissionWrite)) require.Nil(t, a.Authorize(phil, "mytopic", auth.PermissionRead)) require.Nil(t, a.Authorize(phil, "readme", auth.PermissionWrite)) require.Nil(t, a.Authorize(phil, "writeme", auth.PermissionWrite)) require.Nil(t, a.Authorize(phil, "announcements", auth.PermissionWrite)) require.Nil(t, a.Authorize(phil, "everyonewrite", auth.PermissionWrite)) // User cannot do everything require.Nil(t, a.Authorize(ben, "mytopic", auth.PermissionWrite)) require.Nil(t, a.Authorize(ben, "mytopic", auth.PermissionRead)) require.Nil(t, a.Authorize(ben, "readme", auth.PermissionRead)) require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "readme", auth.PermissionWrite)) require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "writeme", auth.PermissionRead)) require.Nil(t, a.Authorize(ben, "writeme", auth.PermissionWrite)) require.Nil(t, a.Authorize(ben, "writeme", auth.PermissionWrite)) require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "everyonewrite", auth.PermissionRead)) require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "everyonewrite", auth.PermissionWrite)) require.Nil(t, a.Authorize(ben, "announcements", auth.PermissionRead)) require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "announcements", auth.PermissionWrite)) // Everyone else can do barely anything require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "sometopicnotinthelist", auth.PermissionRead)) require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "sometopicnotinthelist", auth.PermissionWrite)) require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "mytopic", auth.PermissionRead)) require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "mytopic", auth.PermissionWrite)) require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "readme", auth.PermissionRead)) require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "readme", auth.PermissionWrite)) require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "writeme", auth.PermissionRead)) require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "writeme", auth.PermissionWrite)) require.Equal(t, auth.ErrUnauthorized, a.Authorize(nil, "announcements", auth.PermissionWrite)) require.Nil(t, a.Authorize(nil, "announcements", auth.PermissionRead)) require.Nil(t, a.Authorize(nil, "everyonewrite", auth.PermissionRead)) require.Nil(t, a.Authorize(nil, "everyonewrite", auth.PermissionWrite)) require.Nil(t, a.Authorize(nil, "up1234", auth.PermissionWrite)) // Wildcard permission require.Nil(t, a.Authorize(nil, "up5678", auth.PermissionWrite)) } func TestSQLiteAuth_AddUser_Invalid(t *testing.T) { a := newTestAuth(t, false, false) require.Equal(t, auth.ErrInvalidArgument, a.AddUser(" invalid ", "pass", auth.RoleAdmin)) require.Equal(t, auth.ErrInvalidArgument, a.AddUser("validuser", "pass", "invalid-role")) } func TestSQLiteAuth_AddUser_Timing(t *testing.T) { a := newTestAuth(t, false, false) start := time.Now().UnixMilli() require.Nil(t, a.AddUser("user", "pass", auth.RoleAdmin)) require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis) } func TestSQLiteAuth_Authenticate_Timing(t *testing.T) { a := newTestAuth(t, false, false) require.Nil(t, a.AddUser("user", "pass", auth.RoleAdmin)) // Timing a correct attempt start := time.Now().UnixMilli() _, err := a.Authenticate("user", "pass") require.Nil(t, err) require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis) // Timing an incorrect attempt start = time.Now().UnixMilli() _, err = a.Authenticate("user", "INCORRECT") require.Equal(t, auth.ErrUnauthenticated, err) require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis) // Timing a non-existing user attempt start = time.Now().UnixMilli() _, err = a.Authenticate("DOES-NOT-EXIST", "hithere") require.Equal(t, auth.ErrUnauthenticated, err) require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis) } func TestSQLiteAuth_UserManagement(t *testing.T) { a := newTestAuth(t, false, false) require.Nil(t, a.AddUser("phil", "phil", auth.RoleAdmin)) require.Nil(t, a.AddUser("ben", "ben", auth.RoleUser)) require.Nil(t, a.AllowAccess("ben", "mytopic", true, true)) require.Nil(t, a.AllowAccess("ben", "readme", true, false)) require.Nil(t, a.AllowAccess("ben", "writeme", false, true)) require.Nil(t, a.AllowAccess("ben", "everyonewrite", false, false)) // How unfair! require.Nil(t, a.AllowAccess(auth.Everyone, "announcements", true, false)) require.Nil(t, a.AllowAccess(auth.Everyone, "everyonewrite", true, true)) // Query user details phil, err := a.User("phil") require.Nil(t, err) require.Equal(t, "phil", phil.Name) require.True(t, strings.HasPrefix(phil.Hash, "$2a$10$")) require.Equal(t, auth.RoleAdmin, phil.Role) require.Equal(t, []auth.Grant{}, phil.Grants) ben, err := a.User("ben") require.Nil(t, err) require.Equal(t, "ben", ben.Name) require.True(t, strings.HasPrefix(ben.Hash, "$2a$10$")) require.Equal(t, auth.RoleUser, ben.Role) require.Equal(t, []auth.Grant{ {"mytopic", true, true}, {"readme", true, false}, {"writeme", false, true}, {"everyonewrite", false, false}, }, ben.Grants) everyone, err := a.User(auth.Everyone) require.Nil(t, err) require.Equal(t, "*", everyone.Name) require.Equal(t, "", everyone.Hash) require.Equal(t, auth.RoleAnonymous, everyone.Role) require.Equal(t, []auth.Grant{ {"announcements", true, false}, {"everyonewrite", true, true}, }, everyone.Grants) // Ben: Before revoking require.Nil(t, a.AllowAccess("ben", "mytopic", true, true)) require.Nil(t, a.AllowAccess("ben", "readme", true, false)) require.Nil(t, a.AllowAccess("ben", "writeme", false, true)) require.Nil(t, a.Authorize(ben, "mytopic", auth.PermissionRead)) require.Nil(t, a.Authorize(ben, "mytopic", auth.PermissionWrite)) require.Nil(t, a.Authorize(ben, "readme", auth.PermissionRead)) require.Nil(t, a.Authorize(ben, "writeme", auth.PermissionWrite)) // Revoke access for "ben" to "mytopic", then check again require.Nil(t, a.ResetAccess("ben", "mytopic")) require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "mytopic", auth.PermissionWrite)) // Revoked require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "mytopic", auth.PermissionRead)) // Revoked require.Nil(t, a.Authorize(ben, "readme", auth.PermissionRead)) // Unchanged require.Nil(t, a.Authorize(ben, "writeme", auth.PermissionWrite)) // Unchanged // Revoke rest of the access require.Nil(t, a.ResetAccess("ben", "")) require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "readme", auth.PermissionRead)) // Revoked require.Equal(t, auth.ErrUnauthorized, a.Authorize(ben, "wrtiteme", auth.PermissionWrite)) // Revoked // User list users, err := a.Users() require.Nil(t, err) require.Equal(t, 3, len(users)) require.Equal(t, "phil", users[0].Name) require.Equal(t, "ben", users[1].Name) require.Equal(t, "*", users[2].Name) // Remove user require.Nil(t, a.RemoveUser("ben")) _, err = a.User("ben") require.Equal(t, auth.ErrNotFound, err) users, err = a.Users() require.Nil(t, err) require.Equal(t, 2, len(users)) require.Equal(t, "phil", users[0].Name) require.Equal(t, "*", users[1].Name) } func TestSQLiteAuth_ChangePassword(t *testing.T) { a := newTestAuth(t, false, false) require.Nil(t, a.AddUser("phil", "phil", auth.RoleAdmin)) _, err := a.Authenticate("phil", "phil") require.Nil(t, err) require.Nil(t, a.ChangePassword("phil", "newpass")) _, err = a.Authenticate("phil", "phil") require.Equal(t, auth.ErrUnauthenticated, err) _, err = a.Authenticate("phil", "newpass") require.Nil(t, err) } func TestSQLiteAuth_ChangeRole(t *testing.T) { a := newTestAuth(t, false, false) require.Nil(t, a.AddUser("ben", "ben", auth.RoleUser)) require.Nil(t, a.AllowAccess("ben", "mytopic", true, true)) require.Nil(t, a.AllowAccess("ben", "readme", true, false)) ben, err := a.User("ben") require.Nil(t, err) require.Equal(t, auth.RoleUser, ben.Role) require.Equal(t, 2, len(ben.Grants)) require.Nil(t, a.ChangeRole("ben", auth.RoleAdmin)) ben, err = a.User("ben") require.Nil(t, err) require.Equal(t, auth.RoleAdmin, ben.Role) require.Equal(t, 0, len(ben.Grants)) } func newTestAuth(t *testing.T, defaultRead, defaultWrite bool) *auth.SQLiteAuthManager { filename := filepath.Join(t.TempDir(), "user.db") a, err := auth.NewSQLiteAuthManager(filename, defaultRead, defaultWrite) require.Nil(t, err) return a }