package api import ( "crypto/rand" "encoding/hex" "encoding/json" "net/http" "strconv" "archivdms/internal/audit" "archivdms/internal/storage" ) // --- Admin CRUD for per-tenant SFTP credentials (internal/sftpserver) --- // // Registered behind s.authAdmin (domain_admin/superadmin), tenant-scoped the // same way handleListUsers/handleCreateUser are: sess.TenantID (from the // domain_admin's own session) picks the tenant for domain admins, while a // tenant-less (superadmin) session must specify tenant_id explicitly on // create and cannot list/revoke without one — SFTP credentials always // belong to exactly one tenant's inbox. type createSFTPCredentialRequest struct { Username string `json:"username"` TenantID *int64 `json:"tenant_id,omitempty"` // required only for tenant-less (superadmin) sessions } type sftpCredentialCreatedResponse struct { storage.SFTPCredential Password string `json:"password"` // returned exactly once, in this response only } func (s *Server) handleCreateSFTPCredential(w http.ResponseWriter, r *http.Request) { sess := sessionFromCtx(r.Context()) var req createSFTPCredentialRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if req.Username == "" { writeError(w, http.StatusBadRequest, "username is required") return } tenantID := sess.TenantID if tenantID == nil { tenantID = req.TenantID } if tenantID == nil { writeError(w, http.StatusBadRequest, "tenant_id is required for tenant-less sessions") return } password, err := randomSFTPPassword() if err != nil { writeError(w, http.StatusInternalServerError, "generate password failed") return } cred, err := s.store.CreateSFTPCredential(r.Context(), *tenantID, req.Username, password) if err != nil { s.audlog.Log(audit.Entry{EventType: audit.EventSFTPCredentialCreate, Username: sess.Username, TenantID: tenantID, Success: false, Detail: err.Error()}) writeError(w, http.StatusBadRequest, "create sftp credential failed (username may already be taken)") return } s.audlog.Log(audit.Entry{ EventType: audit.EventSFTPCredentialCreate, Username: sess.Username, TenantID: tenantID, Success: true, Detail: "sftp_username:" + cred.Username, }) writeJSON(w, http.StatusCreated, sftpCredentialCreatedResponse{SFTPCredential: *cred, Password: password}) } func (s *Server) handleListSFTPCredentials(w http.ResponseWriter, r *http.Request) { sess := sessionFromCtx(r.Context()) if sess.TenantID == nil { writeError(w, http.StatusForbidden, "tenant context required") return } creds, err := s.store.ListSFTPCredentials(r.Context(), *sess.TenantID) if err != nil { writeError(w, http.StatusInternalServerError, "list sftp credentials failed") return } writeJSON(w, http.StatusOK, creds) } func (s *Server) handleRevokeSFTPCredential(w http.ResponseWriter, r *http.Request) { sess := sessionFromCtx(r.Context()) if sess.TenantID == nil { writeError(w, http.StatusForbidden, "tenant context required") return } id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid credential id") return } if err := s.store.RevokeSFTPCredential(r.Context(), id, *sess.TenantID); err != nil { s.audlog.Log(audit.Entry{EventType: audit.EventSFTPCredentialRevoke, Username: sess.Username, TenantID: sess.TenantID, DocumentID: r.PathValue("id"), Success: false, Detail: err.Error()}) writeError(w, http.StatusNotFound, "sftp credential not found") return } s.audlog.Log(audit.Entry{EventType: audit.EventSFTPCredentialRevoke, Username: sess.Username, TenantID: sess.TenantID, DocumentID: r.PathValue("id"), Success: true}) writeJSON(w, http.StatusOK, map[string]string{"status": "revoked"}) } func randomSFTPPassword() (string, error) { b := make([]byte, 20) if _, err := rand.Read(b); err != nil { return "", err } return hex.EncodeToString(b), nil }