auth: switch user login to session token and decouple tenant access
This commit is contained in:
54
internal/server/authz.go
Normal file
54
internal/server/authz.go
Normal 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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user