handlers.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. package identity
  2. /*
  3. ArozOS Cluster - identity endpoints
  4. Node-facing (signed, served only by the identity origin):
  5. POST /cluster/acn/auth/verify check a username + password hash
  6. GET /cluster/acn/auth/directory full account directory
  7. POST /cluster/acn/auth/setpassword password change forwarded by a member
  8. Admin-facing (/system/cluster/identity/*, mounted admin-only by the core):
  9. status, origin, sync
  10. */
  11. import (
  12. "encoding/json"
  13. "net/http"
  14. "strings"
  15. "imuslab.com/arozos/mod/cluster/acn"
  16. "imuslab.com/arozos/mod/utils"
  17. )
  18. func (i *Manager) registerACNHandlers() {
  19. srv := i.m.Server()
  20. srv.HandleFunc(acn.BasePath+"/auth/verify", i.handleVerify)
  21. srv.HandleFunc(acn.BasePath+"/auth/directory", i.handleDirectory)
  22. srv.HandleFunc(acn.BasePath+"/auth/setpassword", i.handleSetPassword)
  23. }
  24. // requireOrigin rejects node requests when this node is not the identity origin.
  25. func (i *Manager) requireOrigin(w http.ResponseWriter) bool {
  26. if _, self := i.Origin(); !self {
  27. acn.WriteError(w, http.StatusConflict, "this node is not the identity origin of the cluster")
  28. return false
  29. }
  30. return true
  31. }
  32. func (i *Manager) handleVerify(w http.ResponseWriter, r *http.Request, sender *acn.SignedIdentity, body []byte) {
  33. if !i.requireOrigin(w) {
  34. return
  35. }
  36. var req VerifyRequest
  37. if err := json.Unmarshal(body, &req); err != nil || !validUsername(req.Username) || req.PasswordHash == "" {
  38. acn.WriteJSON(w, VerifyResponse{OK: false, Reason: "Invalid username or password"})
  39. return
  40. }
  41. stored, err := i.acc.PasswordHash(req.Username)
  42. if err != nil || stored == "" || !constantTimeEqual(stored, req.PasswordHash) {
  43. acn.WriteJSON(w, VerifyResponse{OK: false, Reason: "Invalid username or password"})
  44. return
  45. }
  46. groups, _ := i.acc.Groups(req.Username)
  47. if groups == nil {
  48. groups = []string{}
  49. }
  50. acn.WriteJSON(w, VerifyResponse{OK: true, Groups: groups})
  51. }
  52. func (i *Manager) handleDirectory(w http.ResponseWriter, r *http.Request, sender *acn.SignedIdentity, body []byte) {
  53. if !i.requireOrigin(w) {
  54. return
  55. }
  56. acn.WriteJSON(w, i.buildDirectory())
  57. }
  58. func (i *Manager) handleSetPassword(w http.ResponseWriter, r *http.Request, sender *acn.SignedIdentity, body []byte) {
  59. if !i.requireOrigin(w) {
  60. return
  61. }
  62. var req SetPasswordRequest
  63. if err := json.Unmarshal(body, &req); err != nil || !validUsername(req.Username) || req.PasswordHash == "" {
  64. acn.WriteError(w, http.StatusBadRequest, "invalid request")
  65. return
  66. }
  67. if !i.acc.UserExists(req.Username) {
  68. acn.WriteError(w, http.StatusNotFound, "user not found on the identity origin")
  69. return
  70. }
  71. if err := i.acc.SetPasswordHash(req.Username, req.PasswordHash); err != nil {
  72. acn.WriteError(w, http.StatusInternalServerError, err.Error())
  73. return
  74. }
  75. acn.WriteJSON(w, map[string]bool{"ok": true})
  76. }
  77. func constantTimeEqual(a string, b string) bool {
  78. if len(a) != len(b) {
  79. return false
  80. }
  81. var diff byte
  82. for k := 0; k < len(a); k++ {
  83. diff |= a[k] ^ b[k]
  84. }
  85. return diff == 0
  86. }
  87. /*
  88. Admin handlers
  89. */
  90. // HandleStatus returns the identity status.
  91. func (i *Manager) HandleStatus(w http.ResponseWriter, r *http.Request) {
  92. js, _ := json.Marshal(i.Status())
  93. utils.SendJSONResponse(w, string(js))
  94. }
  95. // HandleOrigin sets the identity origin: id=self, id=<node id>, or id= (disable).
  96. func (i *Manager) HandleOrigin(w http.ResponseWriter, r *http.Request) {
  97. if r.Method != http.MethodPost {
  98. utils.SendErrorResponse(w, "POST required")
  99. return
  100. }
  101. id := strings.TrimSpace(r.PostFormValue("id"))
  102. if id == "self" {
  103. id = i.m.NodeID()
  104. }
  105. if err := i.m.SetIdentityOrigin(id); err != nil {
  106. utils.SendErrorResponse(w, err.Error())
  107. return
  108. }
  109. utils.SendOK(w)
  110. }
  111. // HandleSync pulls the origin directory immediately.
  112. func (i *Manager) HandleSync(w http.ResponseWriter, r *http.Request) {
  113. if err := i.SyncNow(); err != nil {
  114. utils.SendErrorResponse(w, err.Error())
  115. return
  116. }
  117. js, _ := json.Marshal(i.Status())
  118. utils.SendJSONResponse(w, string(js))
  119. }
  120. // RegisterAdminRoutes mounts the admin handlers on the given register function.
  121. func (i *Manager) RegisterAdminRoutes(register func(pattern string, handler func(http.ResponseWriter, *http.Request))) {
  122. register("/system/cluster/identity/status", i.HandleStatus)
  123. register("/system/cluster/identity/origin", i.HandleOrigin)
  124. register("/system/cluster/identity/sync", i.HandleSync)
  125. }