auth: switch user login to session token and decouple tenant access

This commit is contained in:
2026-03-03 19:45:09 +08:00
parent 67bc6ecae6
commit 3b555df56c
6 changed files with 795 additions and 147 deletions

54
internal/server/authz.go Normal file
View File

@@ -0,0 +1,54 @@
package server
import (
"net/http"
"strconv"
)
type AccessContext struct {
Kind string
TenantID int64
UserID int64
Role string
Token string
}
func (s *Server) ResolveAccess(r *http.Request, masterToken uint64) (*AccessContext, bool) {
tok := BearerToken(r)
if tok == "" {
return nil, false
}
if tok == strconv.FormatUint(masterToken, 10) {
return &AccessContext{Kind: "master", Role: "admin", Token: tok}, true
}
return s.ResolveTenantAccessToken(tok)
}
func (s *Server) ResolveTenantAccessToken(tok string) (*AccessContext, bool) {
if tok == "" || s.store == nil {
return nil, false
}
if ss, err := s.store.VerifySessionToken(tok); err == nil && ss != nil {
return &AccessContext{
Kind: "session",
TenantID: ss.TenantID,
UserID: ss.UserID,
Role: ss.Role,
Token: tok,
}, true
}
if ten, err := s.store.VerifyAPIKey(tok); err == nil && ten != nil {
return &AccessContext{
Kind: "apikey",
TenantID: ten.ID,
Role: "apikey",
Token: tok,
}, true
}
return nil, false
}

View File

