FDN-01: repository & projektgerüst
Git-Repository für bestehenden archivdms-Code initialisiert, Branch-/Commit-Konvention (feature/<ticket>-<slug>-Branches, Ticket-Prefix in Commit-Nachricht) etabliert.
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"archivdms/internal/audit"
|
||||
"archivdms/internal/userstore"
|
||||
)
|
||||
|
||||
func (s *Server) handleListUsers(w http.ResponseWriter, r *http.Request) {
|
||||
sess := sessionFromCtx(r.Context())
|
||||
var users []*userstore.User
|
||||
var err error
|
||||
if sess.TenantID != nil {
|
||||
users, err = s.users.ListByTenant(r.Context(), *sess.TenantID)
|
||||
} else {
|
||||
users, err = s.users.List("")
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "list users failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, users)
|
||||
}
|
||||
|
||||
type createUserRequest struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Role string `json:"role"`
|
||||
// TenantID is only ever evaluated for a superadmin caller (see
|
||||
// handleCreateUser). For any other role it is silently ignored and the
|
||||
// caller's own sess.TenantID is enforced instead — this is a deliberate
|
||||
// IDOR guard: a domain_admin must never be able to steer a created user
|
||||
// into a tenant other than their own by sending a different tenant_id.
|
||||
TenantID *int64 `json:"tenant_id"`
|
||||
}
|
||||
|
||||
func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
sess := sessionFromCtx(r.Context())
|
||||
var req createUserRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
if req.Role == "" {
|
||||
req.Role = userstore.RoleUser
|
||||
}
|
||||
|
||||
// Tenant assignment is rollenabhängig (security-critical, see plan):
|
||||
// - superadmin: may set tenant_id explicitly from the request body,
|
||||
// including nil for another tenant-less superadmin.
|
||||
// - everyone else (domain_admin, user): tenant_id is ALWAYS hard-forced
|
||||
// to the caller's own sess.TenantID; any tenant_id in the request
|
||||
// body is completely ignored, not merely validated, to close the
|
||||
// IDOR hole where a domain_admin could otherwise create a user in a
|
||||
// tenant they don't administer.
|
||||
tenantID := sess.TenantID
|
||||
if sess.Role == userstore.RoleSuperAdmin {
|
||||
tenantID = req.TenantID
|
||||
}
|
||||
|
||||
user, err := s.users.Create(userstore.CreateUserRequest{
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
Password: req.Password,
|
||||
Role: req.Role,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
if err != nil {
|
||||
s.audlog.Log(audit.Entry{EventType: audit.EventUserMgmt, Username: sess.Username, TenantID: sess.TenantID, Success: false, Detail: "create_user_failed"})
|
||||
writeError(w, http.StatusBadRequest, "create user failed")
|
||||
return
|
||||
}
|
||||
|
||||
s.audlog.Log(audit.Entry{EventType: audit.EventUserMgmt, Username: sess.Username, TenantID: sess.TenantID, Success: true, Detail: "user_created:" + user.Username})
|
||||
writeJSON(w, http.StatusCreated, user)
|
||||
}
|
||||
|
||||
type updateUserRequest struct {
|
||||
Email *string `json:"email"`
|
||||
Role *string `json:"role"`
|
||||
Active *bool `json:"active"`
|
||||
Password *string `json:"password"`
|
||||
}
|
||||
|
||||
func (s *Server) handleUpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||
sess := sessionFromCtx(r.Context())
|
||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid user id")
|
||||
return
|
||||
}
|
||||
var req updateUserRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := s.users.Update(id, userstore.UpdateUserRequest{
|
||||
Email: req.Email, Role: req.Role, Active: req.Active, Password: req.Password,
|
||||
})
|
||||
if err != nil {
|
||||
s.audlog.Log(audit.Entry{EventType: audit.EventUserMgmt, Username: sess.Username, TenantID: sess.TenantID, Success: false, Detail: "update_user_failed"})
|
||||
writeError(w, http.StatusBadRequest, "update user failed")
|
||||
return
|
||||
}
|
||||
|
||||
s.audlog.Log(audit.Entry{EventType: audit.EventUserMgmt, Username: sess.Username, TenantID: sess.TenantID, Success: true, Detail: "user_updated:" + user.Username})
|
||||
writeJSON(w, http.StatusOK, user)
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
sess := sessionFromCtx(r.Context())
|
||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid user id")
|
||||
return
|
||||
}
|
||||
if err := s.users.Delete(id); err != nil {
|
||||
s.audlog.Log(audit.Entry{EventType: audit.EventUserMgmt, Username: sess.Username, TenantID: sess.TenantID, Success: false, Detail: "delete_user_failed"})
|
||||
writeError(w, http.StatusBadRequest, "delete user failed")
|
||||
return
|
||||
}
|
||||
s.audlog.Log(audit.Entry{EventType: audit.EventUserMgmt, Username: sess.Username, TenantID: sess.TenantID, Success: true, Detail: "user_deleted"})
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||
}
|
||||
Reference in New Issue
Block a user