This commit is contained in:
@@ -48,10 +48,6 @@ func HandleLocalLogin(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
// HandleBootstrapStatus answers "does the caller need to run setup". It is
|
||||
// unauthenticated, so it must not report instance-wide state to whoever asks:
|
||||
// on an org host the answer is scoped to that org, and only the apex — the
|
||||
// genuine first-run entry point — gets the global "no users anywhere" answer.
|
||||
func HandleBootstrapStatus(c *gin.Context) {
|
||||
var (
|
||||
n int64
|
||||
@@ -69,9 +65,9 @@ func HandleBootstrapStatus(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"needs_setup": n == 0})
|
||||
}
|
||||
|
||||
// HandleBootstrap creates the very first org and its owner, so its guard stays
|
||||
// deliberately global: it may run once on an empty instance and never again,
|
||||
// regardless of which host it is called on.
|
||||
|
||||
|
||||
|
||||
func HandleBootstrap(c *gin.Context) {
|
||||
n, err := services.CountUsers()
|
||||
if err != nil {
|
||||
@@ -91,14 +87,7 @@ func HandleBootstrap(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "org_name, email, and password (>=8 chars) required"})
|
||||
return
|
||||
}
|
||||
// An upgrade from single-tenant arrives here with no users but with the org
|
||||
// the migrations created and stamped onto every legacy document. Creating a
|
||||
// second org would put the owner somewhere else entirely, and since every
|
||||
// org-scoped read filters on org_id, the operator would land in an empty
|
||||
// Vantage with all their real data still under the migrated org — silent,
|
||||
// total-looking data loss. So adopt the existing org instead, and only
|
||||
// create when there genuinely is none. Same `switch orgCount` shape as
|
||||
// migration 0002.
|
||||
|
||||
orgCount, err := services.CountOrgs()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
@@ -152,9 +141,9 @@ func HandleMe(c *gin.Context) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "session expired"})
|
||||
return
|
||||
}
|
||||
// /auth/me is registered outside Middleware, so it repeats the middleware's
|
||||
// host/org match itself. Without this, a session for org A presented on org
|
||||
// B's host would render the shell while every /api call 403s.
|
||||
|
||||
|
||||
|
||||
if hostOrg, ok := OrgFromHost(c); ok && hostOrg.OrgID != sess.OrgID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "org host mismatch"})
|
||||
return
|
||||
|
||||
@@ -62,8 +62,6 @@ func Middleware() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// An org-less session would turn every downstream scope into
|
||||
// {"org_id": ""} — fail closed rather than query across tenants.
|
||||
if sess.OrgID == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "session has no organization"})
|
||||
return
|
||||
|
||||
@@ -18,9 +18,6 @@ var (
|
||||
provCache = map[string]*oidc.Provider{}
|
||||
)
|
||||
|
||||
// EvictOIDCProvider drops an org's cached provider so the next login rediscovers
|
||||
// it from the (possibly changed) issuer. Called by the API layer after the org's
|
||||
// OIDC config is saved — services cannot import auth, so the handler wires it.
|
||||
func EvictOIDCProvider(orgID string) {
|
||||
provMu.Lock()
|
||||
delete(provCache, orgID)
|
||||
@@ -35,10 +32,6 @@ func redirectURL(c *gin.Context) string {
|
||||
return fmt.Sprintf("%s://%s/auth/oidc/callback", scheme, c.Request.Host)
|
||||
}
|
||||
|
||||
// providerForOrg returns the org's (cached) OIDC provider plus a request-local
|
||||
// oauth2 config. The config is never stored on the cached entry: RedirectURL is
|
||||
// derived from this request's Host, so sharing it would let one in-flight login
|
||||
// overwrite another's redirect URI.
|
||||
func providerForOrg(ctx context.Context, c *gin.Context, orgID string) (*oidc.Provider, *oauth2.Config, error) {
|
||||
cfg, err := services.GetOrgOIDC(orgID)
|
||||
if err != nil || !cfg.Enabled {
|
||||
@@ -130,7 +123,7 @@ func HandleOIDCCallback(c *gin.Context) {
|
||||
email := strings.ToLower(claims.Email)
|
||||
u, err := services.GetUserByEmail(email)
|
||||
if err != nil {
|
||||
// provision new member in THIS org
|
||||
|
||||
u, err = services.CreateUser(orgID, email, "", "member", "oidc")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "provisioning failed"})
|
||||
|
||||
@@ -23,9 +23,9 @@ var (
|
||||
|
||||
const orgCacheTTL = 60 * time.Second
|
||||
|
||||
// appRootLabel is the DNS label the app is deployed under, i.e. the "vantage"
|
||||
// in <slug>.vantage.<tld>. Deployments on another root must set APP_ROOT_LABEL
|
||||
// or every host resolves to no org, disabling the host/session mismatch guard.
|
||||
|
||||
|
||||
|
||||
func appRootLabel() string {
|
||||
if v := os.Getenv("APP_ROOT_LABEL"); v != "" {
|
||||
return strings.ToLower(v)
|
||||
@@ -33,15 +33,15 @@ func appRootLabel() string {
|
||||
return "vantage"
|
||||
}
|
||||
|
||||
// hostSlug extracts the leftmost DNS label if the host is a subdomain of the
|
||||
// app root. Returns "" for the apex or an unknown host shape.
|
||||
|
||||
|
||||
func hostSlug(host string) string {
|
||||
host = strings.ToLower(host)
|
||||
if i := strings.IndexByte(host, ':'); i >= 0 {
|
||||
host = host[:i]
|
||||
}
|
||||
root := appRootLabel()
|
||||
// Expect <slug>.<root>.<...>; apex is <root>.<...>
|
||||
|
||||
parts := strings.Split(host, ".")
|
||||
if len(parts) < 3 {
|
||||
return ""
|
||||
@@ -69,8 +69,8 @@ func OrgFromHost(c *gin.Context) (*models.Org, bool) {
|
||||
|
||||
org, err := services.GetOrgBySlug(slug)
|
||||
if err != nil || org == nil {
|
||||
// Never cache a miss: a just-bootstrapped org would otherwise 404 on its
|
||||
// own subdomain for the rest of the TTL. Misses are cheap and rare.
|
||||
|
||||
|
||||
return nil, false
|
||||
}
|
||||
orgCacheMu.Lock()
|
||||
|
||||
Reference in New Issue
Block a user