refactor(server): rename Org to Instance
Adds migration 0004_org_to_instance, the ScopedCollections list, the AssertNoScopedCollectionMissed boot check, and moves EnsureAuthIndexes into its own file. Two ordering constraints the rename exposed, both now enforced and commented: - 0004 must run BEFORE EnsureAuthIndexes. The index builder creates instances.slug, which would create an empty instances collection and make 0004 refuse to rename orgs onto an existing target. - Migrations 0001 to 0003 run BEFORE 0004 and still read and write org_id, so they use a private legacyOrg struct rather than shared/models.
This commit is contained in:
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
type cachedOrg struct {
|
||||
org *models.Org
|
||||
org *models.Instance
|
||||
at time.Time
|
||||
}
|
||||
|
||||
@@ -23,9 +23,6 @@ var (
|
||||
|
||||
const orgCacheTTL = 60 * time.Second
|
||||
|
||||
|
||||
|
||||
|
||||
func appRootLabel() string {
|
||||
if v := os.Getenv("APP_ROOT_LABEL"); v != "" {
|
||||
return strings.ToLower(v)
|
||||
@@ -33,15 +30,13 @@ func appRootLabel() string {
|
||||
return "vantage"
|
||||
}
|
||||
|
||||
|
||||
|
||||
func hostSlug(host string) string {
|
||||
host = strings.ToLower(host)
|
||||
if i := strings.IndexByte(host, ':'); i >= 0 {
|
||||
host = host[:i]
|
||||
}
|
||||
root := appRootLabel()
|
||||
|
||||
|
||||
parts := strings.Split(host, ".")
|
||||
if len(parts) < 3 {
|
||||
return ""
|
||||
@@ -55,7 +50,7 @@ func hostSlug(host string) string {
|
||||
return parts[0]
|
||||
}
|
||||
|
||||
func OrgFromHost(c *gin.Context) (*models.Org, bool) {
|
||||
func InstanceFromHost(c *gin.Context) (*models.Instance, bool) {
|
||||
slug := hostSlug(c.Request.Host)
|
||||
if slug == "" {
|
||||
return nil, false
|
||||
@@ -67,10 +62,9 @@ func OrgFromHost(c *gin.Context) (*models.Org, bool) {
|
||||
}
|
||||
orgCacheMu.Unlock()
|
||||
|
||||
org, err := services.GetOrgBySlug(slug)
|
||||
org, err := services.GetInstanceBySlug(slug)
|
||||
if err != nil || org == nil {
|
||||
|
||||
|
||||
|
||||
return nil, false
|
||||
}
|
||||
orgCacheMu.Lock()
|
||||
@@ -37,7 +37,7 @@ func HandleLocalLogin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
sessionID, err := SaveSession(c.Request.Context(), &Session{
|
||||
UserID: u.UserID, OrgID: u.OrgID, Role: u.Role, Email: u.Email,
|
||||
UserID: u.UserID, InstanceID: u.InstanceID, Role: u.Role, Email: u.Email,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "session save failed"})
|
||||
@@ -50,13 +50,13 @@ func HandleLocalLogin(c *gin.Context) {
|
||||
|
||||
func HandleBootstrapStatus(c *gin.Context) {
|
||||
var (
|
||||
n int64
|
||||
err error
|
||||
orgName string
|
||||
n int64
|
||||
err error
|
||||
instName string
|
||||
)
|
||||
if org, ok := OrgFromHost(c); ok {
|
||||
n, err = services.CountOrgUsers(org.OrgID)
|
||||
orgName = org.Name
|
||||
if inst, ok := InstanceFromHost(c); ok {
|
||||
n, err = services.CountInstanceUsers(inst.InstanceID)
|
||||
instName = inst.Name
|
||||
} else {
|
||||
n, err = services.CountUsers()
|
||||
}
|
||||
@@ -64,12 +64,9 @@ func HandleBootstrapStatus(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"needs_setup": n == 0, "org_name": orgName})
|
||||
c.JSON(http.StatusOK, gin.H{"needs_setup": n == 0, "instance_name": instName})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
func HandleBootstrap(c *gin.Context) {
|
||||
n, err := services.CountUsers()
|
||||
if err != nil {
|
||||
@@ -81,34 +78,34 @@ func HandleBootstrap(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
OrgName string `json:"org_name"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
InstanceName string `json:"instance_name"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.OrgName == "" || body.Email == "" || len(body.Password) < 8 {
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.InstanceName == "" || body.Email == "" || len(body.Password) < 8 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "org_name, email, and password (>=8 chars) required"})
|
||||
return
|
||||
}
|
||||
|
||||
orgCount, err := services.CountOrgs()
|
||||
orgCount, err := services.CountInstances()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var org *models.Org
|
||||
var inst *models.Instance
|
||||
switch orgCount {
|
||||
case 0:
|
||||
org, err = services.CreateOrg(body.OrgName)
|
||||
inst, err = services.CreateInstance(body.InstanceName)
|
||||
case 1:
|
||||
var existing *models.Org
|
||||
existing, err = services.FirstOrg()
|
||||
var existing *models.Instance
|
||||
existing, err = services.FirstInstance()
|
||||
if err == nil {
|
||||
org, err = services.AdoptOrg(existing.OrgID, body.OrgName)
|
||||
inst, err = services.AdoptInstance(existing.InstanceID, body.InstanceName)
|
||||
}
|
||||
default:
|
||||
c.JSON(http.StatusConflict, gin.H{"error": fmt.Sprintf(
|
||||
"cannot bootstrap: %d organizations already exist but no users do; "+
|
||||
"create the owner against the intended org rather than through setup, "+
|
||||
"create the owner against the intended inst rather than through setup, "+
|
||||
"or remove the unintended orgs and retry", orgCount)})
|
||||
return
|
||||
}
|
||||
@@ -116,20 +113,20 @@ func HandleBootstrap(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
u, err := services.CreateUser(org.OrgID, body.Email, body.Password, "owner", "local")
|
||||
u, err := services.CreateUser(inst.InstanceID, body.Email, body.Password, "owner", "local")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
sessionID, err := SaveSession(c.Request.Context(), &Session{
|
||||
UserID: u.UserID, OrgID: u.OrgID, Role: u.Role, Email: u.Email,
|
||||
UserID: u.UserID, InstanceID: u.InstanceID, Role: u.Role, Email: u.Email,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "session save failed"})
|
||||
return
|
||||
}
|
||||
SetSessionCookie(c, sessionID)
|
||||
c.JSON(http.StatusCreated, gin.H{"org": org, "slug": org.Slug})
|
||||
c.JSON(http.StatusCreated, gin.H{"instance": inst, "slug": inst.Slug})
|
||||
}
|
||||
|
||||
func HandleMe(c *gin.Context) {
|
||||
@@ -143,14 +140,12 @@ func HandleMe(c *gin.Context) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "session expired"})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
if hostOrg, ok := OrgFromHost(c); ok && hostOrg.OrgID != sess.OrgID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "org host mismatch"})
|
||||
|
||||
if hostInstance, ok := InstanceFromHost(c); ok && hostInstance.InstanceID != sess.InstanceID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "instance host mismatch"})
|
||||
return
|
||||
}
|
||||
|
||||
org, _ := services.GetOrg(sess.OrgID)
|
||||
c.JSON(http.StatusOK, gin.H{"user": sess, "org": org})
|
||||
inst, _ := services.GetInstance(sess.InstanceID)
|
||||
c.JSON(http.StatusOK, gin.H{"user": sess, "instance": inst})
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ func GetSessionFromContext(c *gin.Context) *Session {
|
||||
return sess
|
||||
}
|
||||
|
||||
func OrgID(c *gin.Context) string {
|
||||
func InstanceID(c *gin.Context) string {
|
||||
if s := GetSessionFromContext(c); s != nil {
|
||||
return s.OrgID
|
||||
return s.InstanceID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -62,15 +62,15 @@ func Middleware() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
if sess.OrgID == "" {
|
||||
if sess.InstanceID == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "session has no organization"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set(ctxSessionKey, sess)
|
||||
|
||||
if hostOrg, ok := OrgFromHost(c); ok && hostOrg.OrgID != sess.OrgID {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "org host mismatch"})
|
||||
if hostInstance, ok := InstanceFromHost(c); ok && hostInstance.InstanceID != sess.InstanceID {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "instance host mismatch"})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ var (
|
||||
provCache = map[string]*oidc.Provider{}
|
||||
)
|
||||
|
||||
func EvictOIDCProvider(orgID string) {
|
||||
func EvictOIDCProvider(instanceID string) {
|
||||
provMu.Lock()
|
||||
delete(provCache, orgID)
|
||||
delete(provCache, instanceID)
|
||||
provMu.Unlock()
|
||||
}
|
||||
|
||||
@@ -32,17 +32,17 @@ func redirectURL(c *gin.Context) string {
|
||||
return fmt.Sprintf("%s://%s/auth/oidc/callback", scheme, c.Request.Host)
|
||||
}
|
||||
|
||||
func providerForOrg(ctx context.Context, c *gin.Context, orgID string) (*oidc.Provider, *oauth2.Config, error) {
|
||||
cfg, err := services.GetOrgOIDC(orgID)
|
||||
func providerForOrg(ctx context.Context, c *gin.Context, instanceID string) (*oidc.Provider, *oauth2.Config, error) {
|
||||
cfg, err := services.GetInstanceOIDC(instanceID)
|
||||
if err != nil || !cfg.Enabled {
|
||||
return nil, nil, fmt.Errorf("org SSO not configured")
|
||||
return nil, nil, fmt.Errorf("inst SSO not configured")
|
||||
}
|
||||
secret, err := services.GetOrgOIDCSecret(orgID)
|
||||
secret, err := services.GetInstanceOIDCSecret(instanceID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
provMu.Lock()
|
||||
p := provCache[orgID]
|
||||
p := provCache[instanceID]
|
||||
provMu.Unlock()
|
||||
if p == nil {
|
||||
p, err = oidc.NewProvider(ctx, cfg.Issuer)
|
||||
@@ -50,7 +50,7 @@ func providerForOrg(ctx context.Context, c *gin.Context, orgID string) (*oidc.Pr
|
||||
return nil, nil, err
|
||||
}
|
||||
provMu.Lock()
|
||||
provCache[orgID] = p
|
||||
provCache[instanceID] = p
|
||||
provMu.Unlock()
|
||||
}
|
||||
return p, &oauth2.Config{
|
||||
@@ -61,13 +61,13 @@ func providerForOrg(ctx context.Context, c *gin.Context, orgID string) (*oidc.Pr
|
||||
}
|
||||
|
||||
func HandleOIDCStart(c *gin.Context) {
|
||||
org, ok := OrgFromHost(c)
|
||||
inst, ok := InstanceFromHost(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "unknown organization host"})
|
||||
return
|
||||
}
|
||||
ctx := c.Request.Context()
|
||||
_, oauthCfg, err := providerForOrg(ctx, c, org.OrgID)
|
||||
_, oauthCfg, err := providerForOrg(ctx, c, inst.InstanceID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -77,7 +77,7 @@ func HandleOIDCStart(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "state gen failed"})
|
||||
return
|
||||
}
|
||||
if err := SaveStateOrg(ctx, state, org.OrgID); err != nil {
|
||||
if err := SaveStateOrg(ctx, state, inst.InstanceID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "state save failed"})
|
||||
return
|
||||
}
|
||||
@@ -86,12 +86,12 @@ func HandleOIDCStart(c *gin.Context) {
|
||||
|
||||
func HandleOIDCCallback(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
orgID, ok := ConsumeStateOrg(ctx, c.Query("state"))
|
||||
instanceID, ok := ConsumeStateOrg(ctx, c.Query("state"))
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid state"})
|
||||
return
|
||||
}
|
||||
provider, oauthCfg, err := providerForOrg(ctx, c, orgID)
|
||||
provider, oauthCfg, err := providerForOrg(ctx, c, instanceID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -123,19 +123,19 @@ func HandleOIDCCallback(c *gin.Context) {
|
||||
email := strings.ToLower(claims.Email)
|
||||
u, err := services.GetUserByEmail(email)
|
||||
if err != nil {
|
||||
|
||||
u, err = services.CreateUser(orgID, email, "", "member", "oidc")
|
||||
|
||||
u, err = services.CreateUser(instanceID, email, "", "member", "oidc")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "provisioning failed"})
|
||||
return
|
||||
}
|
||||
} else if u.OrgID != orgID {
|
||||
} else if u.InstanceID != instanceID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "email belongs to a different organization"})
|
||||
return
|
||||
}
|
||||
|
||||
sessionID, err := SaveSession(ctx, &Session{
|
||||
UserID: u.UserID, OrgID: u.OrgID, Role: u.Role, Email: u.Email, Name: claims.Name,
|
||||
UserID: u.UserID, InstanceID: u.InstanceID, Role: u.Role, Email: u.Email, Name: claims.Name,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "session save failed"})
|
||||
|
||||
@@ -16,11 +16,11 @@ const sessionPrefix = "km:session:"
|
||||
const statePrefix = "km:state:"
|
||||
|
||||
type Session struct {
|
||||
UserID string `json:"user_id"`
|
||||
OrgID string `json:"org_id"`
|
||||
Role string `json:"role"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
UserID string `json:"user_id"`
|
||||
InstanceID string `json:"instance_id"`
|
||||
Role string `json:"role"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
var rdb *redis.Client
|
||||
@@ -71,14 +71,14 @@ func DeleteSession(ctx context.Context, id string) error {
|
||||
return rdb.Del(ctx, sessionPrefix+id).Err()
|
||||
}
|
||||
|
||||
func SaveStateOrg(ctx context.Context, state, orgID string) error {
|
||||
return rdb.Set(ctx, statePrefix+state, orgID, 10*time.Minute).Err()
|
||||
func SaveStateOrg(ctx context.Context, state, instanceID string) error {
|
||||
return rdb.Set(ctx, statePrefix+state, instanceID, 10*time.Minute).Err()
|
||||
}
|
||||
|
||||
func ConsumeStateOrg(ctx context.Context, state string) (string, bool) {
|
||||
orgID, err := rdb.GetDel(ctx, statePrefix+state).Result()
|
||||
if err != nil || orgID == "" {
|
||||
instanceID, err := rdb.GetDel(ctx, statePrefix+state).Result()
|
||||
if err != nil || instanceID == "" {
|
||||
return "", false
|
||||
}
|
||||
return orgID, true
|
||||
return instanceID, true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user