mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-10 02:31:59 +09:00
db, sqliteconfig: consolidate query and config helpers
This commit is contained in:
@@ -135,7 +135,7 @@ var setPolicy = &cobra.Command{
|
|||||||
}
|
}
|
||||||
defer d.Close()
|
defer d.Close()
|
||||||
|
|
||||||
users, err := d.ListUsers()
|
users, err := d.ListUsers(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("loading users for policy validation: %w", err)
|
return fmt.Errorf("loading users for policy validation: %w", err)
|
||||||
}
|
}
|
||||||
@@ -194,7 +194,7 @@ var checkPolicy = &cobra.Command{
|
|||||||
}
|
}
|
||||||
defer d.Close()
|
defer d.Close()
|
||||||
|
|
||||||
users, err := d.ListUsers()
|
users, err := d.ListUsers(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("loading users: %w", err)
|
return fmt.Errorf("loading users: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-50
@@ -205,61 +205,20 @@ func validateAPIKey(db *gorm.DB, keyStr string) (*types.APIKey, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New format: parse and verify
|
// New format: parse and verify
|
||||||
const expectedMinLength = apiKeyPrefixLength + 1 + apiKeyHashLength
|
prefix, secret, err := parsePrefixedKey(
|
||||||
if len(prefixAndSecret) < expectedMinLength {
|
prefixAndSecret,
|
||||||
return nil, fmt.Errorf(
|
apiKeyPrefixLength,
|
||||||
"%w: key too short, expected at least %d chars after prefix, got %d",
|
apiKeyHashLength,
|
||||||
ErrAPIKeyFailedToParse,
|
ErrAPIKeyFailedToParse,
|
||||||
expectedMinLength,
|
)
|
||||||
len(prefixAndSecret),
|
if err != nil {
|
||||||
)
|
return nil, err
|
||||||
}
|
|
||||||
|
|
||||||
// Use fixed-length parsing
|
|
||||||
prefix := prefixAndSecret[:apiKeyPrefixLength]
|
|
||||||
|
|
||||||
// Validate separator at expected position
|
|
||||||
if prefixAndSecret[apiKeyPrefixLength] != '-' {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"%w: expected separator '-' at position %d, got '%c'",
|
|
||||||
ErrAPIKeyFailedToParse,
|
|
||||||
apiKeyPrefixLength,
|
|
||||||
prefixAndSecret[apiKeyPrefixLength],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
secret := prefixAndSecret[apiKeyPrefixLength+1:]
|
|
||||||
|
|
||||||
// Validate secret length
|
|
||||||
if len(secret) != apiKeyHashLength {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"%w: secret length mismatch, expected %d chars, got %d",
|
|
||||||
ErrAPIKeyFailedToParse,
|
|
||||||
apiKeyHashLength,
|
|
||||||
len(secret),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate prefix contains only base64 URL-safe characters
|
|
||||||
if !isValidBase64URLSafe(prefix) {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"%w: prefix contains invalid characters (expected base64 URL-safe: A-Za-z0-9_-)",
|
|
||||||
ErrAPIKeyFailedToParse,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate secret contains only base64 URL-safe characters
|
|
||||||
if !isValidBase64URLSafe(secret) {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"%w: secret contains invalid characters (expected base64 URL-safe: A-Za-z0-9_-)",
|
|
||||||
ErrAPIKeyFailedToParse,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look up by prefix (indexed)
|
// Look up by prefix (indexed)
|
||||||
var key types.APIKey
|
var key types.APIKey
|
||||||
|
|
||||||
err := db.First(&key, "prefix = ?", prefix).Error
|
err = db.First(&key, "prefix = ?", prefix).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("API key not found: %w", err)
|
return nil, fmt.Errorf("API key not found: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-53
@@ -220,7 +220,7 @@ AND auth_key_id NOT IN (
|
|||||||
{
|
{
|
||||||
ID: "202505141324",
|
ID: "202505141324",
|
||||||
Migrate: func(tx *gorm.DB) error {
|
Migrate: func(tx *gorm.DB) error {
|
||||||
users, err := ListUsers(tx)
|
users, err := ListUsers(tx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("listing users: %w", err)
|
return fmt.Errorf("listing users: %w", err)
|
||||||
}
|
}
|
||||||
@@ -617,7 +617,7 @@ AND auth_key_id NOT IN (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Load users and nodes to create PolicyManager
|
// 2. Load users and nodes to create PolicyManager
|
||||||
users, err := ListUsers(tx)
|
users, err := ListUsers(tx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("loading users for RequestTags migration: %w", err)
|
return fmt.Errorf("loading users for RequestTags migration: %w", err)
|
||||||
}
|
}
|
||||||
@@ -970,59 +970,20 @@ func openDB(cfg types.DatabaseConfig) (*gorm.DB, error) {
|
|||||||
|
|
||||||
func runMigrations(cfg types.DatabaseConfig, dbConn *gorm.DB, migrations *gormigrate.Gormigrate) error {
|
func runMigrations(cfg types.DatabaseConfig, dbConn *gorm.DB, migrations *gormigrate.Gormigrate) error {
|
||||||
if cfg.Type == types.DatabaseSqlite {
|
if cfg.Type == types.DatabaseSqlite {
|
||||||
// SQLite: Run migrations step-by-step, only disabling foreign keys when necessary
|
// SQLite: Run the early migrations that GORM cannot handle safely with
|
||||||
|
// foreign keys enabled (route and pre-auth-key automigrations) with FK
|
||||||
// List of migration IDs that require foreign keys to be disabled
|
// disabled, then run everything else with FK enabled.
|
||||||
// These are migrations that perform complex schema changes that GORM cannot handle safely with FK enabled
|
//
|
||||||
// NO NEW MIGRATIONS SHOULD BE ADDED HERE. ALL NEW MIGRATIONS MUST RUN WITH FOREIGN KEYS ENABLED.
|
// NO NEW MIGRATIONS SHOULD RUN WITH FK DISABLED. As of 2025-07-02, all
|
||||||
migrationsRequiringFKDisabled := map[string]bool{
|
// new migrations must run with foreign keys enabled via the
|
||||||
"202501221827": true, // Route table automigration with FK constraint issues
|
// migrations.Migrate() call below.
|
||||||
"202501311657": true, // PreAuthKey table automigration with FK constraint issues
|
if err := dbConn.Exec("PRAGMA foreign_keys = OFF").Error; err != nil { //nolint:noinlineerr
|
||||||
// Add other migration IDs here as they are identified to need FK disabled
|
return fmt.Errorf("disabling foreign keys: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all migration IDs in order from the actual migration definitions
|
// Run up to and including the last migration that requires FK disabled.
|
||||||
// Only IDs that are in the migrationsRequiringFKDisabled map will be processed with FK disabled
|
if err := migrations.MigrateTo("202501311657"); err != nil { //nolint:noinlineerr
|
||||||
// any other new migrations are ran after.
|
return fmt.Errorf("running migration 202501311657: %w", err)
|
||||||
migrationIDs := []string{
|
|
||||||
// v0.25.0
|
|
||||||
"202501221827",
|
|
||||||
"202501311657",
|
|
||||||
"202502070949",
|
|
||||||
|
|
||||||
// v0.26.0
|
|
||||||
"202502131714",
|
|
||||||
"202502171819",
|
|
||||||
"202505091439",
|
|
||||||
"202505141324",
|
|
||||||
|
|
||||||
// As of 2025-07-02, no new IDs should be added here.
|
|
||||||
// They will be ran by the migrations.Migrate() call below.
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, migrationID := range migrationIDs {
|
|
||||||
log.Trace().Caller().Str("migration_id", migrationID).Msg("running migration")
|
|
||||||
needsFKDisabled := migrationsRequiringFKDisabled[migrationID]
|
|
||||||
|
|
||||||
if needsFKDisabled {
|
|
||||||
// Disable foreign keys for this migration
|
|
||||||
err := dbConn.Exec("PRAGMA foreign_keys = OFF").Error
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("disabling foreign keys for migration %s: %w", migrationID, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Ensure foreign keys are enabled for this migration
|
|
||||||
err := dbConn.Exec("PRAGMA foreign_keys = ON").Error
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("enabling foreign keys for migration %s: %w", migrationID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run up to this specific migration (will only run the next pending migration)
|
|
||||||
err := migrations.MigrateTo(migrationID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("running migration %s: %w", migrationID, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dbConn.Exec("PRAGMA foreign_keys = ON").Error; err != nil { //nolint:noinlineerr
|
if err := dbConn.Exec("PRAGMA foreign_keys = ON").Error; err != nil { //nolint:noinlineerr
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func TestSQLiteMigrationAndDataValidation(t *testing.T) {
|
|||||||
|
|
||||||
// Verify users data preservation
|
// Verify users data preservation
|
||||||
users, err := Read(hsdb.DB, func(rx *gorm.DB) ([]types.User, error) {
|
users, err := Read(hsdb.DB, func(rx *gorm.DB) ([]types.User, error) {
|
||||||
return ListUsers(rx)
|
return ListUsers(rx, nil)
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, users, 1, "should preserve all 1 user from original schema")
|
assert.Len(t, users, 1, "should preserve all 1 user from original schema")
|
||||||
|
|||||||
+38
-37
@@ -30,6 +30,15 @@ const (
|
|||||||
// ErrNodeNameNotUnique is returned when a node name is not unique.
|
// ErrNodeNameNotUnique is returned when a node name is not unique.
|
||||||
var ErrNodeNameNotUnique = errors.New("node name is not unique")
|
var ErrNodeNameNotUnique = errors.New("node name is not unique")
|
||||||
|
|
||||||
|
// preloadNode returns a session that eager-loads a node's AuthKey, the
|
||||||
|
// AuthKey's User, and the node's User.
|
||||||
|
func preloadNode(tx *gorm.DB) *gorm.DB {
|
||||||
|
return tx.
|
||||||
|
Preload("AuthKey").
|
||||||
|
Preload("AuthKey.User").
|
||||||
|
Preload("User")
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNodeNotFound = errors.New("node not found")
|
ErrNodeNotFound = errors.New("node not found")
|
||||||
ErrNodeRouteIsNotAvailable = errors.New("route is not available on node")
|
ErrNodeRouteIsNotAvailable = errors.New("route is not available on node")
|
||||||
@@ -52,10 +61,7 @@ func (hsdb *HSDatabase) ListPeers(nodeID types.NodeID, peerIDs ...types.NodeID)
|
|||||||
func ListPeers(tx *gorm.DB, nodeID types.NodeID, peerIDs ...types.NodeID) (types.Nodes, error) {
|
func ListPeers(tx *gorm.DB, nodeID types.NodeID, peerIDs ...types.NodeID) (types.Nodes, error) {
|
||||||
nodes := types.Nodes{}
|
nodes := types.Nodes{}
|
||||||
|
|
||||||
err := tx.
|
err := preloadNode(tx).
|
||||||
Preload("AuthKey").
|
|
||||||
Preload("AuthKey.User").
|
|
||||||
Preload("User").
|
|
||||||
Where("id <> ?", nodeID).
|
Where("id <> ?", nodeID).
|
||||||
Where(peerIDs).Find(&nodes).Error
|
Where(peerIDs).Find(&nodes).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -78,10 +84,7 @@ func (hsdb *HSDatabase) ListNodes(nodeIDs ...types.NodeID) (types.Nodes, error)
|
|||||||
func ListNodes(tx *gorm.DB, nodeIDs ...types.NodeID) (types.Nodes, error) {
|
func ListNodes(tx *gorm.DB, nodeIDs ...types.NodeID) (types.Nodes, error) {
|
||||||
nodes := types.Nodes{}
|
nodes := types.Nodes{}
|
||||||
|
|
||||||
err := tx.
|
err := preloadNode(tx).
|
||||||
Preload("AuthKey").
|
|
||||||
Preload("AuthKey.User").
|
|
||||||
Preload("User").
|
|
||||||
Where(nodeIDs).Find(&nodes).Error
|
Where(nodeIDs).Find(&nodes).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -111,18 +114,22 @@ func (hsdb *HSDatabase) getNode(uid types.UserID, name string) (*types.Node, err
|
|||||||
|
|
||||||
// getNode finds a [types.Node] by name and user and returns the [types.Node] struct.
|
// getNode finds a [types.Node] by name and user and returns the [types.Node] struct.
|
||||||
func getNode(tx *gorm.DB, uid types.UserID, name string) (*types.Node, error) {
|
func getNode(tx *gorm.DB, uid types.UserID, name string) (*types.Node, error) {
|
||||||
nodes, err := ListNodesByUser(tx, uid)
|
uidPtr := uint(uid)
|
||||||
|
|
||||||
|
node := types.Node{}
|
||||||
|
|
||||||
|
err := preloadNode(tx).
|
||||||
|
Where(&types.Node{UserID: &uidPtr, Hostname: name}).
|
||||||
|
First(&node).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, ErrNodeNotFound
|
||||||
|
}
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, m := range nodes {
|
return &node, nil
|
||||||
if m.Hostname == name {
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, ErrNodeNotFound
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hsdb *HSDatabase) GetNodeByID(id types.NodeID) (*types.Node, error) {
|
func (hsdb *HSDatabase) GetNodeByID(id types.NodeID) (*types.Node, error) {
|
||||||
@@ -132,11 +139,8 @@ func (hsdb *HSDatabase) GetNodeByID(id types.NodeID) (*types.Node, error) {
|
|||||||
// GetNodeByID finds a [types.Node] by ID and returns the [types.Node] struct.
|
// GetNodeByID finds a [types.Node] by ID and returns the [types.Node] struct.
|
||||||
func GetNodeByID(tx *gorm.DB, id types.NodeID) (*types.Node, error) {
|
func GetNodeByID(tx *gorm.DB, id types.NodeID) (*types.Node, error) {
|
||||||
mach := types.Node{}
|
mach := types.Node{}
|
||||||
if result := tx.
|
if result := preloadNode(tx).
|
||||||
Preload("AuthKey").
|
First(&mach, "id = ?", id); result.Error != nil {
|
||||||
Preload("AuthKey.User").
|
|
||||||
Preload("User").
|
|
||||||
Find(&types.Node{ID: id}).First(&mach); result.Error != nil {
|
|
||||||
return nil, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,10 +157,7 @@ func GetNodeByNodeKey(
|
|||||||
nodeKey key.NodePublic,
|
nodeKey key.NodePublic,
|
||||||
) (*types.Node, error) {
|
) (*types.Node, error) {
|
||||||
mach := types.Node{}
|
mach := types.Node{}
|
||||||
if result := tx.
|
if result := preloadNode(tx).
|
||||||
Preload("AuthKey").
|
|
||||||
Preload("AuthKey.User").
|
|
||||||
Preload("User").
|
|
||||||
First(&mach, "node_key = ?", nodeKey.String()); result.Error != nil {
|
First(&mach, "node_key = ?", nodeKey.String()); result.Error != nil {
|
||||||
return nil, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
@@ -522,6 +523,15 @@ func (e *EphemeralGarbageCollector) Start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// firstOr returns the first non-empty option, or def if none is provided.
|
||||||
|
func firstOr(def string, opt []string) string {
|
||||||
|
if len(opt) > 0 && opt[0] != "" {
|
||||||
|
return opt[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
func (hsdb *HSDatabase) CreateNodeForTest(user *types.User, hostname ...string) *types.Node {
|
func (hsdb *HSDatabase) CreateNodeForTest(user *types.User, hostname ...string) *types.Node {
|
||||||
if !testing.Testing() {
|
if !testing.Testing() {
|
||||||
panic("CreateNodeForTest can only be called during tests")
|
panic("CreateNodeForTest can only be called during tests")
|
||||||
@@ -531,10 +541,7 @@ func (hsdb *HSDatabase) CreateNodeForTest(user *types.User, hostname ...string)
|
|||||||
panic("CreateNodeForTest requires a valid user")
|
panic("CreateNodeForTest requires a valid user")
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeName := defaultTestNodePrefix
|
nodeName := firstOr(defaultTestNodePrefix, hostname)
|
||||||
if len(hostname) > 0 && hostname[0] != "" {
|
|
||||||
nodeName = hostname[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a preauth key for the node
|
// Create a preauth key for the node
|
||||||
pak, err := hsdb.CreatePreAuthKey(user.TypedID(), false, false, nil, nil)
|
pak, err := hsdb.CreatePreAuthKey(user.TypedID(), false, false, nil, nil)
|
||||||
@@ -604,10 +611,7 @@ func (hsdb *HSDatabase) CreateNodesForTest(user *types.User, count int, hostname
|
|||||||
panic("CreateNodesForTest requires a valid user")
|
panic("CreateNodesForTest requires a valid user")
|
||||||
}
|
}
|
||||||
|
|
||||||
prefix := defaultTestNodePrefix
|
prefix := firstOr(defaultTestNodePrefix, hostnamePrefix)
|
||||||
if len(hostnamePrefix) > 0 && hostnamePrefix[0] != "" {
|
|
||||||
prefix = hostnamePrefix[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes := make([]*types.Node, count)
|
nodes := make([]*types.Node, count)
|
||||||
for i := range count {
|
for i := range count {
|
||||||
@@ -627,10 +631,7 @@ func (hsdb *HSDatabase) CreateRegisteredNodesForTest(user *types.User, count int
|
|||||||
panic("CreateRegisteredNodesForTest requires a valid user")
|
panic("CreateRegisteredNodesForTest requires a valid user")
|
||||||
}
|
}
|
||||||
|
|
||||||
prefix := defaultTestNodePrefix
|
prefix := firstOr(defaultTestNodePrefix, hostnamePrefix)
|
||||||
if len(hostnamePrefix) > 0 && hostnamePrefix[0] != "" {
|
|
||||||
prefix = hostnamePrefix[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes := make([]*types.Node, count)
|
nodes := make([]*types.Node, count)
|
||||||
for i := range count {
|
for i := range count {
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ func TestAutoApproveRoutes(t *testing.T) {
|
|||||||
err = adb.DB.Save(&nodeTagged).Error
|
err = adb.DB.Save(&nodeTagged).Error
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
users, err := adb.ListUsers()
|
users, err := adb.ListUsers(nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodes, err := adb.ListNodes()
|
nodes, err := adb.ListNodes()
|
||||||
|
|||||||
@@ -211,60 +211,18 @@ func findAuthKey(tx *gorm.DB, keyStr string) (*types.PreAuthKey, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New format: hskey-auth-{12-char-prefix}-{64-char-hash}
|
// New format: hskey-auth-{12-char-prefix}-{64-char-hash}
|
||||||
// Expected minimum length: 12 (prefix) + 1 (separator) + 64 (hash) = 77
|
prefix, hash, err := parsePrefixedKey(
|
||||||
const expectedMinLength = authKeyPrefixLength + 1 + authKeyLength
|
prefixAndHash,
|
||||||
if len(prefixAndHash) < expectedMinLength {
|
authKeyPrefixLength,
|
||||||
return nil, fmt.Errorf(
|
authKeyLength,
|
||||||
"%w: key too short, expected at least %d chars after prefix, got %d",
|
ErrPreAuthKeyFailedToParse,
|
||||||
ErrPreAuthKeyFailedToParse,
|
)
|
||||||
expectedMinLength,
|
if err != nil {
|
||||||
len(prefixAndHash),
|
return nil, err
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use fixed-length parsing instead of separator-based to handle dashes in base64 URL-safe
|
|
||||||
prefix := prefixAndHash[:authKeyPrefixLength]
|
|
||||||
|
|
||||||
// Validate separator at expected position
|
|
||||||
if prefixAndHash[authKeyPrefixLength] != '-' {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"%w: expected separator '-' at position %d, got '%c'",
|
|
||||||
ErrPreAuthKeyFailedToParse,
|
|
||||||
authKeyPrefixLength,
|
|
||||||
prefixAndHash[authKeyPrefixLength],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
hash := prefixAndHash[authKeyPrefixLength+1:]
|
|
||||||
|
|
||||||
// Validate hash length
|
|
||||||
if len(hash) != authKeyLength {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"%w: hash length mismatch, expected %d chars, got %d",
|
|
||||||
ErrPreAuthKeyFailedToParse,
|
|
||||||
authKeyLength,
|
|
||||||
len(hash),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate prefix contains only base64 URL-safe characters
|
|
||||||
if !isValidBase64URLSafe(prefix) {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"%w: prefix contains invalid characters (expected base64 URL-safe: A-Za-z0-9_-)",
|
|
||||||
ErrPreAuthKeyFailedToParse,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate hash contains only base64 URL-safe characters
|
|
||||||
if !isValidBase64URLSafe(hash) {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"%w: hash contains invalid characters (expected base64 URL-safe: A-Za-z0-9_-)",
|
|
||||||
ErrPreAuthKeyFailedToParse,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look up key by prefix
|
// Look up key by prefix
|
||||||
err := tx.Preload("User").First(&pak, "prefix = ?", prefix).Error
|
err = tx.Preload("User").First(&pak, "prefix = ?", prefix).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrPreAuthKeyNotFound
|
return nil, ErrPreAuthKeyNotFound
|
||||||
}
|
}
|
||||||
@@ -278,6 +236,69 @@ func findAuthKey(tx *gorm.DB, keyStr string) (*types.PreAuthKey, error) {
|
|||||||
return &pak, nil
|
return &pak, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parsePrefixedKey splits the prefix-and-secret portion of a new-format key
|
||||||
|
// (the part after the "hskey-*-" prefix) into its fixed-length prefix and
|
||||||
|
// secret components, validating the length, separator position, and that both
|
||||||
|
// components are base64 URL-safe. Fixed-length parsing is used instead of
|
||||||
|
// separator-based to handle dashes in base64 URL-safe characters.
|
||||||
|
func parsePrefixedKey(
|
||||||
|
prefixAndSecret string,
|
||||||
|
prefixLen, secretLen int,
|
||||||
|
parseErr error,
|
||||||
|
) (string, string, error) {
|
||||||
|
expectedMinLength := prefixLen + 1 + secretLen
|
||||||
|
if len(prefixAndSecret) < expectedMinLength {
|
||||||
|
return "", "", fmt.Errorf(
|
||||||
|
"%w: key too short, expected at least %d chars after prefix, got %d",
|
||||||
|
parseErr,
|
||||||
|
expectedMinLength,
|
||||||
|
len(prefixAndSecret),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := prefixAndSecret[:prefixLen]
|
||||||
|
|
||||||
|
// Validate separator at expected position
|
||||||
|
if prefixAndSecret[prefixLen] != '-' {
|
||||||
|
return "", "", fmt.Errorf(
|
||||||
|
"%w: expected separator '-' at position %d, got '%c'",
|
||||||
|
parseErr,
|
||||||
|
prefixLen,
|
||||||
|
prefixAndSecret[prefixLen],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret := prefixAndSecret[prefixLen+1:]
|
||||||
|
|
||||||
|
// Validate secret length
|
||||||
|
if len(secret) != secretLen {
|
||||||
|
return "", "", fmt.Errorf(
|
||||||
|
"%w: secret length mismatch, expected %d chars, got %d",
|
||||||
|
parseErr,
|
||||||
|
secretLen,
|
||||||
|
len(secret),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate prefix contains only base64 URL-safe characters
|
||||||
|
if !isValidBase64URLSafe(prefix) {
|
||||||
|
return "", "", fmt.Errorf(
|
||||||
|
"%w: prefix contains invalid characters (expected base64 URL-safe: A-Za-z0-9_-)",
|
||||||
|
parseErr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate secret contains only base64 URL-safe characters
|
||||||
|
if !isValidBase64URLSafe(secret) {
|
||||||
|
return "", "", fmt.Errorf(
|
||||||
|
"%w: secret contains invalid characters (expected base64 URL-safe: A-Za-z0-9_-)",
|
||||||
|
parseErr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefix, secret, nil
|
||||||
|
}
|
||||||
|
|
||||||
// isValidBase64URLSafe checks if a string contains only base64 URL-safe characters.
|
// isValidBase64URLSafe checks if a string contains only base64 URL-safe characters.
|
||||||
func isValidBase64URLSafe(s string) bool {
|
func isValidBase64URLSafe(s string) bool {
|
||||||
for _, c := range s {
|
for _, c := range s {
|
||||||
|
|||||||
@@ -347,33 +347,6 @@ func (c *Config) ToURL() (string, error) {
|
|||||||
return "", fmt.Errorf("invalid config: %w", err)
|
return "", fmt.Errorf("invalid config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var pragmas []string
|
|
||||||
|
|
||||||
// Add pragma parameters only if they're set (non-zero/non-empty)
|
|
||||||
if c.BusyTimeout > 0 {
|
|
||||||
pragmas = append(pragmas, fmt.Sprintf("busy_timeout=%d", c.BusyTimeout))
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.JournalMode != "" {
|
|
||||||
pragmas = append(pragmas, fmt.Sprintf("journal_mode=%s", c.JournalMode))
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.AutoVacuum != "" {
|
|
||||||
pragmas = append(pragmas, fmt.Sprintf("auto_vacuum=%s", c.AutoVacuum))
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.WALAutocheckpoint >= 0 {
|
|
||||||
pragmas = append(pragmas, fmt.Sprintf("wal_autocheckpoint=%d", c.WALAutocheckpoint))
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Synchronous != "" {
|
|
||||||
pragmas = append(pragmas, fmt.Sprintf("synchronous=%s", c.Synchronous))
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.ForeignKeys {
|
|
||||||
pragmas = append(pragmas, "foreign_keys=ON")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle different database types
|
// Handle different database types
|
||||||
var baseURL string
|
var baseURL string
|
||||||
if c.Path == ":memory:" {
|
if c.Path == ":memory:" {
|
||||||
@@ -383,16 +356,36 @@ func (c *Config) ToURL() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build query parameters
|
// Build query parameters
|
||||||
queryParts := make([]string, 0, 1+len(pragmas))
|
var queryParts []string
|
||||||
|
|
||||||
// Add _txlock first (it's a connection parameter, not a pragma)
|
// Add _txlock first (it's a connection parameter, not a pragma)
|
||||||
if c.TxLock != "" {
|
if c.TxLock != "" {
|
||||||
queryParts = append(queryParts, "_txlock="+string(c.TxLock))
|
queryParts = append(queryParts, "_txlock="+string(c.TxLock))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add pragma parameters
|
// Add pragma parameters only if they're set (non-zero/non-empty)
|
||||||
for _, pragma := range pragmas {
|
if c.BusyTimeout > 0 {
|
||||||
queryParts = append(queryParts, "_pragma="+pragma)
|
queryParts = append(queryParts, fmt.Sprintf("_pragma=busy_timeout=%d", c.BusyTimeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.JournalMode != "" {
|
||||||
|
queryParts = append(queryParts, fmt.Sprintf("_pragma=journal_mode=%s", c.JournalMode))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.AutoVacuum != "" {
|
||||||
|
queryParts = append(queryParts, fmt.Sprintf("_pragma=auto_vacuum=%s", c.AutoVacuum))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.WALAutocheckpoint >= 0 {
|
||||||
|
queryParts = append(queryParts, fmt.Sprintf("_pragma=wal_autocheckpoint=%d", c.WALAutocheckpoint))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Synchronous != "" {
|
||||||
|
queryParts = append(queryParts, fmt.Sprintf("_pragma=synchronous=%s", c.Synchronous))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ForeignKeys {
|
||||||
|
queryParts = append(queryParts, "_pragma=foreign_keys=ON")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(queryParts) > 0 {
|
if len(queryParts) > 0 {
|
||||||
|
|||||||
@@ -47,45 +47,45 @@ func (TextSerialiser) Scan(ctx context.Context, field *schema.Field, dst reflect
|
|||||||
fieldValue = fieldValue.Elem()
|
fieldValue = fieldValue.Elem()
|
||||||
}
|
}
|
||||||
|
|
||||||
if dbValue != nil {
|
if dbValue == nil {
|
||||||
var bytes []byte
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
switch v := dbValue.(type) {
|
var bytes []byte
|
||||||
case []byte:
|
|
||||||
bytes = v
|
switch v := dbValue.(type) {
|
||||||
case string:
|
case []byte:
|
||||||
bytes = []byte(v)
|
bytes = v
|
||||||
default:
|
case string:
|
||||||
return fmt.Errorf("%w: %#v", errUnmarshalTextValue, dbValue)
|
bytes = []byte(v)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w: %#v", errUnmarshalTextValue, dbValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isTextUnmarshaler(fieldValue) {
|
||||||
|
return fmt.Errorf("%w: %T", errUnsupportedType, fieldValue.Interface())
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeInstantiatePtr(fieldValue)
|
||||||
|
f := fieldValue.MethodByName("UnmarshalText")
|
||||||
|
args := []reflect.Value{reflect.ValueOf(bytes)}
|
||||||
|
|
||||||
|
ret := f.Call(args)
|
||||||
|
if !ret[0].IsNil() {
|
||||||
|
if err, ok := ret[0].Interface().(error); ok {
|
||||||
|
return decodingError(field.Name, err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if isTextUnmarshaler(fieldValue) {
|
// If the underlying field is to a pointer type, we need to
|
||||||
maybeInstantiatePtr(fieldValue)
|
// assign the value as a pointer to it.
|
||||||
f := fieldValue.MethodByName("UnmarshalText")
|
// If it is not a pointer, we need to assign the value to the
|
||||||
args := []reflect.Value{reflect.ValueOf(bytes)}
|
// field.
|
||||||
|
dstField := field.ReflectValueOf(ctx, dst)
|
||||||
ret := f.Call(args)
|
if dstField.Kind() == reflect.Pointer {
|
||||||
if !ret[0].IsNil() {
|
dstField.Set(fieldValue)
|
||||||
if err, ok := ret[0].Interface().(error); ok {
|
} else {
|
||||||
return decodingError(field.Name, err)
|
dstField.Set(fieldValue.Elem())
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the underlying field is to a pointer type, we need to
|
|
||||||
// assign the value as a pointer to it.
|
|
||||||
// If it is not a pointer, we need to assign the value to the
|
|
||||||
// field.
|
|
||||||
dstField := field.ReflectValueOf(ctx, dst)
|
|
||||||
if dstField.Kind() == reflect.Pointer {
|
|
||||||
dstField.Set(fieldValue)
|
|
||||||
} else {
|
|
||||||
dstField.Set(fieldValue.Elem())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("%w: %T", errUnsupportedType, fieldValue.Interface())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+12
-28
@@ -12,11 +12,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrUserExists = errors.New("user already exists")
|
ErrUserExists = errors.New("user already exists")
|
||||||
ErrUserNotFound = errors.New("user not found")
|
ErrUserNotFound = errors.New("user not found")
|
||||||
ErrUserStillHasNodes = errors.New("user not empty: node(s) found")
|
ErrUserStillHasNodes = errors.New("user not empty: node(s) found")
|
||||||
ErrUserWhereInvalidCount = errors.New("expect 0 or 1 where User structs")
|
ErrUserNotUnique = errors.New("expected exactly one user")
|
||||||
ErrUserNotUnique = errors.New("expected exactly one user")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (hsdb *HSDatabase) CreateUser(user types.User) (*types.User, error) {
|
func (hsdb *HSDatabase) CreateUser(user types.User) (*types.User, error) {
|
||||||
@@ -152,24 +151,15 @@ func GetUserByOIDCIdentifier(tx *gorm.DB, id string) (*types.User, error) {
|
|||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hsdb *HSDatabase) ListUsers(where ...*types.User) ([]types.User, error) {
|
func (hsdb *HSDatabase) ListUsers(filter *types.User) ([]types.User, error) {
|
||||||
return ListUsers(hsdb.DB, where...)
|
return ListUsers(hsdb.DB, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListUsers gets all the existing users.
|
// ListUsers gets all the existing users, optionally filtered by a non-nil filter.
|
||||||
func ListUsers(tx *gorm.DB, where ...*types.User) ([]types.User, error) {
|
func ListUsers(tx *gorm.DB, filter *types.User) ([]types.User, error) {
|
||||||
if len(where) > 1 {
|
|
||||||
return nil, fmt.Errorf("%w, got %d", ErrUserWhereInvalidCount, len(where))
|
|
||||||
}
|
|
||||||
|
|
||||||
var user *types.User
|
|
||||||
if len(where) == 1 {
|
|
||||||
user = where[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
users := []types.User{}
|
users := []types.User{}
|
||||||
|
|
||||||
err := tx.Where(user).Find(&users).Error
|
err := tx.Where(filter).Find(&users).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -202,7 +192,7 @@ func ListNodesByUser(tx *gorm.DB, uid types.UserID) (types.Nodes, error) {
|
|||||||
|
|
||||||
uidPtr := uint(uid)
|
uidPtr := uint(uid)
|
||||||
|
|
||||||
err := tx.Preload("AuthKey").Preload("AuthKey.User").Preload("User").Where(&types.Node{UserID: &uidPtr}).Find(&nodes).Error
|
err := preloadNode(tx).Where(&types.Node{UserID: &uidPtr}).Find(&nodes).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -215,10 +205,7 @@ func (hsdb *HSDatabase) CreateUserForTest(name ...string) *types.User {
|
|||||||
panic("CreateUserForTest can only be called during tests")
|
panic("CreateUserForTest can only be called during tests")
|
||||||
}
|
}
|
||||||
|
|
||||||
userName := "testuser"
|
userName := firstOr("testuser", name)
|
||||||
if len(name) > 0 && name[0] != "" {
|
|
||||||
userName = name[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := hsdb.CreateUser(types.User{Name: userName})
|
user, err := hsdb.CreateUser(types.User{Name: userName})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -233,10 +220,7 @@ func (hsdb *HSDatabase) CreateUsersForTest(count int, namePrefix ...string) []*t
|
|||||||
panic("CreateUsersForTest can only be called during tests")
|
panic("CreateUsersForTest can only be called during tests")
|
||||||
}
|
}
|
||||||
|
|
||||||
prefix := "testuser"
|
prefix := firstOr("testuser", namePrefix)
|
||||||
if len(namePrefix) > 0 && namePrefix[0] != "" {
|
|
||||||
prefix = namePrefix[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
users := make([]*types.User, count)
|
users := make([]*types.User, count)
|
||||||
for i := range count {
|
for i := range count {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func TestCreateAndDestroyUser(t *testing.T) {
|
|||||||
user := db.CreateUserForTest("test")
|
user := db.CreateUserForTest("test")
|
||||||
assert.Equal(t, "test", user.Name)
|
assert.Equal(t, "test", user.Name)
|
||||||
|
|
||||||
users, err := db.ListUsers()
|
users, err := db.ListUsers(nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, users, 1)
|
assert.Len(t, users, 1)
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ func TestRenameUser(t *testing.T) {
|
|||||||
userTest := db.CreateUserForTest("test")
|
userTest := db.CreateUserForTest("test")
|
||||||
assert.Equal(t, "test", userTest.Name)
|
assert.Equal(t, "test", userTest.Name)
|
||||||
|
|
||||||
users, err := db.ListUsers()
|
users, err := db.ListUsers(nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, users, 1)
|
assert.Len(t, users, 1)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errVersionUpgrade = errors.New("version upgrade not supported")
|
var errVersionUpgrade = errors.New("version upgrade not supported")
|
||||||
@@ -66,22 +67,20 @@ func parseVersion(s string) (semver, error) {
|
|||||||
return semver{}, fmt.Errorf("%q: %w", s, errVersionFormat)
|
return semver{}, fmt.Errorf("%q: %w", s, errVersionFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
major, err := strconv.Atoi(parts[0])
|
var out [3]int
|
||||||
if err != nil {
|
|
||||||
return semver{}, fmt.Errorf("invalid major version in %q: %w", s, err)
|
names := [...]string{"major", "minor", "patch"}
|
||||||
|
|
||||||
|
for i, p := range parts {
|
||||||
|
n, err := strconv.Atoi(p)
|
||||||
|
if err != nil {
|
||||||
|
return semver{}, fmt.Errorf("invalid %s version in %q: %w", names[i], s, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out[i] = n
|
||||||
}
|
}
|
||||||
|
|
||||||
minor, err := strconv.Atoi(parts[1])
|
return semver{Major: out[0], Minor: out[1], Patch: out[2]}, nil
|
||||||
if err != nil {
|
|
||||||
return semver{}, fmt.Errorf("invalid minor version in %q: %w", s, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
patch, err := strconv.Atoi(parts[2])
|
|
||||||
if err != nil {
|
|
||||||
return semver{}, fmt.Errorf("invalid patch version in %q: %w", s, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return semver{Major: major, Minor: minor, Patch: patch}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensureDatabaseVersionTable creates the database_versions table if it
|
// ensureDatabaseVersionTable creates the database_versions table if it
|
||||||
@@ -118,23 +117,12 @@ func getDatabaseVersion(db *gorm.DB) (string, error) {
|
|||||||
func setDatabaseVersion(db *gorm.DB, version string) error {
|
func setDatabaseVersion(db *gorm.DB, version string) error {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
|
||||||
// Try update first, then insert if no rows affected.
|
err := db.Clauses(clause.OnConflict{
|
||||||
result := db.Exec(
|
Columns: []clause.Column{{Name: "id"}},
|
||||||
"UPDATE database_versions SET version = ?, updated_at = ? WHERE id = 1",
|
DoUpdates: clause.AssignmentColumns([]string{"version", "updated_at"}),
|
||||||
version, now,
|
}).Create(&DatabaseVersion{ID: 1, Version: version, UpdatedAt: now}).Error
|
||||||
)
|
if err != nil {
|
||||||
if result.Error != nil {
|
return fmt.Errorf("upserting database version: %w", err)
|
||||||
return fmt.Errorf("updating database version: %w", result.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.RowsAffected == 0 {
|
|
||||||
err := db.Exec(
|
|
||||||
"INSERT INTO database_versions (id, version, updated_at) VALUES (1, ?, ?)",
|
|
||||||
version, now,
|
|
||||||
).Error
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("inserting database version: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ func NewState(cfg *types.Config) (*State, error) {
|
|||||||
node.IsOnline = new(false)
|
node.IsOnline = new(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
users, err := db.ListUsers()
|
users, err := db.ListUsers(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("loading users: %w", err)
|
return nil, fmt.Errorf("loading users: %w", err)
|
||||||
}
|
}
|
||||||
@@ -508,7 +508,7 @@ func (s *State) ListUsersWithFilter(filter *types.User) ([]types.User, error) {
|
|||||||
|
|
||||||
// ListAllUsers retrieves all users in the system.
|
// ListAllUsers retrieves all users in the system.
|
||||||
func (s *State) ListAllUsers() ([]types.User, error) {
|
func (s *State) ListAllUsers() ([]types.User, error) {
|
||||||
return s.db.ListUsers()
|
return s.db.ListUsers(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// persistNodeRowToDB writes the node's database row, re-reading the
|
// persistNodeRowToDB writes the node's database row, re-reading the
|
||||||
|
|||||||
Reference in New Issue
Block a user