Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
519f8f8879 | ||
| 4b54e50a0d | |||
| 453c52584c | |||
| bcab16f2bf |
14
db/db.go
14
db/db.go
@@ -65,11 +65,11 @@ func Open(path string) (*sql.DB, error) {
|
|||||||
// connection to serialize all access and eliminate busy errors.
|
// connection to serialize all access and eliminate busy errors.
|
||||||
database.SetMaxOpenConns(1)
|
database.SetMaxOpenConns(1)
|
||||||
|
|
||||||
// Ensure permissions are correct even if the file already existed.
|
// Best-effort permissions tightening. This may fail inside rootless
|
||||||
if err := os.Chmod(path, 0600); err != nil {
|
// podman containers where fchmod is denied in the user namespace.
|
||||||
_ = database.Close()
|
// The database still functions correctly without it.
|
||||||
return nil, fmt.Errorf("db: chmod %s: %w", path, err)
|
// See: log/2026-04-03-uid-incident.md
|
||||||
}
|
_ = os.Chmod(path, 0600)
|
||||||
|
|
||||||
return database, nil
|
return database, nil
|
||||||
}
|
}
|
||||||
@@ -168,9 +168,7 @@ func Snapshot(database *sql.DB, destPath string) error {
|
|||||||
return fmt.Errorf("db: snapshot: %w", err)
|
return fmt.Errorf("db: snapshot: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.Chmod(destPath, 0600); err != nil {
|
_ = os.Chmod(destPath, 0600) // best-effort; may fail in rootless containers
|
||||||
return fmt.Errorf("db: chmod snapshot %s: %w", destPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
44
sso/sso.go
44
sso/sso.go
@@ -22,6 +22,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -38,9 +39,18 @@ const (
|
|||||||
// Config holds the SSO client configuration. The values must match the
|
// Config holds the SSO client configuration. The values must match the
|
||||||
// SSO client registration in MCIAS config.
|
// SSO client registration in MCIAS config.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// MciasURL is the base URL of the MCIAS server.
|
// MciasURL is the base URL of the MCIAS server used for the
|
||||||
|
// server-to-server authorization-code exchange. This is typically the
|
||||||
|
// internal/Tailnet address so the exchange does not depend on the public
|
||||||
|
// edge.
|
||||||
MciasURL string
|
MciasURL string
|
||||||
|
|
||||||
|
// PublicURL, when set, is the browser-facing MCIAS base URL used to build
|
||||||
|
// the authorize redirect. It must be resolvable and reachable by end-user
|
||||||
|
// browsers (e.g. the public hostname). When empty, MciasURL is used for
|
||||||
|
// both, which only works when MciasURL is itself browser-reachable.
|
||||||
|
PublicURL string
|
||||||
|
|
||||||
// ClientID is the registered SSO client identifier.
|
// ClientID is the registered SSO client identifier.
|
||||||
ClientID string
|
ClientID string
|
||||||
|
|
||||||
@@ -103,9 +113,19 @@ func New(cfg Config) (*Client, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizeURL returns the MCIAS authorize URL with the given state parameter.
|
// authorizeBase returns the browser-facing MCIAS base URL: PublicURL when
|
||||||
|
// configured, otherwise MciasURL.
|
||||||
|
func (c *Client) authorizeBase() string {
|
||||||
|
if c.cfg.PublicURL != "" {
|
||||||
|
return c.cfg.PublicURL
|
||||||
|
}
|
||||||
|
return c.cfg.MciasURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizeURL returns the MCIAS authorize URL (browser-facing) with the
|
||||||
|
// given state parameter.
|
||||||
func (c *Client) AuthorizeURL(state string) string {
|
func (c *Client) AuthorizeURL(state string) string {
|
||||||
base := strings.TrimRight(c.cfg.MciasURL, "/")
|
base := strings.TrimRight(c.authorizeBase(), "/")
|
||||||
return base + "/sso/authorize?" + url.Values{
|
return base + "/sso/authorize?" + url.Values{
|
||||||
"client_id": {c.cfg.ClientID},
|
"client_id": {c.cfg.ClientID},
|
||||||
"redirect_uri": {c.cfg.RedirectURI},
|
"redirect_uri": {c.cfg.RedirectURI},
|
||||||
@@ -229,7 +249,7 @@ func ValidateStateCookie(w http.ResponseWriter, r *http.Request, prefix, querySt
|
|||||||
// redirect back to it after SSO login completes.
|
// redirect back to it after SSO login completes.
|
||||||
func SetReturnToCookie(w http.ResponseWriter, r *http.Request, prefix string) {
|
func SetReturnToCookie(w http.ResponseWriter, r *http.Request, prefix string) {
|
||||||
path := r.URL.Path
|
path := r.URL.Path
|
||||||
if path == "" || path == "/login" || path == "/sso/callback" {
|
if path == "" || path == "/login" || strings.HasPrefix(path, "/sso/") {
|
||||||
path = "/"
|
path = "/"
|
||||||
}
|
}
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
@@ -268,6 +288,12 @@ func ConsumeReturnToCookie(w http.ResponseWriter, r *http.Request, prefix string
|
|||||||
|
|
||||||
// RedirectToLogin generates a state, sets the state and return-to cookies,
|
// RedirectToLogin generates a state, sets the state and return-to cookies,
|
||||||
// and redirects the user to the MCIAS authorize URL.
|
// and redirects the user to the MCIAS authorize URL.
|
||||||
|
//
|
||||||
|
// The redirect is performed via a 200 response with an HTML meta-refresh
|
||||||
|
// instead of a 302. Some browsers (notably Firefox) do not reliably store
|
||||||
|
// Set-Cookie headers on 302 responses that redirect to a different origin,
|
||||||
|
// even when the origins are same-site. Using a 200 response ensures the
|
||||||
|
// cookies are stored before the browser navigates away.
|
||||||
func RedirectToLogin(w http.ResponseWriter, r *http.Request, client *Client, cookiePrefix string) error {
|
func RedirectToLogin(w http.ResponseWriter, r *http.Request, client *Client, cookiePrefix string) error {
|
||||||
state, err := GenerateState()
|
state, err := GenerateState()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -276,7 +302,15 @@ func RedirectToLogin(w http.ResponseWriter, r *http.Request, client *Client, coo
|
|||||||
|
|
||||||
SetStateCookie(w, cookiePrefix, state)
|
SetStateCookie(w, cookiePrefix, state)
|
||||||
SetReturnToCookie(w, r, cookiePrefix)
|
SetReturnToCookie(w, r, cookiePrefix)
|
||||||
http.Redirect(w, r, client.AuthorizeURL(state), http.StatusFound)
|
|
||||||
|
authorizeURL := client.AuthorizeURL(state)
|
||||||
|
escaped := html.EscapeString(authorizeURL)
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = fmt.Fprintf(w, `<!DOCTYPE html>
|
||||||
|
<html><head><meta http-equiv="refresh" content="0;url=%s"></head>
|
||||||
|
<body><p>Redirecting to <a href="%s">MCIAS</a>...</p></body></html>`,
|
||||||
|
escaped, escaped)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ func TestReturnToDefaultsToRoot(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReturnToSkipsLoginPaths(t *testing.T) {
|
func TestReturnToSkipsLoginPaths(t *testing.T) {
|
||||||
for _, p := range []string{"/login", "/sso/callback"} {
|
for _, p := range []string{"/login", "/sso/callback", "/sso/redirect"} {
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
req := httptest.NewRequest(http.MethodGet, p, nil)
|
req := httptest.NewRequest(http.MethodGet, p, nil)
|
||||||
SetReturnToCookie(rec, req, "mcr")
|
SetReturnToCookie(rec, req, "mcr")
|
||||||
|
|||||||
Reference in New Issue
Block a user