Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Next

- Fixed `server.data_path`, `headscale.config_path`, `headscale.dns_records_path` and `headscale.tls_cert_path` being silently lowercased, which pointed Headplane at a different location for any path containing a capital letter (closes [#612](https://github.com/tale/headplane/issues/612)).
- Fixed the Headplane agent falling back to an interactive Tailscale login. The agent now starts with a pre-auth-key, preserves its existing state across restarts, and auto-approves itself when Headscale requires manual approval (closes [#582](https://github.com/tale/headplane/issues/582)).
- Fixed creating pre-auth keys with an expiry of 1000 days or more. The number input submitted its locale-formatted value (`365,000`, `365 000`, `365.000`), which either failed with a 500 or silently created a key with a truncated expiry. The raw value is now submitted and the server rejects malformed expiries with a 400 (closes [#596](https://github.com/tale/headplane/issues/596)).

Expand Down
16 changes: 8 additions & 8 deletions app/server/config/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const serverConfig = type({
host: 'string.ip = "0.0.0.0"',
port: "number.integer = 3000",
base_url: "string.url?",
data_path: 'string.lower = "/var/lib/headplane/"',
data_path: 'string = "/var/lib/headplane/"',
info_secret: "string?",

cookie_secret: "(32 <= string <= 32)",
Expand Down Expand Up @@ -65,7 +65,7 @@ const partialServerConfig = type({
host: "string.ip?",
port: "number.integer?",
base_url: "string.url?",
data_path: "string.lower?",
data_path: "string?",
info_secret: "string?",

cookie_secret: "(32 <= string <= 32)?",
Expand Down Expand Up @@ -94,10 +94,10 @@ const headscaleConfig = type({
.pipe((v) => (v.endsWith("/") ? v.slice(0, -1) : v))
.optional(),
api_key: "string?",
config_path: "string.lower?",
config_path: "string?",
config_strict: "boolean = true",
dns_records_path: "string.lower?",
tls_cert_path: "string.lower?",
dns_records_path: "string?",
tls_cert_path: "string?",
});

const partialHeadscaleConfig = type({
Expand All @@ -108,10 +108,10 @@ const partialHeadscaleConfig = type({
.pipe((v) => (v.endsWith("/") ? v.slice(0, -1) : v))
.optional(),
api_key: "string?",
config_path: "string.lower?",
config_path: "string?",
config_strict: "boolean?",
dns_records_path: "string.lower?",
tls_cert_path: "string.lower?",
dns_records_path: "string?",
tls_cert_path: "string?",
});

const assignableRole = '"admin" | "network_admin" | "it_admin" | "auditor" | "viewer" | "member"';
Expand Down
67 changes: 67 additions & 0 deletions tests/unit/config/path-casing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { dump } from "js-yaml";
import { beforeEach, describe, expect, test } from "vitest";

import { loadConfig } from "~/server/config/load";

import { clearFakeFiles, createFakeFile } from "../setup/overlay-fs";

const CONFIG_PATH = "/etc/headplane/config.yaml";

function writeConfig(overrides: {
server?: Record<string, unknown>;
headscale?: Record<string, unknown>;
}) {
createFakeFile(
CONFIG_PATH,
dump({
server: {
host: "0.0.0.0",
port: 3000,
cookie_secret: "abcdefghijklmnopqrstuvwxyz123456",
cookie_secure: false,
...overrides.server,
},
headscale: { url: "http://localhost:8080", ...overrides.headscale },
}),
);
}

// `string.lower` rewrites the value it validates rather than rejecting it, so
// declaring a filesystem path with it silently points Headplane at a different
// directory on a case-sensitive filesystem.
describe("filesystem paths preserve their casing", () => {
beforeEach(() => {
clearFakeFiles();
});

test("loadConfig_withMixedCaseDataPath_preservesIt", async () => {
writeConfig({ server: { data_path: "/srv/Headplane" } });
const config = await loadConfig(CONFIG_PATH);

expect(config.server.data_path).toBe("/srv/Headplane");
});

test("loadConfig_withMixedCaseHeadscalePaths_preservesThem", async () => {
writeConfig({
headscale: {
config_path: "/opt/Headscale/config.yaml",
dns_records_path: "/opt/Headscale/records.json",
tls_cert_path: "/opt/Headscale/Cert.pem",
},
});
const config = await loadConfig(CONFIG_PATH);

expect(config.headscale.config_path).toBe("/opt/Headscale/config.yaml");
expect(config.headscale.dns_records_path).toBe("/opt/Headscale/records.json");
expect(config.headscale.tls_cert_path).toBe("/opt/Headscale/Cert.pem");
});

test("loadConfig_withMixedCaseCookieDomain_stillLowercasesIt", async () => {
// Not a path: DNS is case-insensitive, so normalising here is correct and
// should stay.
writeConfig({ server: { cookie_domain: "Headplane.Example.COM" } });
const config = await loadConfig(CONFIG_PATH);

expect(config.server.cookie_domain).toBe("headplane.example.com");
});
});
Loading