init
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/iceBear67/simplepages/internal/store"
|
||||
)
|
||||
|
||||
// BootstrapKeyName is the name given to the first-run admin key, so an operator
|
||||
// listing keys can tell it apart from ones they minted themselves.
|
||||
const BootstrapKeyName = "bootstrap"
|
||||
|
||||
// EnsureAdminKey mints an admin key and writes its token to tokenPath when the
|
||||
// server has no usable admin key at all, and reports whether it did.
|
||||
//
|
||||
// This is the only place the server ever writes a token to disk, and it exists
|
||||
// because a fresh install would otherwise have no way to authenticate the call
|
||||
// that creates the first key. The file is 0600 and the log line tells the
|
||||
// operator to delete it once they have copied the token out.
|
||||
//
|
||||
// "Usable" excludes revoked and expired keys, so an installation whose only
|
||||
// admin key was revoked recovers by restarting rather than by hand-editing the
|
||||
// database.
|
||||
func EnsureAdminKey(ctx context.Context, db *store.DB, tokenPath string, log *slog.Logger) (bool, error) {
|
||||
n, err := db.CountUsableAdminKeys(ctx)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("count admin keys: %w", err)
|
||||
}
|
||||
if n > 0 {
|
||||
// Nothing to mint. If a token file is still lying around from an earlier
|
||||
// bootstrap, say so: it is a live credential until that key is revoked.
|
||||
if _, err := os.Lstat(tokenPath); err == nil && log != nil {
|
||||
log.Warn("bootstrap token file still present; delete it once the token is stored elsewhere",
|
||||
"path", tokenPath)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
token, keyID, hash, err := Mint()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := writeTokenFile(tokenPath, token); err != nil {
|
||||
return false, err
|
||||
}
|
||||
k := &store.APIKey{
|
||||
ID: keyID,
|
||||
SecretHash: hash[:],
|
||||
Scope: store.ScopeAdmin,
|
||||
Name: BootstrapKeyName,
|
||||
}
|
||||
if err := db.CreateKey(ctx, k); err != nil {
|
||||
// The file names a key that does not exist. Remove it rather than leave
|
||||
// an operator holding a token that will never authenticate.
|
||||
_ = os.Remove(tokenPath)
|
||||
return false, fmt.Errorf("create bootstrap key: %w", err)
|
||||
}
|
||||
|
||||
if log != nil {
|
||||
// The key id is the public half of the token and is safe to log; the
|
||||
// token itself is not, which is the whole reason for the file.
|
||||
log.Warn("no admin key found; minted a bootstrap admin key",
|
||||
"key_id", keyID, "path", tokenPath,
|
||||
"action", "read the token, then delete this file")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// writeTokenFile writes the token with 0600 permissions.
|
||||
//
|
||||
// It writes to a temporary file in the same directory and renames it into
|
||||
// place. That is not for atomicity — nothing reads this concurrently — but
|
||||
// because rename replaces the destination without following it. Opening the
|
||||
// path directly would follow a symlink someone had planted there and write a
|
||||
// live credential wherever it pointed.
|
||||
func writeTokenFile(path, token string) error {
|
||||
dir := filepath.Dir(path)
|
||||
f, err := os.CreateTemp(dir, ".bootstrap-token-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create bootstrap token file: %w", err)
|
||||
}
|
||||
tmp := f.Name()
|
||||
defer func() {
|
||||
f.Close()
|
||||
os.Remove(tmp) // no-op once the rename has succeeded
|
||||
}()
|
||||
|
||||
// CreateTemp already makes the file 0600, but say so explicitly: this is the
|
||||
// property that matters and it should not depend on a documented default.
|
||||
if err := f.Chmod(0o600); err != nil {
|
||||
return fmt.Errorf("chmod bootstrap token file: %w", err)
|
||||
}
|
||||
if _, err := f.WriteString(token + "\n"); err != nil {
|
||||
return fmt.Errorf("write bootstrap token: %w", err)
|
||||
}
|
||||
if err := f.Sync(); err != nil {
|
||||
return fmt.Errorf("sync bootstrap token: %w", err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return fmt.Errorf("close bootstrap token: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmp, path); err != nil {
|
||||
return fmt.Errorf("install bootstrap token: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user