fix(server): per-org settings and ESO read token
The settings collection was a single global document, so every org shared one SMTP config, alert config, retention policy and ESO read token. GetSecretGroupDecryptedAny then flattened every org's secrets for a group into one map, meaning any tenant's token read every tenant's secrets. - settings gains org_id; GetSettings/SaveSettings/RotateSecretsReadToken/ GetWorkflowLogRetentionDays all take orgID - VerifySecretsReadToken replaced by ResolveSecretsReadToken, which resolves the org from the presented token's hash; the ESO endpoint derives its org from the token rather than a session, since it is called machine-to-machine - GetSecretGroupDecryptedAny deleted in favour of the org-scoped variant - settings and token-rotation routes now require owner/admin - offline sweep and log retention resolve org per server / per run - migration 0002 stamps the legacy settings doc with the default org Note: /api/settings now 403s for members; the web settings page needs a matching role check.
This commit is contained in:
@@ -59,9 +59,13 @@ func RegisterRoutes(r *gin.Engine) {
|
||||
|
||||
apiGroup.GET("/audit", listAuditEvents)
|
||||
|
||||
apiGroup.GET("/settings", getSettings)
|
||||
apiGroup.PUT("/settings", saveSettings)
|
||||
apiGroup.POST("/settings/secrets-token", rotateSecretsToken)
|
||||
settings := apiGroup.Group("/settings")
|
||||
settings.Use(auth.RequireRole("owner", "admin"))
|
||||
{
|
||||
settings.GET("", getSettings)
|
||||
settings.PUT("", saveSettings)
|
||||
settings.POST("/secrets-token", rotateSecretsToken)
|
||||
}
|
||||
|
||||
apiGroup.GET("/secrets", listSecretGroups)
|
||||
apiGroup.POST("/secrets", createSecretGroup)
|
||||
@@ -456,7 +460,7 @@ func listAuditEvents(c *gin.Context) {
|
||||
}
|
||||
|
||||
func getSettings(c *gin.Context) {
|
||||
s, err := services.GetSettings()
|
||||
s, err := services.GetSettings(auth.OrgID(c))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -474,7 +478,7 @@ func saveSettings(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := services.SaveSettings(body.Alerts, body.Email, body.WorkflowLogRetentionDays); err != nil {
|
||||
if err := services.SaveSettings(auth.OrgID(c), body.Alerts, body.Email, body.WorkflowLogRetentionDays); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -19,19 +19,29 @@ func validName(s string) bool {
|
||||
return s != "" && len(s) <= 128 && groupNamePattern.MatchString(s)
|
||||
}
|
||||
|
||||
// secretsReadAuth validates the ESO bearer token on the public read endpoint.
|
||||
// ctxSecretsOrgKey carries the org resolved from the ESO bearer token.
|
||||
const ctxSecretsOrgKey = "km_secrets_org"
|
||||
|
||||
// secretsReadAuth validates the ESO bearer token on the public read endpoint
|
||||
// and stashes the org the token belongs to.
|
||||
//
|
||||
// This is the one endpoint whose org does NOT come from the session or the
|
||||
// host: External Secrets Operator calls it machine-to-machine with no session,
|
||||
// so the token itself is the org-bearing credential.
|
||||
func secretsReadAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
const prefix = "Bearer "
|
||||
auth := c.GetHeader("Authorization")
|
||||
if len(auth) <= len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if len(authHeader) <= len(prefix) || !strings.EqualFold(authHeader[:len(prefix)], prefix) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing bearer token"})
|
||||
return
|
||||
}
|
||||
if !services.VerifySecretsReadToken(auth[len(prefix):]) {
|
||||
orgID, ok := services.ResolveSecretsReadToken(authHeader[len(prefix):])
|
||||
if !ok {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
|
||||
return
|
||||
}
|
||||
c.Set(ctxSecretsOrgKey, orgID)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -41,7 +51,9 @@ func secretsReadAuth() gin.HandlerFunc {
|
||||
// (ESO treats 404 as "deleted").
|
||||
func esoGetGroup(c *gin.Context) {
|
||||
group := c.Param("group")
|
||||
values, err := services.GetSecretGroupDecryptedAny(group)
|
||||
// Org comes from the bearer token (set by secretsReadAuth), not a session.
|
||||
orgID := c.GetString(ctxSecretsOrgKey)
|
||||
values, err := services.GetSecretGroupDecrypted(orgID, group)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "store error"})
|
||||
return
|
||||
@@ -179,7 +191,7 @@ func deleteSecretGroup(c *gin.Context) {
|
||||
}
|
||||
|
||||
func rotateSecretsToken(c *gin.Context) {
|
||||
token, err := services.RotateSecretsReadToken()
|
||||
token, err := services.RotateSecretsReadToken(auth.OrgID(c))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user