Skip to content

Commit

Permalink
Add admin helper to soft-delete a user
Browse files Browse the repository at this point in the history
  • Loading branch information
tobias committed Feb 17, 2024
1 parent d4129f1 commit 1655377
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 46 deletions.
29 changes: 28 additions & 1 deletion src/clojars/admin.clj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
[clojure.java.shell :as shell]
[clojure.pprint :refer [pprint]]
[clojure.string :as str]
[clojure.tools.nrepl.server :as nrepl])
[clojure.tools.nrepl.server :as nrepl]
[next.jdbc :as jdbc])
(:import
java.text.SimpleDateFormat))

Expand Down Expand Up @@ -175,6 +176,32 @@
(println "User is *not* an active member of the group."))))
(println "Clojars TXT record not found."))))

(defn delete-user!
"This is really a soft-deletion, where the user is mostly left in place, but has
a new username of the form `deleted_<current-ms>` everywhere appropriate.
Returns the new username."
[old-username]
(let [user-record (db/find-user *db* old-username)
_ (when (nil? user-record)
(throw (ex-info (format "User %s not found!" old-username) {})))
user-id (:id user-record)
new-username (format "deleted_%s" (System/currentTimeMillis))
empty-default-groups (into []
(filter
#(empty? (db/jars-by-groupname *db* %)))
(db/default-user-groups old-username))]
(jdbc/with-transaction [tx *db*]
(db/disable-otp! tx old-username)
(db/clear-password-reset-code! tx old-username)
(db/disable-deploy-tokens-for-user tx user-id)
(db/rename-user tx old-username new-username)
;; Clear email, set random password
(db/update-user tx new-username ""
(db/hexadecimalize (db/generate-secure-token 20)))
(run! (fn [group] (db/delete-group tx group)) empty-default-groups))
new-username))

(defn handler [mapping]
(nrepl/default-handler
(with-meta
Expand Down
78 changes: 63 additions & 15 deletions src/clojars/db.clj
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,11 @@
:added_by added-by
:admin (boolean admin?)}))

(defn default-user-groups
[username]
(mapv #(format "%s.clojars.%s" % username)
["net" "org"]))

(defn add-user
[db email username password]
(let [record {:email email
Expand All @@ -680,24 +685,44 @@
:send_deploy_emails true
:created (get-time)}]
(sql/insert! db :users (set/rename-keys record {:username user-column}))
(doseq [groupname [(str "net.clojars." username)
(str "org.clojars." username)]]
(doseq [groupname (default-user-groups username)]
(add-member* db groupname SCOPE-ALL username "clojars" true)
(verify-group! db username groupname))
record))

(defn update-user
[db account email username password]
(let [fields {:email email
:username username
:account account}]
(sql/update! db :users
(cond-> fields
true (set/rename-keys {:username user-column})
true (dissoc :account)
(seq password) (assoc :password (bcrypt password)))
{user-column account})
fields))
[db account email password]
(sql/update! db :users
(cond-> {:email email}
(seq password) (assoc :password (bcrypt password)))
{user-column account})
{:email email
:username account
:account account})

(defn rename-user
[db old-name new-name]
(jdbc/with-transaction [tx db]
(sql/update! tx :users
{user-column new-name}
{user-column old-name})
(sql/update! tx :permissions
{user-column new-name}
{user-column old-name})
(sql/update! tx :permissions
{:added_by new-name}
{:added_by old-name})
(sql/update! tx :group_verifications
{:verified_by new-name}
{:verified_by old-name})
(sql/update! tx :jars
{user-column new-name}
{user-column old-name})
(sql/update! tx :audit
{user-column new-name}
{user-column old-name})
{:username new-name
:account new-name}))

(defn update-user-notifications
[db account prefs]
Expand Down Expand Up @@ -739,6 +764,12 @@
{user-column username})
reset-code))

(defn clear-password-reset-code!
[db username]
(sql/update! db :users
{:password_reset_code nil}
{user-column username}))

(defn set-otp-secret-key!
[db username]
(let [secret-key (ot/generate-secret-key)]
Expand Down Expand Up @@ -793,6 +824,14 @@
(update record :token bcrypt))
record))

(defn find-deploy-tokens-for-user
[db user-id]
(mapv #(dissoc % :token)
(q db
{:select :*
:from :deploy_tokens
:where [:= :user_id user-id]})))

(defn consume-deploy-token
[db token-id]
(sql/update! db :deploy_tokens
Expand All @@ -807,6 +846,13 @@
:updated (get-time)}
{:id token-id}))