@@ -34,12 +34,47 @@ func writeJSON(w http.ResponseWriter, status int, body string) {
}
func (s *Server) HandleAdminCreateTenant(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
tenants, err := s.store.ListTenants()
if err != nil {
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"list tenants failed"}`)
return
}
resp := struct {
Error int `json:"error"`
Message string `json:"message"`
Tenants []store.Tenant `json:"tenants"`
}{0, "ok", tenants}
b, _ := json.Marshal(resp)
writeJSON(w, http.StatusOK, string(b))
return
}
// update tenant status via /api/v1/admin/tenants/{id}?status=0|1
if r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/admin/tenants/") {
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
if len(parts) >= 4 {
var id int64
_, _ = fmt.Sscanf(parts[len(parts)-1], "%d", &id)
st := r.URL.Query().Get("status")
if id > 0 && st != "" {
status := 0
if st == "1" {
status = 1
}
_ = s.store.UpdateTenantStatus(id, status)
writeJSON(w, http.StatusOK, `{"error":0,"message":"ok"}`)
return
}
}
}
if r.Method != http.MethodPost {
writeJSON(w, http.StatusMethodNotAllowed, `{"error":1,"message":"method not allowed"}`)
return
}
var req struct {
Name string `json:"name"`
Name string `json:"name"`
AdminPassword string `json:"admin_password"`
OperatorPassword string `json:"operator_password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" {
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"bad request"}`)
@@ -49,23 +84,39 @@ func (s *Server) HandleAdminCreateTenant(w http.ResponseWriter, r *http.Request)
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"store not ready"}`)
return
}
ten, err := s.store.CreateTenant(req.Name)
var ten *store.Tenant
var admin *store.User
var op *store.User
var err error
if req.AdminPassword != "" && req.OperatorPassword != "" {
ten, admin, op, err = s.store.CreateTenantWithUsers(req.Name, req.AdminPassword, req.OperatorPassword)
} else {
ten, err = s.store.CreateTenant(req.Name)
}
if err != nil {
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"create tenant failed"}`)
return
}
resp := struct {
Error int `json:"error"`
Message string `json:"message"`
Tenant int64 `json:"tenant_id"`
Subnet string `json:"subnet"`
}{0, "ok", ten.ID, ten.Subnet}
Error int `json:"error"`
Message string `json:"message"`
Tenant int64 `json:"tenant_id"`
Subnet string `json:"subnet"`
AdminUser string `json:"admin_user"`
OperatorUser string `json:"operator_user"`
}{0, "ok", ten.ID, ten.Subnet, "", ""}
if admin != nil {
resp.AdminUser = admin.Email
}
if op != nil {
resp.OperatorUser = op.Email
}
b, _ := json.Marshal(resp)
writeJSON(w, http.StatusOK, string(b))
}
func (s *Server) HandleAdminCreateAPIKey(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
if r.Method != http.MethodPost && r.Method != http.MethodGet {
writeJSON(w, http.StatusMethodNotAllowed, `{"error":1,"message":"method not allowed"}`)
return
}
@@ -87,6 +138,37 @@ func (s *Server) HandleAdminCreateAPIKey(w http.ResponseWriter, r *http.Request)
writeJSON(w, http.StatusBadRequest, `{"error":1,"message":"bad request"}`)
return
}
if r.Method == http.MethodGet {
keys, err := s.store.ListAPIKeys(tenantID)
if err != nil {
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"list keys failed"}`)
return
}
resp := struct {
Error int `json:"error"`
Message string `json:"message"`
Keys []store.APIKey `json:"keys"`
}{0, "ok", keys}
b, _ := json.Marshal(resp)
writeJSON(w, http.StatusOK, string(b))
return
}
// update key status via /api/v1/admin/tenants/{id}/keys/{keyId}?status=0|1
if strings.Contains(r.URL.Path, "/keys/") {
parts2 := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
var keyID int64
_, _ = fmt.Sscanf(parts2[len(parts2)-1], "%d", &keyID)
st := r.URL.Query().Get("status")
if keyID > 0 && st != "" {
status := 0
if st == "1" {
status = 1
}
_ = s.store.UpdateAPIKeyStatus(keyID, status)
writeJSON(w, http.StatusOK, `{"error":0,"message":"ok"}`)
return
}
}
var req struct {
Scope string `json:"scope"`
TTL int64 `json:"ttl"` // seconds
@@ -112,21 +194,41 @@ func (s *Server) HandleAdminCreateAPIKey(w http.ResponseWriter, r *http.Request)
}
func (s *Server) HandleTenantEnroll(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
if r.Method != http.MethodPost && r.Method != http.MethodGet {
writeJSON(w, http.StatusMethodNotAllowed, `{"error":1,"message":"method not allowed"}`)
return
}
// tenant auth by API key
// tenant auth by session/apikey
if s.store == nil {
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"store not ready"}`)
return
}
tok := BearerToken(r)
ten, err := s.store.VerifyAPIKey(tok)
if err != nil || ten == nil {
ac, ok := s.ResolveTenantAccessToken(tok)
if !ok || ac.TenantID <= 0 {
writeJSON(w, http.StatusUnauthorized, `{"error":1,"message":"unauthorized"}`)
return
}
ten, err := s.store.GetTenantByID(ac.TenantID)
if err != nil || ten == nil || ten.Status != 1 {
writeJSON(w, http.StatusUnauthorized, `{"error":1,"message":"unauthorized"}`)
return
}
if r.Method == http.MethodGet {
tokens, err := s.store.ListEnrollTokens(ten.ID)
if err != nil {
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"list enroll failed"}`)
return
}
resp := struct {
Error int `json:"error"`
Message string `json:"message"`
Enrolls []store.EnrollToken `json:"enrolls"`
}{0, "ok", tokens}
b, _ := json.Marshal(resp)
writeJSON(w, http.StatusOK, string(b))
return
}
code, err := s.store.CreateEnrollToken(ten.ID, 10*time.Minute, 5)
if err != nil {
writeJSON(w, http.StatusInternalServerError, `{"error":1,"message":"create enroll failed"}`)
@@ -147,6 +249,24 @@ func (s *Server) HandleEnrollConsume(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusMethodNotAllowed, `{"error":1,"message":"method not allowed"}`)
return
}
// revoke support: /api/v1/enroll/consume/{id}?status=0
if strings.Contains(r.URL.Path, "/enroll/consume/") {
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
if len(parts) >= 4 {
var id int64
_, _ = fmt.Sscanf(parts[len(parts)-1], "%d", &id)
st := r.URL.Query().Get("status")
if id > 0 && st != "" {
status := 0
if st == "1" {
status = 1
}
_ = s.store.UpdateEnrollStatus(id, status)
writeJSON(w, http.StatusOK, `{"error":0,"message":"ok"}`)
return
}
}
}
var req struct {
Code string `json:"code"`
NodeName string `json:"node"`
@@ -171,12 +291,13 @@ func (s *Server) HandleEnrollConsume(w http.ResponseWriter, r *http.Request) {
return
}
resp := struct {
Error int `json:"error"`
Message string `json:"message"`
NodeID int64 `json:"node_id"`
Secret string `json:"node_secret"`
Tenant int64 `json:"tenant_id"`
}{0, "ok", cred.NodeID, cred.Secret, cred.TenantID}
Error int `json:"error"`
Message string `json:"message"`
NodeID int64 `json:"node_id"`
Secret string `json:"node_secret"`
Tenant int64 `json:"tenant_id"`
CreatedAt int64 `json:"created_at"`
}{0, "ok", cred.NodeID, cred.Secret, cred.TenantID, cred.CreatedAt}
b, _ := json.Marshal(resp)
writeJSON(w, http.StatusOK, string(b))
}