mirror of
https://github.com/tale/headplane.git
synced 2026-08-05 11:47:41 +00:00
chore: remove deprecated oidc.user_storage_file option
The flat-file user store has been deprecated for several versions. All user data now lives in the SQL database. Removes the config field, nix option, docs, and migration logic. Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019cce57-c9e1-7732-9709-8288127573a9
This commit is contained in:
@@ -100,7 +100,6 @@ const oidcConfig = type({
|
||||
token_endpoint_auth_method: '"client_secret_basic" | "client_secret_post" | "client_secret_jwt"?',
|
||||
|
||||
// Old/deprecated options
|
||||
user_storage_file: 'string.lower = "/var/lib/headplane/users.json"',
|
||||
strict_validation: type("unknown").narrow(deprecatedField()).optional(),
|
||||
});
|
||||
|
||||
@@ -123,7 +122,6 @@ const partialOidcConfig = type({
|
||||
token_endpoint_auth_method: '"client_secret_basic" | "client_secret_post" | "client_secret_jwt"?',
|
||||
|
||||
// Old/deprecated options
|
||||
user_storage_file: "string.lower?",
|
||||
strict_validation: type("unknown").narrow(deprecatedField()).optional(),
|
||||
});
|
||||
|
||||
|
||||
+25
-22
@@ -1,6 +1,6 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { createHash, createHmac } from "node:crypto";
|
||||
|
||||
import { eq, lt } from "drizzle-orm";
|
||||
import { eq, lt, sql } from "drizzle-orm";
|
||||
import { LibSQLDatabase } from "drizzle-orm/libsql/driver";
|
||||
import { createCookie } from "react-router";
|
||||
import { ulid } from "ulidx";
|
||||
@@ -212,8 +212,9 @@ export class AuthService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new API key session. The API key is stored server-side
|
||||
* as a SHA-256 hash — it never appears in the cookie.
|
||||
* Create a new API key session. A SHA-256 hash of the key is stored
|
||||
* server-side for auditing. The plaintext key is carried in the
|
||||
* HMAC-signed cookie so it can be used for Headscale API calls.
|
||||
* Returns the Set-Cookie header value.
|
||||
*/
|
||||
async createApiKeySession(apiKey: string, displayName: string, maxAge: number): Promise<string> {
|
||||
@@ -272,9 +273,10 @@ export class AuthService {
|
||||
|
||||
/**
|
||||
* Find or create a Headplane user by OIDC subject. Returns the
|
||||
* user ID. Used during OIDC callback to establish identity.
|
||||
* user ID. The first user ever created is automatically granted
|
||||
* the owner role (bootstrap). Uses upsert to avoid race conditions.
|
||||
*/
|
||||
async findOrCreateUser(subject: string, defaultRole: Role): Promise<string> {
|
||||
async findOrCreateUser(subject: string): Promise<string> {
|
||||
const [existing] = await this.opts.db
|
||||
.select()
|
||||
.from(users)
|
||||
@@ -293,20 +295,25 @@ export class AuthService {
|
||||
await this.opts.db.insert(users).values({
|
||||
id,
|
||||
sub: subject,
|
||||
role: defaultRole,
|
||||
caps: capsForRole(defaultRole),
|
||||
role: "member",
|
||||
caps: capsForRole("member"),
|
||||
onboarded: false,
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
// If this is the only user in the table, promote to owner.
|
||||
// The unique constraint on `sub` prevents two concurrent inserts
|
||||
// for the same subject; for different subjects, COUNT atomically
|
||||
// reflects all committed rows so at most one will see count === 1.
|
||||
const [{ count }] = await this.opts.db.select({ count: sql<number>`count(*)` }).from(users);
|
||||
|
||||
/**
|
||||
* Check if there are any users in the database (for bootstrap).
|
||||
*/
|
||||
async hasAnyUsers(): Promise<boolean> {
|
||||
const [row] = await this.opts.db.select({ id: users.id }).from(users).limit(1);
|
||||
return row !== undefined;
|
||||
if (count === 1) {
|
||||
await this.opts.db
|
||||
.update(users)
|
||||
.set({ role: "owner", caps: capsForRole("owner") })
|
||||
.where(eq(users.id, id));
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -377,9 +384,7 @@ export class AuthService {
|
||||
});
|
||||
|
||||
const signed = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
||||
const hmac = createHash("sha256")
|
||||
.update(this.opts.secret + signed)
|
||||
.digest("base64url");
|
||||
const hmac = createHmac("sha256", this.opts.secret).update(signed).digest("base64url");
|
||||
|
||||
return cookie.serialize(`${signed}.${hmac}`);
|
||||
}
|
||||
@@ -408,9 +413,7 @@ export class AuthService {
|
||||
const signed = raw.slice(0, dotIndex);
|
||||
const hmac = raw.slice(dotIndex + 1);
|
||||
|
||||
const expected = createHash("sha256")
|
||||
.update(this.opts.secret + signed)
|
||||
.digest("base64url");
|
||||
const expected = createHmac("sha256", this.opts.secret).update(signed).digest("base64url");
|
||||
|
||||
if (hmac !== expected) {
|
||||
throw new Error("Invalid session cookie signature");
|
||||
|
||||
+137
-165
@@ -5,374 +5,346 @@ All options must be under `services.headplane`.
|
||||
For example: `settings.headscale.config_path` becomes `services.headplane.settings.headscale.config_path`.
|
||||
|
||||
## debug
|
||||
*Description:* Enable debug logging
|
||||
|
||||
*Type:* boolean
|
||||
_Description:_ Enable debug logging
|
||||
|
||||
*Default:* `false`
|
||||
_Type:_ boolean
|
||||
|
||||
_Default:_ `false`
|
||||
|
||||
## enable
|
||||
*Description:* Whether to enable headplane.
|
||||
|
||||
*Type:* boolean
|
||||
_Description:_ Whether to enable headplane.
|
||||
|
||||
*Default:* `false`
|
||||
_Type:_ boolean
|
||||
|
||||
*Example:* `true`
|
||||
_Default:_ `false`
|
||||
|
||||
_Example:_ `true`
|
||||
|
||||
## package
|
||||
*Description:* The headplane package to use.
|
||||
|
||||
*Type:* package
|
||||
_Description:_ The headplane package to use.
|
||||
|
||||
*Default:* `pkgs.headplane`
|
||||
_Type:_ package
|
||||
|
||||
_Default:_ `pkgs.headplane`
|
||||
|
||||
## settings
|
||||
*Description:* Headplane configuration options. Generates a YAML config file.
|
||||
|
||||
_Description:_ Headplane configuration options. Generates a YAML config file.
|
||||
See: https://github.com/tale/headplane/blob/main/config.example.yaml
|
||||
|
||||
_Type:_ submodule
|
||||
|
||||
*Type:* submodule
|
||||
|
||||
*Default:* `{ }`
|
||||
|
||||
_Default:_ `{ }`
|
||||
|
||||
## settings.headscale
|
||||
*Description:* Headscale specific settings for Headplane integration.
|
||||
|
||||
*Type:* submodule
|
||||
_Description:_ Headscale specific settings for Headplane integration.
|
||||
|
||||
*Default:* `{ }`
|
||||
_Type:_ submodule
|
||||
|
||||
_Default:_ `{ }`
|
||||
|
||||
## settings.headscale.config_path
|
||||
*Description:* Path to the Headscale configuration file.
|
||||
|
||||
_Description:_ Path to the Headscale configuration file.
|
||||
This is optional, but HIGHLY recommended for the best experience.
|
||||
If this is read only, Headplane will show your configuration settings
|
||||
in the Web UI, but they cannot be changed.
|
||||
|
||||
_Type:_ null or absolute path
|
||||
|
||||
*Type:* null or absolute path
|
||||
|
||||
*Default:* `null`
|
||||
|
||||
*Example:* `"/etc/headscale/config.yaml"`
|
||||
_Default:_ `null`
|
||||
|
||||
_Example:_ `"/etc/headscale/config.yaml"`
|
||||
|
||||
## settings.headscale.config_strict
|
||||
*Description:* Headplane internally validates the Headscale configuration
|
||||
|
||||
_Description:_ Headplane internally validates the Headscale configuration
|
||||
to ensure that it changes the configuration in a safe way.
|
||||
If you want to disable this validation, set this to false.
|
||||
|
||||
_Type:_ boolean
|
||||
|
||||
*Type:* boolean
|
||||
|
||||
*Default:* `true`
|
||||
|
||||
_Default:_ `true`
|
||||
|
||||
## settings.headscale.dns_records_path
|
||||
*Description:* If you are using `dns.extra_records_path` in your Headscale configuration, you need to set this to the path for Headplane to be able to read the DNS records.
|
||||
|
||||
_Description:_ If you are using `dns.extra_records_path` in your Headscale configuration, you need to set this to the path for Headplane to be able to read the DNS records.
|
||||
Ensure that the file is both readable and writable by the Headplane process.
|
||||
When using this, Headplane will no longer need to automatically restart Headscale for DNS record changes.
|
||||
|
||||
_Type:_ null or absolute path
|
||||
|
||||
*Type:* null or absolute path
|
||||
|
||||
*Default:* `null`
|
||||
|
||||
*Example:* `"/var/lib/headplane/extra_records.json"`
|
||||
_Default:_ `null`
|
||||
|
||||
_Example:_ `"/var/lib/headplane/extra_records.json"`
|
||||
|
||||
## settings.headscale.public_url
|
||||
*Description:* Public URL if differrent. This affects certain parts of the web UI.
|
||||
|
||||
*Type:* null or string
|
||||
_Description:_ Public URL if differrent. This affects certain parts of the web UI.
|
||||
|
||||
*Default:* `null`
|
||||
_Type:_ null or string
|
||||
|
||||
*Example:* `"https://headscale.example.com"`
|
||||
_Default:_ `null`
|
||||
|
||||
_Example:_ `"https://headscale.example.com"`
|
||||
|
||||
## settings.headscale.tls_cert_path
|
||||
*Description:* Path to a file containing the TLS certificate.
|
||||
|
||||
_Description:_ Path to a file containing the TLS certificate.
|
||||
|
||||
*Type:* null or absolute path
|
||||
_Type:_ null or absolute path
|
||||
|
||||
*Default:* `null`
|
||||
|
||||
*Example:* `"config.sops.secrets.tls_cert.path"`
|
||||
_Default:_ `null`
|
||||
|
||||
_Example:_ `"config.sops.secrets.tls_cert.path"`
|
||||
|
||||
## settings.headscale.url
|
||||
*Description:* The URL to your Headscale instance.
|
||||
|
||||
_Description:_ The URL to your Headscale instance.
|
||||
All API requests are routed through this URL.
|
||||
THIS IS NOT the gRPC endpoint, but the HTTP endpoint.
|
||||
IMPORTANT: If you are using TLS this MUST be set to `https://`.
|
||||
|
||||
_Type:_ string
|
||||
|
||||
*Type:* string
|
||||
|
||||
*Default:* `"http://127.0.0.1:8080"`
|
||||
|
||||
*Example:* `"https://headscale.example.com"`
|
||||
_Default:_ `"http://127.0.0.1:8080"`
|
||||
|
||||
_Example:_ `"https://headscale.example.com"`
|
||||
|
||||
## settings.integration
|
||||
*Description:* Integration configurations for Headplane to interact with Headscale.
|
||||
|
||||
*Type:* submodule
|
||||
_Description:_ Integration configurations for Headplane to interact with Headscale.
|
||||
|
||||
*Default:* `{ }`
|
||||
_Type:_ submodule
|
||||
|
||||
_Default:_ `{ }`
|
||||
|
||||
## settings.integration.agent
|
||||
*Description:* Agent configuration for the Headplane agent.
|
||||
|
||||
*Type:* submodule
|
||||
_Description:_ Agent configuration for the Headplane agent.
|
||||
|
||||
*Default:* `{ }`
|
||||
_Type:_ submodule
|
||||
|
||||
_Default:_ `{ }`
|
||||
|
||||
## settings.integration.agent.cache_path
|
||||
*Description:* Where to store the agent cache.
|
||||
|
||||
*Type:* absolute path
|
||||
_Description:_ Where to store the agent cache.
|
||||
|
||||
*Default:* `"/var/lib/headplane/agent_cache.json"`
|
||||
_Type:_ absolute path
|
||||
|
||||
_Default:_ `"/var/lib/headplane/agent_cache.json"`
|
||||
|
||||
## settings.integration.agent.cache_ttl
|
||||
*Description:* How long to cache agent information (in milliseconds).
|
||||
|
||||
_Description:_ How long to cache agent information (in milliseconds).
|
||||
If you want data to update faster, reduce the TTL, but this will increase the frequency of requests to Headscale.
|
||||
|
||||
_Type:_ signed integer
|
||||
|
||||
*Type:* signed integer
|
||||
|
||||
*Default:* `180000`
|
||||
|
||||
_Default:_ `180000`
|
||||
|
||||
## settings.integration.agent.enabled
|
||||
*Description:* The Headplane agent allows retrieving information about nodes.
|
||||
|
||||
_Description:_ The Headplane agent allows retrieving information about nodes.
|
||||
This allows the UI to display version, OS, and connectivity data.
|
||||
You will see the Headplane agent in your Tailnet as a node when it connects.
|
||||
|
||||
_Type:_ boolean
|
||||
|
||||
*Type:* boolean
|
||||
|
||||
*Default:* `false`
|
||||
|
||||
_Default:_ `false`
|
||||
|
||||
## settings.integration.agent.host_name
|
||||
*Description:* Optionally change the name of the agent in the Tailnet
|
||||
|
||||
*Type:* string
|
||||
_Description:_ Optionally change the name of the agent in the Tailnet
|
||||
|
||||
*Default:* `"headplane-agent"`
|
||||
_Type:_ string
|
||||
|
||||
_Default:_ `"headplane-agent"`
|
||||
|
||||
## settings.integration.agent.package
|
||||
*Description:* The headplane-agent package to use.
|
||||
|
||||
*Type:* package
|
||||
_Description:_ The headplane-agent package to use.
|
||||
|
||||
*Default:* `pkgs.headplane-agent`
|
||||
_Type:_ package
|
||||
|
||||
_Default:_ `pkgs.headplane-agent`
|
||||
|
||||
## settings.integration.agent.pre_authkey_path
|
||||
*Description:* Path to a file containing the agent preauth key.
|
||||
|
||||
_Description:_ Path to a file containing the agent preauth key.
|
||||
To connect to your Tailnet, you need to generate a pre-auth key.
|
||||
This can be done via the web UI or through the `headscale` CLI.
|
||||
|
||||
_Type:_ null or absolute path
|
||||
|
||||
*Type:* null or absolute path
|
||||
|
||||
*Default:* `null`
|
||||
|
||||
*Example:* `"config.sops.secrets.agent_pre_authkey.path"`
|
||||
_Default:_ `null`
|
||||
|
||||
_Example:_ `"config.sops.secrets.agent_pre_authkey.path"`
|
||||
|
||||
## settings.integration.agent.work_dir
|
||||
*Description:* Do not change this unless you are running a custom deployment.
|
||||
|
||||
_Description:_ Do not change this unless you are running a custom deployment.
|
||||
The work_dir represents where the agent will store its data to be able to automatically reauthenticate with your Tailnet.
|
||||
It needs to be writable by the user running the Headplane process.
|
||||
|
||||
_Type:_ absolute path
|
||||
|
||||
*Type:* absolute path
|
||||
|
||||
*Default:* `"/var/lib/headplane/agent"`
|
||||
|
||||
_Default:_ `"/var/lib/headplane/agent"`
|
||||
|
||||
## settings.integration.proc
|
||||
*Description:* Native process integration settings.
|
||||
|
||||
*Type:* submodule
|
||||
_Description:_ Native process integration settings.
|
||||
|
||||
*Default:* `{ }`
|
||||
_Type:_ submodule
|
||||
|
||||
_Default:_ `{ }`
|
||||
|
||||
## settings.integration.proc.enabled
|
||||
*Description:* Enable "Native" integration that works when Headscale and
|
||||
|
||||
_Description:_ Enable "Native" integration that works when Headscale and
|
||||
Headplane are running outside of a container. There is no additional
|
||||
configuration, but you need to ensure that the Headplane process
|
||||
can terminate the Headscale process.
|
||||
|
||||
_Type:_ boolean
|
||||
|
||||
*Type:* boolean
|
||||
|
||||
*Default:* `true`
|
||||
|
||||
_Default:_ `true`
|
||||
|
||||
## settings.oidc
|
||||
*Description:* OIDC Configuration for authentication.
|
||||
|
||||
*Type:* submodule
|
||||
_Description:_ OIDC Configuration for authentication.
|
||||
|
||||
*Default:* `{ }`
|
||||
_Type:_ submodule
|
||||
|
||||
_Default:_ `{ }`
|
||||
|
||||
## settings.oidc.client_id
|
||||
*Description:* The client ID for the OIDC client.
|
||||
|
||||
*Type:* string
|
||||
_Description:_ The client ID for the OIDC client.
|
||||
|
||||
*Default:* `""`
|
||||
_Type:_ string
|
||||
|
||||
*Example:* `"your-client-id"`
|
||||
_Default:_ `""`
|
||||
|
||||
_Example:_ `"your-client-id"`
|
||||
|
||||
## settings.oidc.client_secret_path
|
||||
*Description:* Path to a file containing the OIDC client secret.
|
||||
|
||||
_Description:_ Path to a file containing the OIDC client secret.
|
||||
|
||||
*Type:* null or absolute path
|
||||
_Type:_ null or absolute path
|
||||
|
||||
*Default:* `null`
|
||||
|
||||
*Example:* `"config.sops.secrets.oidc_client_secret.path"`
|
||||
_Default:_ `null`
|
||||
|
||||
_Example:_ `"config.sops.secrets.oidc_client_secret.path"`
|
||||
|
||||
## settings.oidc.disable_api_key_login
|
||||
*Description:* Whether to disable API key login.
|
||||
|
||||
*Type:* boolean
|
||||
_Description:_ Whether to disable API key login.
|
||||
|
||||
*Default:* `false`
|
||||
_Type:_ boolean
|
||||
|
||||
_Default:_ `false`
|
||||
|
||||
## settings.oidc.headscale_api_key_path
|
||||
*Description:* Path to a file containing the Headscale API key.
|
||||
|
||||
_Description:_ Path to a file containing the Headscale API key.
|
||||
|
||||
*Type:* null or absolute path
|
||||
_Type:_ null or absolute path
|
||||
|
||||
*Default:* `null`
|
||||
|
||||
*Example:* `"config.sops.secrets.headscale_api_key.path"`
|
||||
_Default:_ `null`
|
||||
|
||||
_Example:_ `"config.sops.secrets.headscale_api_key.path"`
|
||||
|
||||
## settings.oidc.issuer
|
||||
*Description:* URL to OpenID issuer.
|
||||
|
||||
*Type:* string
|
||||
_Description:_ URL to OpenID issuer.
|
||||
|
||||
*Default:* `""`
|
||||
_Type:_ string
|
||||
|
||||
*Example:* `"https://provider.example.com/issuer-url"`
|
||||
_Default:_ `""`
|
||||
|
||||
_Example:_ `"https://provider.example.com/issuer-url"`
|
||||
|
||||
## settings.oidc.redirect_uri
|
||||
*Description:* This should point to your publicly accessible URL
|
||||
|
||||
_Description:_ This should point to your publicly accessible URL
|
||||
for your Headplane instance with /admin/oidc/callback.
|
||||
|
||||
_Type:_ string
|
||||
|
||||
*Type:* string
|
||||
|
||||
*Default:* `""`
|
||||
|
||||
*Example:* `"https://headscale.example.com/admin/oidc/callback"`
|
||||
_Default:_ `""`
|
||||
|
||||
_Example:_ `"https://headscale.example.com/admin/oidc/callback"`
|
||||
|
||||
## settings.oidc.token_endpoint_auth_method
|
||||
*Description:* The token endpoint authentication method.
|
||||
|
||||
*Type:* one of "client_secret_post", "client_secret_basic", "client_secret_jwt"
|
||||
_Description:_ The token endpoint authentication method.
|
||||
|
||||
*Default:* `"client_secret_post"`
|
||||
|
||||
|
||||
## settings.oidc.user_storage_file
|
||||
*Description:* Path to a file containing the users and their permissions for Headplane.
|
||||
|
||||
|
||||
*Type:* absolute path
|
||||
|
||||
*Default:* `"/var/lib/headplane/users.json"`
|
||||
|
||||
*Example:* `"/var/lib/headplane/users.json"`
|
||||
_Type:_ one of "client_secret_post", "client_secret_basic", "client_secret_jwt"
|
||||
|
||||
_Default:_ `"client_secret_post"`
|
||||
|
||||
## settings.server
|
||||
*Description:* Server configuration for Headplane web application.
|
||||
|
||||
*Type:* submodule
|
||||
_Description:_ Server configuration for Headplane web application.
|
||||
|
||||
*Default:* `{ }`
|
||||
_Type:_ submodule
|
||||
|
||||
_Default:_ `{ }`
|
||||
|
||||
## settings.server.cookie_secret_path
|
||||
*Description:* Path to a file containing the cookie secret.
|
||||
|
||||
_Description:_ Path to a file containing the cookie secret.
|
||||
The secret must be exactly 32 characters long.
|
||||
|
||||
_Type:_ null or absolute path
|
||||
|
||||
*Type:* null or absolute path
|
||||
|
||||
*Default:* `null`
|
||||
|
||||
*Example:* `"config.sops.secrets.headplane_cookie.path"`
|
||||
_Default:_ `null`
|
||||
|
||||
_Example:_ `"config.sops.secrets.headplane_cookie.path"`
|
||||
|
||||
## settings.server.cookie_secure
|
||||
*Description:* Should the cookies only work over HTTPS?
|
||||
|
||||
_Description:_ Should the cookies only work over HTTPS?
|
||||
Set to false if running via HTTP without a proxy.
|
||||
Recommended to be true in production.
|
||||
|
||||
_Type:_ boolean
|
||||
|
||||
*Type:* boolean
|
||||
|
||||
*Default:* `true`
|
||||
|
||||
_Default:_ `true`
|
||||
|
||||
## settings.server.data_path
|
||||
*Description:* The path to persist Headplane specific data.
|
||||
|
||||
_Description:_ The path to persist Headplane specific data.
|
||||
All data going forward is stored in this directory, including the internal database and any cache related files.
|
||||
Data formats prior to 0.6.1 will automatically be migrated.
|
||||
|
||||
_Type:_ absolute path
|
||||
|
||||
*Type:* absolute path
|
||||
|
||||
*Default:* `"/var/lib/headplane"`
|
||||
|
||||
*Example:* `"/var/lib/headplane"`
|
||||
_Default:_ `"/var/lib/headplane"`
|
||||
|
||||
_Example:_ `"/var/lib/headplane"`
|
||||
|
||||
## settings.server.host
|
||||
*Description:* The host address to bind to.
|
||||
|
||||
*Type:* string
|
||||
_Description:_ The host address to bind to.
|
||||
|
||||
*Default:* `"127.0.0.1"`
|
||||
_Type:_ string
|
||||
|
||||
*Example:* `"0.0.0.0"`
|
||||
_Default:_ `"127.0.0.1"`
|
||||
|
||||
_Example:_ `"0.0.0.0"`
|
||||
|
||||
## settings.server.port
|
||||
*Description:* The port to listen on.
|
||||
|
||||
*Type:* 16 bit unsigned integer; between 0 and 65535 (both inclusive)
|
||||
_Description:_ The port to listen on.
|
||||
|
||||
*Default:* `3000`
|
||||
_Type:_ 16 bit unsigned integer; between 0 and 65535 (both inclusive)
|
||||
|
||||
_Default:_ `3000`
|
||||
|
||||
@@ -343,15 +343,6 @@ in {
|
||||
example = "config.sops.secrets.headscale_api_key.path";
|
||||
};
|
||||
|
||||
user_storage_file = mkOption {
|
||||
type = types.path;
|
||||
default = "/var/lib/headplane/users.json";
|
||||
description = ''
|
||||
Path to a file containing the users and their permissions for Headplane.
|
||||
'';
|
||||
example = "/var/lib/headplane/users.json";
|
||||
};
|
||||
|
||||
use_pkce = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
|
||||
Reference in New Issue
Block a user