(defn disable-deploy-tokens-for-user
[db user-id]
(sql/update! db :deploy_tokens
{:disabled true
:updated (get-time)}
{:user_id user-id}))

(defn set-deploy-token-used
[db token-id]
(sql/update! db :deploy_tokens
Expand Down Expand Up @@ -1002,8 +1048,10 @@

;; does not delete jars in the group. should it?
(defn delete-group
[db group-id]
(sql/delete! db :permissions {:group_name group-id}))
[db groupname]
(jdbc/with-transaction [tx db]
(sql/delete! tx :permissions {:group_name groupname})
(sql/delete! tx :group_verifications {:group_name groupname})))

(defn- find-groups-jars-information
[db group-id]
Expand Down
2 changes: 1 addition & 1 deletion src/clojars/web/user.clj
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@
(let [old-email (:email (find-user-by-user-or-email db account))
email-changed? (not= old-email email)
password-changed? (seq password)]
(update-user db account email account password)
(update-user db account email password)
(log/info {:status :success})
(when email-changed?
(event/emit event-emitter :email-changed
Expand Down
67 changes: 67 additions & 0 deletions test/clojars/unit/admin_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
(delete! [_ group# artifact#]
(swap! *search-removals* conj (format "%s/%s" group# artifact#))))
admin/*storage* (storage/fs-storage (:repo (config)))]
(db/add-user admin/*db* "[email protected]" "testuser" "password")
(help/add-verified-group "testuser" "org.ham")
(db/add-jar admin/*db* "testuser" {:group "org.ham" :name "biscuit" :version "1" :description "delete me"})
(db/add-jar admin/*db* "testuser" {:group "org.ham" :name "biscuit" :version "2" :description ""})
Expand Down Expand Up @@ -142,3 +143,69 @@
(admin/verify-group! "testuser" "org.hambiscuit")))
(is (some #{"testuser"} (db/group-activenames help/*db* "org.hambiscuit")))))

(deftest delete-user!-works
;; Given: a user with:
;; - otp
;; - password reset code
;; - deploy token
(db/enable-otp! admin/*db* "testuser")
(db/set-otp-secret-key! admin/*db* "testuser")
(db/set-password-reset-code! admin/*db* "testuser")
(db/add-deploy-token admin/*db* "testuser" "token" nil nil false (db/get-time))

;; And: a shared default group with a jar
(db/add-admin admin/*db* "org.clojars.testuser" db/SCOPE-ALL "otheruser" "testuser")
(db/add-jar admin/*db* "testuser" {:group "org.clojars.testuser"
:name "jar"
:version "1.0"})

;; And: and audit record
(db/add-audit admin/*db* "testing" "testuser" nil nil nil "a message")

(let [user-id (:id (db/find-user admin/*db* "testuser"))

;; When: we mark the user as deleted
new-username (admin/delete-user! "testuser")]

;; Then: the user is really renamed to a deleted placeholder
(is (re-find #"deleted_\d+" new-username))
(is (nil? (db/find-user admin/*db* "testuser")))

;; And: they have no email, and otp & password recovery is cleared
(is (match?
{:email ""
:otp_active false
:otp_recovery_code nil
:otp_secret_key nil
:password_reset_code nil
:user new-username}
(db/find-user admin/*db* new-username)))

;; And: their deploy tokens are disabled
(is (:disabled (first (db/find-deploy-tokens-for-user admin/*db* user-id))))

;; And: empty default groups are deleted
(is (empty? (db/group-allnames admin/*db* "net.clojars.testuser")))
(is (empty? (db/find-group-verification admin/*db* "net.clojars.testuser")))

;; And: non-empty default groups are left in place, but with the new username
(is (= [new-username "otheruser"] (db/group-allnames admin/*db* "org.clojars.testuser")))
(is (some? (db/find-group-verification admin/*db* "org.clojars.testuser")))

;; And: existing non-default groups are left in place, but with the new username
(is (= [new-username] (db/group-allnames admin/*db* "org.ham")))
(is (match? {:verified_by new-username}
(db/find-group-verification admin/*db* "org.ham")))

;; And: any jars they deployed have the new username
(is (seq (db/jars-by-username admin/*db* new-username)))
(is (empty? (db/jars-by-username admin/*db* "testuser")))

;; And: their audit records have been reassigned to the new username
(is (seq (db/find-audit admin/*db* {:username new-username})))
(is (empty? (db/find-audit admin/*db* {:username "testuser"})))))

(deftest delete-user!-with-non-existent-user
(is (thrown-with-msg?
Exception #"User does-not-exist not found!"
(admin/delete-user! "does-not-exist"))))
Loading

0 comments on commit 1655377

Please sign in to comment.