package authconfig import ( "bytes" "errors" "os" "path/filepath" "strings" "testing" "github.com/aritmolab/thothii/tools/tht/internal/authprojection" "github.com/aritmolab/thothii/tools/tht/internal/safeio" "github.com/aritmolab/thothii/tools/tht/internal/testsupport" ) func TestLoadSnapshotBytesReturnsValidatedCopiedCanonicalBytes(t *testing.T) { for _, fixture := range []struct{ name, auth, users string }{ {"local", defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin"))}, {"oidc", "version: 1\nmode: oidc\npublicUrl: https://example.invalid\n", ""}, } { t.Run(fixture.name, func(t *testing.T) { directory := writeAuthFiles(t, fixture.auth, fixture.users) snapshot, err := loadSnapshotBytes(directory) if err != nil { t.Fatal(err) } if snapshot.Mode != fixture.name || !bytes.Equal(snapshot.Auth, []byte(fixture.auth)) || !bytes.Equal(snapshot.Users, []byte(fixture.users)) { t.Fatal("loadSnapshotBytes() did not preserve the validated canonical bytes") } if _, err := authprojection.NewSnapshot(snapshot.Mode, snapshot.Auth, snapshot.Users); err != nil { t.Fatalf("loadSnapshotBytes() returned invalid projection data: %v", err) } snapshot.Auth[0] ^= 1 onDisk, err := os.ReadFile(filepath.Join(directory, authFileName)) if err != nil || !bytes.Equal(onDisk, []byte(fixture.auth)) { t.Fatal("loadSnapshotBytes() returned aliasing canonical storage") } }) } } const publicFixedHash = "$argon2id$v=19$m=65536,t=3,p=1$AAECAwQFBgcICQoLDA0ODw$DRo8ZSPI8G5OCvnFFapbVEjP69aDjy1Sw9i2743cPC4" func TestLoadReadsTheStrictLocalConfigurationAndRegistry(t *testing.T) { directory := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin"))) config, registry, err := Load(directory) if err != nil { t.Fatal(err) } if config.Version != 1 || config.Mode != "local" || config.Local.UsersFile != "users.yaml" { t.Fatalf("Load() config = %#v, want local auth.yaml configuration", config) } if len(registry.Users) != 1 || registry.Users[0].Username != "admin" || registry.Users[0].PasswordHash != publicFixedHash { t.Fatalf("Load() registry = %#v, want one saved administrator", registry) } } func TestLoadRejectsUnsafeConfigurationAndRegistryFiles(t *testing.T) { for name, arrange := range map[string]func(t *testing.T, directory string){ "symlinked directory": func(t *testing.T, directory string) { real := directory + "-real" if err := os.Rename(directory, real); err != nil { t.Fatal(err) } testsupport.SymlinkOrSkip(t, real, directory) }, "symlinked auth file": func(t *testing.T, directory string) { if err := os.Remove(filepath.Join(directory, "auth.yaml")); err != nil { t.Fatal(err) } testsupport.SymlinkOrSkip(t, filepath.Join(directory, "users.yaml"), filepath.Join(directory, "auth.yaml")) }, "symlinked users file": func(t *testing.T, directory string) { if err := os.Remove(filepath.Join(directory, "users.yaml")); err != nil { t.Fatal(err) } testsupport.SymlinkOrSkip(t, filepath.Join(directory, "auth.yaml"), filepath.Join(directory, "users.yaml")) }, "hard linked users file": func(t *testing.T, directory string) { linked := filepath.Join(directory, "users-linked.yaml") if err := os.Link(filepath.Join(directory, "users.yaml"), linked); err != nil { t.Fatal(err) } }, "world readable users file": func(t *testing.T, directory string) { if err := os.Chmod(filepath.Join(directory, "users.yaml"), 0o644); err != nil { t.Fatal(err) } }, } { t.Run(name, func(t *testing.T) { directory := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin"))) arrange(t, directory) if _, _, err := Load(directory); err == nil { t.Fatal("Load() accepted an unsafe authentication path") } }) } } func TestLoadRejectsDuplicateAndUnknownYAMLFields(t *testing.T) { for name, fixture := range map[string]struct{ auth, users string }{ "duplicate auth field": { auth: "version: 1\nmode: local\nmode: oidc\npublicUrl: http://127.0.0.1:8080\nlocal:\n usersFile: users.yaml\n", users: registryYAML(adminUserYAML("admin", "Admin", true, "admin")), }, "unknown auth field": { auth: strings.Replace(defaultAuthYAML, "version: 1", "version: 1\nunexpected: true", 1), users: registryYAML(adminUserYAML("admin", "Admin", true, "admin")), }, "unknown user field": { auth: defaultAuthYAML, users: strings.Replace(registryYAML(adminUserYAML("admin", "Admin", true, "admin")), " enabled: true", " enabled: true\n unexpected: true", 1), }, } { t.Run(name, func(t *testing.T) { directory := writeAuthFiles(t, fixture.auth, fixture.users) if _, _, err := Load(directory); err == nil { t.Fatal("Load() accepted malformed YAML") } }) } } func TestLoadRejectsOversizedYAMLBeforeParsing(t *testing.T) { directory := writeAuthFiles(t, defaultAuthYAML, registryYAML(adminUserYAML("admin", "Admin", true, "admin"))) if err := os.WriteFile(filepath.Join(directory, "users.yaml"), []byte(strings.Repeat("#", 1<<20)+"\n"), 0o600); err != nil { t.Fatal(err) } if _, _, err := Load(directory); !errors.Is(err, safeio.ErrUnsafeFile) { t.Fatalf("Load() error = %v, want bounded unsafe-file error", err) } } const defaultAuthYAML = "version: 1\nmode: local\npublicUrl: http://127.0.0.1:8080\nlocal:\n usersFile: users.yaml\n" func writeAuthFiles(t *testing.T, auth, users string) string { t.Helper() temporaryRoot, err := filepath.EvalSymlinks(os.TempDir()) if err != nil { t.Fatal(err) } directory, err := os.MkdirTemp(temporaryRoot, "tht-authconfig-") if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.RemoveAll(directory) }) if err := os.Chmod(directory, 0o700); err != nil { t.Fatal(err) } for name, contents := range map[string]string{"auth.yaml": auth, "users.yaml": users} { if err := os.WriteFile(filepath.Join(directory, name), []byte(contents), 0o600); err != nil { t.Fatal(err) } } return directory } func registryYAML(users ...string) string { return "version: 1\nusers:\n" + strings.Join(users, "") } func adminUserYAML(username, displayName string, enabled bool, role string) string { return " - id: 6ba7b810-9dad-4ed1-80b4-00c04fd430c8\n" + " username: " + username + "\n" + " displayName: " + displayName + "\n" + " passwordHash: " + publicFixedHash + "\n" + " roles:\n - " + role + "\n" + " enabled: " + map[bool]string{true: "true", false: "false"}[enabled] + "\n" + " authRevision: 1\n" }