From 35df6be29794579cf677afad4a3cb8f4c0a2c9c5 Mon Sep 17 00:00:00 2001 From: khaled basbous Date: Tue, 22 Jul 2025 11:26:50 +0300 Subject: [PATCH 01/12] fix(group): Create a subgroup from an operation on parent group instead of using an add template --- .../sixsq/nuvla/server/resources/group.clj | 48 ++++-- .../server/resources/group_lifecycle_test.clj | 149 +++++++++--------- 2 files changed, 106 insertions(+), 91 deletions(-) diff --git a/code/src/com/sixsq/nuvla/server/resources/group.clj b/code/src/com/sixsq/nuvla/server/resources/group.clj index 44f02e025..60f425844 100644 --- a/code/src/com/sixsq/nuvla/server/resources/group.clj +++ b/code/src/com/sixsq/nuvla/server/resources/group.clj @@ -92,28 +92,18 @@ that start with 'nuvla-' are reserved for the server. (defn tpl->group [{:keys [group-identifier] :as resource} request] - (let [id (str resource-type "/" group-identifier) - active-claim (auth/current-active-claim request) - inherit? (and - (not= "group/nuvla-admin" active-claim) - (str/starts-with? active-claim "group/")) - {parent-id :id - parents :parents - :as _group} (when inherit? - (crud/retrieve-by-id-as-admin active-claim)) - user-id (auth/current-user-id request)] + (let [id (str resource-type "/" group-identifier) + user-id (auth/current-user-id request)] (-> resource (dissoc :group-identifier) (assoc :id id :users (cond-> [] - (not= "internal" user-id) (conj user-id))) - (cond-> inherit? (assoc :parents (conj parents parent-id)))))) + (not= "internal" user-id) (conj user-id)))))) (defn add-impl [{{:keys [id] :as body} :body :as request}] (a/throw-cannot-add collection-acl request) - (throw-subgroups-limit-reached request) (-> body u/strip-service-attrs (assoc :id id @@ -197,11 +187,14 @@ that start with 'nuvla-' are reserved for the server. (defmethod crud/set-operations resource-type [{:keys [id] :as resource} request] - (let [invite-op (u/action-map id :invite) - can-manage? (a/can-manage? resource request) - can-edit-data? (a/can-edit-data? resource request)] + (let [invite-op (u/action-map id :invite) + add-subgroup-op (u/action-map id :add-subgroup) + can-manage? (a/can-manage? resource request) + can-edit-data? (a/can-edit-data? resource request) + can-manage-edit? (and can-manage? can-edit-data?)] (cond-> (crud/set-standard-operations resource request) - (and can-manage? can-edit-data?) (update :operations conj invite-op)))) + can-manage-edit? (update :operations conj invite-op) + can-manage-edit? (update :operations conj add-subgroup-op)))) (defn throw-is-already-in-group @@ -252,6 +245,27 @@ that start with 'nuvla-' are reserved for the server. (catch Exception e (or (ex-data e) (throw e))))) +(defmethod crud/do-action [resource-type "add-subgroup"] + [{{:keys [name description group-identifier]} :body {:keys [uuid]} :params :as request}] + (try + (let [parent-id (str resource-type "/" uuid) + {parents :parents :as _parent-group} (-> (crud/retrieve-by-id-as-admin parent-id) + (throw-cannot-manage request "add-subgroup")) + subgroup-parents (conj parents parent-id) + id (str resource-type "/" group-identifier) + user-id (auth/current-user-id request) + body (cond-> {:id id + :parents subgroup-parents + :users (cond-> [] + (not= "internal" user-id) (conj user-id))} + (not (str/blank? name)) (assoc :name name) + (not (str/blank? description)) (assoc :description description))] + (-> (assoc request :body body) + throw-subgroups-limit-reached + add-impl)) + (catch Exception e + (or (ex-data e) (throw e))))) + ;; ;; collection diff --git a/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj b/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj index 87bcbbc19..ca592c611 100644 --- a/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj +++ b/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj @@ -166,7 +166,7 @@ (request invite-url :request :put :body (j/write-value-as-string {:username "jane@example.com" - :redirect-url "https://phishing.com"})) + :redirect-url "https://phishing.com"})) (ltu/body->edn) (ltu/is-status 400) (ltu/message-matches config-nuvla/error-msg-not-authorised-redirect-url))) @@ -204,67 +204,67 @@ :group-identifier group-id}})] (testing "A user should be able to create a group and see it" - (let [abs-uri (-> session-user - (request base-uri - :request-method :post - :body (j/write-value-as-string (valid-create "a"))) - (ltu/body->edn) - (ltu/is-status 201) - (ltu/location-url))] - (-> session-user - (request abs-uri) - (ltu/body->edn) - (ltu/is-status 200) - (ltu/is-key-value :parents nil)))) - - (testing "A group should be able to create a subgroup and see it" - (let [abs-uri (-> session-group-a - (request base-uri - :request-method :post - :body (j/write-value-as-string (valid-create "b"))) - (ltu/body->edn) - (ltu/is-status 201) - (ltu/location-url))] - (-> session-group-a - (request abs-uri) - (ltu/body->edn) - (ltu/is-status 200) - (ltu/is-key-value :parents ["group/a"])) - (testing "subgroup is able to see himself" - (-> session-group-b - (request abs-uri) - (ltu/body->edn) - (ltu/is-status 200))))) - - (testing "A group should be able to create a subgroup and see it with all parents" - (let [abs-uri (-> session-group-b - (request base-uri - :request-method :post - :body (j/write-value-as-string (valid-create "c"))) - (ltu/body->edn) - (ltu/is-status 201) - (ltu/location-url))] - (-> session-group-b - (request abs-uri) - (ltu/body->edn) - (ltu/is-status 200) - (ltu/is-key-value :parents ["group/a" "group/b"])) - - (testing "parents field cannot be updated" - (-> session-admin - (request abs-uri - :request-method :put - :body (j/write-value-as-string {:parents ["change-not-allowed"]})) - (ltu/body->edn) - (ltu/is-status 200) - (ltu/is-key-value :parents ["group/a" "group/b"])) - (-> session-admin - (request (str abs-uri "?select=parents") - :request-method :put - :body (j/write-value-as-string {})) - (ltu/body->edn) - (ltu/is-status 200) - (ltu/is-key-value :parents ["group/a" "group/b"]))))) + (let [abs-uri-grp-a (-> session-user + (request base-uri + :request-method :post + :body (j/write-value-as-string (valid-create "a"))) + (ltu/body->edn) + (ltu/is-status 201) + (ltu/location-url)) + add-subgroup-a-url (-> session-user + (request abs-uri-grp-a) + (ltu/body->edn) + (ltu/is-status 200) + (ltu/is-key-value :parents nil) + (ltu/is-operation-present :add-subgroup) + (ltu/get-op-url :add-subgroup))] + + (testing "A user should be able to create a subgroup level 1 and see it" + (let [abs-uri-grp-b (-> session-user + (request add-subgroup-a-url + :request-method :put + :body (j/write-value-as-string {:group-identifier "b"})) + (ltu/body->edn) + (ltu/is-status 201) + (ltu/location-url)) + add-subgroup-b-url (-> session-user + (request abs-uri-grp-b) + (ltu/body->edn) + (ltu/is-status 200) + (ltu/is-key-value :parents ["group/a"]) + (ltu/is-operation-present :add-subgroup) + (ltu/get-op-url :add-subgroup))] + + (testing "A user should be able to create a subgroup level 2 and see it with all parents" + (let [abs-uri-grp-c (-> session-user + (request add-subgroup-b-url + :request-method :put + :body (j/write-value-as-string {:group-identifier "c"})) + (ltu/body->edn) + (ltu/is-status 201) + (ltu/location-url))] + (-> session-group-b + (request abs-uri-grp-c) + (ltu/body->edn) + (ltu/is-status 200) + (ltu/is-key-value :parents ["group/a" "group/b"]) + (ltu/is-operation-present :add-subgroup)) + + (testing "parents field cannot be updated" + (-> session-admin + (request abs-uri-grp-c + :request-method :put + :body (j/write-value-as-string {:parents ["change-not-allowed"]})) + (ltu/body->edn) + (ltu/is-status 200) + (ltu/is-key-value :parents ["group/a" "group/b"])) + (-> session-admin + (request (str abs-uri-grp-c "?select=parents") + :request-method :put + :body (j/write-value-as-string {})) + (ltu/body->edn) + (ltu/is-status 200) + (ltu/is-key-value :parents ["group/a" "group/b"]))))))))) (testing "A user should not be able to create the 19th group of a group" (let [session-group-d (header session-json authn-info-header @@ -272,25 +272,26 @@ (-> session-user (request base-uri :request-method :post - :body (j/write-value-as-string (valid-create "d"))) + :body (j/write-value-as-string (valid-create "d1"))) ltu/body->edn (ltu/is-status 201)) - (doseq [group-idx (range 19)] - (-> session-group-d - (request base-uri + (doseq [group-idx (range 2 21)] + (-> session-user + (request (str base-uri (str "/d" (dec group-idx)) "/add-subgroup") :request-method :post - :body (j/write-value-as-string (valid-create (str "d" group-idx)))) + :body (j/write-value-as-string {:group-identifier (str "d" group-idx)})) ltu/body->edn (ltu/is-status 201))) - (-> session-group-d - (request (str base-uri "?filter=parents='group/d'&last=0") + (-> session-user + (request (str base-uri "?filter=parents='group/d1'&last=0") :request-method :put) ltu/body->edn - (ltu/is-status 200)) - (-> session-group-d - (request base-uri - :request-method :post - :body (j/write-value-as-string (valid-create "d-unwanted1"))) + (ltu/is-status 200) + (ltu/is-count 19)) + (-> session-user + (request (str base-uri "/d20/add-subgroup") + :request-method :put + :body (j/write-value-as-string {:group-identifier "d-unwanted1"})) ltu/body->edn (ltu/is-status 409) (ltu/message-matches "A group cannot have more than 19 subgroups!")))) From e2670266a1ebffc919c03f80e1dedd089c6cf713 Mon Sep 17 00:00:00 2001 From: khaled basbous Date: Wed, 23 Jul 2025 09:05:55 +0300 Subject: [PATCH 02/12] fix tests --- .../server/resources/group_lifecycle_test.clj | 54 ++--- .../session_password_lifecycle_test.clj | 221 ++++++++++-------- 2 files changed, 146 insertions(+), 129 deletions(-) diff --git a/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj b/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj index ca592c611..0c4e714b3 100644 --- a/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj +++ b/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj @@ -17,7 +17,7 @@ [postal.core :as postal])) -(use-fixtures :once ltu/with-test-server-fixture) +(use-fixtures :each ltu/with-test-server-fixture) (def base-uri (str p/service-context t/resource-type)) @@ -188,8 +188,6 @@ session-json (content-type (session app) "application/json") session-admin (header session-json authn-info-header "user/super group/nuvla-admin group/nuvla-user group/nuvla-anon group/nuvla-admin") session-user (header session-json authn-info-header "user/jane user/jane group/nuvla-user group/nuvla-anon") - session-group-a (header session-json authn-info-header "user/jane group/a user/jane group/nuvla-user group/nuvla-anon group/a") - session-group-b (header session-json authn-info-header "user/jane group/b user/jane group/nuvla-user group/nuvla-anon group/b") href (str group-tpl/resource-type "/generic") @@ -243,7 +241,7 @@ (ltu/body->edn) (ltu/is-status 201) (ltu/location-url))] - (-> session-group-b + (-> session-user (request abs-uri-grp-c) (ltu/body->edn) (ltu/is-status 200) @@ -267,34 +265,32 @@ (ltu/is-key-value :parents ["group/a" "group/b"]))))))))) (testing "A user should not be able to create the 19th group of a group" - (let [session-group-d (header session-json authn-info-header - "user/jane group/d user/jane group/nuvla-user group/nuvla-anon group/d")] + (-> session-user + (request base-uri + :request-method :post + :body (j/write-value-as-string (valid-create "d1"))) + ltu/body->edn + (ltu/is-status 201)) + (doseq [group-idx (range 2 21)] (-> session-user - (request base-uri + (request (str base-uri (str "/d" (dec group-idx)) "/add-subgroup") :request-method :post - :body (j/write-value-as-string (valid-create "d1"))) + :body (j/write-value-as-string {:group-identifier (str "d" group-idx)})) ltu/body->edn - (ltu/is-status 201)) - (doseq [group-idx (range 2 21)] - (-> session-user - (request (str base-uri (str "/d" (dec group-idx)) "/add-subgroup") - :request-method :post - :body (j/write-value-as-string {:group-identifier (str "d" group-idx)})) - ltu/body->edn - (ltu/is-status 201))) - (-> session-user - (request (str base-uri "?filter=parents='group/d1'&last=0") - :request-method :put) - ltu/body->edn - (ltu/is-status 200) - (ltu/is-count 19)) - (-> session-user - (request (str base-uri "/d20/add-subgroup") - :request-method :put - :body (j/write-value-as-string {:group-identifier "d-unwanted1"})) - ltu/body->edn - (ltu/is-status 409) - (ltu/message-matches "A group cannot have more than 19 subgroups!")))) + (ltu/is-status 201))) + (-> session-user + (request (str base-uri "?filter=parents='group/d1'&last=0") + :request-method :put) + ltu/body->edn + (ltu/is-status 200) + (ltu/is-count 19)) + (-> session-user + (request (str base-uri "/d20/add-subgroup") + :request-method :put + :body (j/write-value-as-string {:group-identifier "d-unwanted1"})) + ltu/body->edn + (ltu/is-status 409) + (ltu/message-matches "A group cannot have more than 19 subgroups!"))) (testing "delete group that have children is not allowed" (-> session-admin diff --git a/code/test/com/sixsq/nuvla/server/resources/session_password_lifecycle_test.clj b/code/test/com/sixsq/nuvla/server/resources/session_password_lifecycle_test.clj index dda6ef283..d0fcba1db 100644 --- a/code/test/com/sixsq/nuvla/server/resources/session_password_lifecycle_test.clj +++ b/code/test/com/sixsq/nuvla/server/resources/session_password_lifecycle_test.clj @@ -80,6 +80,12 @@ :name (str "Group " group-id) :description (str "Group " group-id " description")}}) +(defn valid-create-subgrp + [group-id] + {:group-identifier group-id + :name (str "Group " group-id) + :description (str "Group " group-id " description")}) + (deftest lifecycle (let [app (ltu/ring-app) @@ -128,8 +134,7 @@ :request-method :post :body (j/write-value-as-string unauthorized-create)) (ltu/body->edn) - (ltu/is-status 403)) - ) + (ltu/is-status 403))) ;; anon with valid activated user can create session @@ -452,12 +457,11 @@ handler auth/current-authentication :active-claim)))) - (testing "switch to subgroup is possible" - (-> (header session-json authn-info-header (str "user/x " group-a " user/x group/nuvla-user group/nuvla-anon " group-a)) - (request grp-base-uri - :request-method :post - :body (j/write-value-as-string (valid-create-grp "switch-test-b"))) + (-> session-admin + (request (str p/service-context group-a "/add-subgroup") + :request-method :put + :body (j/write-value-as-string (valid-create-subgrp "switch-test-b"))) (ltu/body->edn) (ltu/is-status 201)) @@ -525,51 +529,66 @@ (deftest get-groups-lifecycle-test - (let [app (ltu/ring-app) - session-json (content-type (session app) "application/json") - session-anon (header session-json authn-info-header "user/unknown user/unknown group/nuvla-anon") - session-admin (header session-json authn-info-header "user/super group/nuvla-admin group/nuvla-user group/nuvla-anon group/nuvla-admin") - user-id (create-user session-admin - :username "tarzan" - :password "TarzanTarzan-0" - :activated? true - :email "tarzan@example.org") - session-user (header session-json authn-info-header (str user-id user-id " group/nuvla-user group/nuvla-anon")) - session-group-a (header session-json authn-info-header "user/x group/a user/x group/nuvla-user group/nuvla-anon group/a") - session-group-b (header session-json authn-info-header "user/x group/b user/x group/nuvla-user group/nuvla-anon group/b") - href (str st/resource-type "/password")] - - (-> session-admin - (request grp-base-uri - :request-method :post - :body (j/write-value-as-string (valid-create-grp "a"))) - (ltu/body->edn) - (ltu/is-status 201)) - (-> session-group-a - (request grp-base-uri - :request-method :post - :body (j/write-value-as-string (valid-create-grp "b"))) - (ltu/body->edn) - (ltu/is-status 201)) - (-> session-group-a - (request grp-base-uri - :request-method :post - :body (j/write-value-as-string (valid-create-grp "b1"))) - (ltu/body->edn) - (ltu/is-status 201)) - (-> session-group-b - (request grp-base-uri - :request-method :post - :body (j/write-value-as-string (valid-create-grp "c"))) - (ltu/body->edn) - (ltu/is-status 201)) + (let [app (ltu/ring-app) + session-json (content-type (session app) "application/json") + session-anon (header session-json authn-info-header "user/unknown user/unknown group/nuvla-anon") + session-admin (header session-json authn-info-header "user/super group/nuvla-admin group/nuvla-user group/nuvla-anon group/nuvla-admin") + user-id (create-user session-admin + :username "tarzan" + :password "TarzanTarzan-0" + :activated? true + :email "tarzan@example.org") + session-user (header session-json authn-info-header (str user-id user-id " group/nuvla-user group/nuvla-anon")) + href (str st/resource-type "/password")] + + (let [grp-a-url (-> session-admin + (request grp-base-uri + :request-method :post + :body (j/write-value-as-string (valid-create-grp "a"))) + (ltu/body->edn) + (ltu/is-status 201) + (ltu/location-url)) + grp-a-add-subgroup (-> session-admin + (request grp-a-url) + (ltu/is-status 200) + (ltu/body->edn) + (ltu/is-operation-present :add-subgroup) + (ltu/get-op-url :add-subgroup)) + grp-b-url (-> session-admin + (request grp-a-add-subgroup + :request-method :put + :body (j/write-value-as-string (valid-create-subgrp "b"))) + (ltu/body->edn) + (ltu/is-status 201) + (ltu/location-url)) + grp-b-add-subgroup (-> session-admin + (request grp-b-url) + (ltu/is-status 200) + (ltu/body->edn) + (ltu/is-operation-present :add-subgroup) + (ltu/get-op-url :add-subgroup))] + + + + (-> session-admin + (request grp-a-add-subgroup + :request-method :put + :body (j/write-value-as-string (valid-create-subgrp "b1"))) + (ltu/body->edn) + (ltu/is-status 201)) + (-> session-admin + (request grp-b-add-subgroup + :request-method :put + :body (j/write-value-as-string (valid-create-subgrp "c"))) + (ltu/body->edn) + (ltu/is-status 201))) (let [resp (-> session-anon (request base-uri :request-method :post :body (j/write-value-as-string {:template {:href href - :username "tarzan" - :password "TarzanTarzan-0"}})) + :username "tarzan" + :password "TarzanTarzan-0"}})) (ltu/body->edn) (ltu/is-set-cookie) (ltu/is-status 201)) @@ -667,51 +686,54 @@ (deftest get-peers-lifecycle-test - (let [app (ltu/ring-app) - session-json (content-type (session app) "application/json") - session-anon (header session-json authn-info-header "user/unknown user/unknown group/nuvla-anon") - session-admin (header session-json authn-info-header "user/super group/nuvla-admin group/nuvla-user group/nuvla-anon group/nuvla-admin") - user-id (create-user session-admin - :username "peer0" - :password "Peer0Peer-0" - :activated? true - :email "peer-0@example.org") - peer-1 (create-user session-admin - :username "peer1" - :password "Peer1Peer-1" - :activated? true - :email "peer-1@example.org") - peer-2 (create-user session-admin - :username "peer2" - :password "Peer2Peer-2" - :activated? false - :email "peer-2@example.org") - peer-3 (create-user session-admin - :username "peer3" - :password "Peer3Peer-3" - :activated? true - :email "peer-3@example.org") - session-user (header session-json authn-info-header (str user-id user-id " group/nuvla-user group/nuvla-anon")) - session-group-a (header session-json authn-info-header "user/x group/peers-test-a user/x group/nuvla-user group/nuvla-anon group/peers-test-a") - href (str st/resource-type "/password") - - resp (-> session-anon - (request base-uri - :request-method :post - :body (j/write-value-as-string {:template {:href href - :username "peer0" - :password "Peer0Peer-0"}})) - (ltu/body->edn) - (ltu/is-set-cookie) - (ltu/is-status 201)) - id (ltu/body-resource-id resp) - abs-uri (ltu/location-url resp) - session-with-id (header session-json authn-info-header (str user-id user-id " group/nuvla-user group/nuvla-anon " id)) - get-peers-url (-> session-user - (header authn-info-header (str user-id " " user-id " group/nuvla-user group/nuvla-anon " id)) - (request abs-uri) - (ltu/body->edn) - (ltu/get-op-url :get-peers))] + (let [app (ltu/ring-app) + session-json (content-type (session app) "application/json") + session-anon (header session-json authn-info-header "user/unknown user/unknown group/nuvla-anon") + session-admin (header session-json authn-info-header "user/super group/nuvla-admin group/nuvla-user group/nuvla-anon group/nuvla-admin") + user-id (create-user session-admin + :username "peer0" + :password "Peer0Peer-0" + :activated? true + :email "peer-0@example.org") + peer-1 (create-user session-admin + :username "peer1" + :password "Peer1Peer-1" + :activated? true + :email "peer-1@example.org") + peer-2 (create-user session-admin + :username "peer2" + :password "Peer2Peer-2" + :activated? false + :email "peer-2@example.org") + peer-3 (create-user session-admin + :username "peer3" + :password "Peer3Peer-3" + :activated? true + :email "peer-3@example.org") + grp-a-id "group/peers-test-a" + user-authn-header (str user-id " " user-id " group/nuvla-user group/nuvla-anon") + session-user (header session-json authn-info-header user-authn-header) + href (str st/resource-type "/password") + + resp (-> session-anon + (request base-uri + :request-method :post + :body (j/write-value-as-string {:template {:href href + :username "peer0" + :password "Peer0Peer-0"}})) + (ltu/body->edn) + (ltu/is-set-cookie) + (ltu/is-status 201)) + id (ltu/body-resource-id resp) + abs-uri (ltu/location-url resp) + user-authn-header-with-id (str user-authn-header " " id) + + session-with-id (header session-json authn-info-header user-authn-header-with-id) + get-peers-url (-> session-with-id + (request abs-uri) + (ltu/body->edn) + (ltu/is-status 200) + (ltu/get-op-url :get-peers))] (testing "admin should get all users with validated emails" (-> session-admin @@ -732,9 +754,8 @@ (ltu/body) (= {}) (is "Get peers body should be empty"))) - - (-> session-admin - (request (-> session-admin + (-> session-with-id + (request (-> session-with-id (request grp-base-uri :request-method :post :body (j/write-value-as-string (valid-create-grp "peers-test-a"))) @@ -758,11 +779,11 @@ (is "Get peers body should be himself and peer-1"))) (testing "user should get peers of subgroup also" - (-> session-admin - (request (-> session-group-a - (request grp-base-uri - :request-method :post - :body (j/write-value-as-string (valid-create-grp "peers-test-b"))) + (-> session-user + (request (-> session-user + (request (str p/service-context grp-a-id "/add-subgroup") + :request-method :put + :body (j/write-value-as-string (valid-create-subgrp "peers-test-b"))) (ltu/body->edn) (ltu/is-status 201) (ltu/location-url)) From 49805cffbff7f60cd516b883078197ea36f26fae Mon Sep 17 00:00:00 2001 From: khaled basbous Date: Thu, 24 Jul 2025 08:33:06 +0300 Subject: [PATCH 03/12] fix(group): act on groups always with my user-id even if I switched my active claim to a group. Excluding special groups. --- code/src/com/sixsq/nuvla/auth/utils.clj | 11 ++ .../sixsq/nuvla/server/resources/group.clj | 161 +++++++++--------- .../server/resources/group_lifecycle_test.clj | 1 - 3 files changed, 96 insertions(+), 77 deletions(-) diff --git a/code/src/com/sixsq/nuvla/auth/utils.clj b/code/src/com/sixsq/nuvla/auth/utils.clj index 6a5ae5ae9..19be240a2 100644 --- a/code/src/com/sixsq/nuvla/auth/utils.clj +++ b/code/src/com/sixsq/nuvla/auth/utils.clj @@ -48,3 +48,14 @@ :claims (filter #(str/starts-with? % "session/")) first)) + +(defn request-as-user + [request] + (let [user-id (current-user-id request) + active-claim (current-active-claim request)] + (if (and + (str/starts-with? active-claim "group/") + (not (#{"group/nuvla-admin" "group/nuvla-user" "group/nuvla-anon"} active-claim))) + (update request :nuvla/authn merge {:active-claim user-id + :claims #{user-id "group/nuvla-user" "group/nuvla-anon"}}) + request))) diff --git a/code/src/com/sixsq/nuvla/server/resources/group.clj b/code/src/com/sixsq/nuvla/server/resources/group.clj index 60f425844..9f538cd65 100644 --- a/code/src/com/sixsq/nuvla/server/resources/group.clj +++ b/code/src/com/sixsq/nuvla/server/resources/group.clj @@ -114,38 +114,40 @@ that start with 'nuvla-' are reserved for the server. crud/validate db/add)) - (defmethod crud/add resource-type - [{:keys [body] :as request}] - (a/throw-cannot-add collection-acl request) - (let [authn-info (auth/current-authentication request) - desc-attrs (u/select-desc-keys body) - body (-> body - (assoc :resource-type create-type) - (std-crud/resolve-hrefs authn-info) - (update-in [:template] merge desc-attrs) ;; validate desc attrs - (crud/validate) - :template - (tpl->group request))] - (add-impl (assoc request :body body)))) + [{:keys [body] :as original-request}] + (let [request (auth/request-as-user original-request)] + (a/throw-cannot-add collection-acl request) + (let [authn-info (auth/current-authentication request) + desc-attrs (u/select-desc-keys body) + body (-> body + (assoc :resource-type create-type) + (std-crud/resolve-hrefs authn-info) + (update-in [:template] merge desc-attrs) ;; validate desc attrs + (crud/validate) + :template + (tpl->group request))] + (add-impl (assoc request :body body))))) (def retrieve-impl (std-crud/retrieve-fn resource-type)) (defmethod crud/retrieve resource-type - [request] - (retrieve-impl request)) + [original-request] + (let [request (auth/request-as-user original-request)] + (retrieve-impl request))) (def edit-impl (std-crud/edit-fn resource-type)) (defmethod crud/edit resource-type - [request] - (let [id (str resource-type "/" (-> request :params :uuid)) - users (get-in request [:body :users] []) - acl (get-in request [:body :acl] (:acl (crud/retrieve-by-id-as-admin id)))] + [original-request] + (let [request (auth/request-as-user original-request) + id (str resource-type "/" (-> request :params :uuid)) + users (get-in request [:body :users] []) + acl (get-in request [:body :acl] (:acl (crud/retrieve-by-id-as-admin id)))] (-> request (assoc-in [:body :acl] acl) (update-in [:body :acl :view-meta] (comp vec set concat) (conj users id)) @@ -174,10 +176,11 @@ that start with 'nuvla-' are reserved for the server. (defmethod crud/delete resource-type - [request] - (-> request - throw-when-have-child - delete-impl)) + [original-request] + (let [request (auth/request-as-user original-request)] + (-> request + throw-when-have-child + delete-impl))) ;; @@ -212,59 +215,64 @@ that start with 'nuvla-' are reserved for the server. (defmethod crud/do-action [resource-type "invite"] - [{base-uri :base-uri {username :username - redirect-url :redirect-url - set-password-url :set-password-url} :body {uuid :uuid} :params :as request}] - (try - (config-nuvla/throw-is-not-authorised-redirect-url redirect-url) - (let [id (str resource-type "/" uuid) - user-id (auth-password/identifier->user-id username) - _group (-> (crud/retrieve-by-id-as-admin id) - (throw-cannot-manage request "invite") - (throw-is-already-in-group user-id)) - invited-by (auth-password/invited-by request) - email (if-let [email-address (some-> user-id auth-password/user-id->email)] - email-address - (if (s/valid? ::spec-core/email username) - username - (throw (r/ex-response (str "invalid email '" username "'") 400)))) - callback-url (callback-join-group/create-callback - base-uri id - :data (cond-> {:email email} - user-id (assoc :user-id user-id) - redirect-url (assoc :redirect-url redirect-url) - set-password-url (assoc :set-password-url set-password-url)) - :expires (u/ttl->timestamp 2592000)) ;; expire after one month - invite-url (if (and (nil? user-id) set-password-url) - (str set-password-url "?callback=" (gen-util/encode-uri-component callback-url) - "&type=" (gen-util/encode-uri-component "invitation") - "&username=" (gen-util/encode-uri-component email)) - callback-url)] - (email-utils/send-join-group-email id invited-by invite-url email) - (r/map-response (format "successfully invited to %s" id) 200 id)) - (catch Exception e - (or (ex-data e) (throw e))))) + [original-request] + (let [{base-uri :base-uri + {username :username + redirect-url :redirect-url + set-password-url :set-password-url} :body + {uuid :uuid} :params :as request} (auth/request-as-user original-request)] + (try + (config-nuvla/throw-is-not-authorised-redirect-url redirect-url) + (let [id (str resource-type "/" uuid) + user-id (auth-password/identifier->user-id username) + _group (-> (crud/retrieve-by-id-as-admin id) + (throw-cannot-manage request "invite") + (throw-is-already-in-group user-id)) + invited-by (auth-password/invited-by request) + email (if-let [email-address (some-> user-id auth-password/user-id->email)] + email-address + (if (s/valid? ::spec-core/email username) + username + (throw (r/ex-response (str "invalid email '" username "'") 400)))) + callback-url (callback-join-group/create-callback + base-uri id + :data (cond-> {:email email} + user-id (assoc :user-id user-id) + redirect-url (assoc :redirect-url redirect-url) + set-password-url (assoc :set-password-url set-password-url)) + :expires (u/ttl->timestamp 2592000)) ;; expire after one month + invite-url (if (and (nil? user-id) set-password-url) + (str set-password-url "?callback=" (gen-util/encode-uri-component callback-url) + "&type=" (gen-util/encode-uri-component "invitation") + "&username=" (gen-util/encode-uri-component email)) + callback-url)] + (email-utils/send-join-group-email id invited-by invite-url email) + (r/map-response (format "successfully invited to %s" id) 200 id)) + (catch Exception e + (or (ex-data e) (throw e)))))) (defmethod crud/do-action [resource-type "add-subgroup"] - [{{:keys [name description group-identifier]} :body {:keys [uuid]} :params :as request}] - (try - (let [parent-id (str resource-type "/" uuid) - {parents :parents :as _parent-group} (-> (crud/retrieve-by-id-as-admin parent-id) - (throw-cannot-manage request "add-subgroup")) - subgroup-parents (conj parents parent-id) - id (str resource-type "/" group-identifier) - user-id (auth/current-user-id request) - body (cond-> {:id id - :parents subgroup-parents - :users (cond-> [] - (not= "internal" user-id) (conj user-id))} - (not (str/blank? name)) (assoc :name name) - (not (str/blank? description)) (assoc :description description))] - (-> (assoc request :body body) - throw-subgroups-limit-reached - add-impl)) - (catch Exception e - (or (ex-data e) (throw e))))) + [original-request] + (let [{{:keys [name description group-identifier]} :body + {:keys [uuid]} :params :as request} (auth/request-as-user original-request)] + (try + (let [parent-id (str resource-type "/" uuid) + {parents :parents :as _parent-group} (-> (crud/retrieve-by-id-as-admin parent-id) + (throw-cannot-manage request "add-subgroup")) + subgroup-parents (conj parents parent-id) + id (str resource-type "/" group-identifier) + user-id (auth/current-user-id request) + body (cond-> {:id id + :parents subgroup-parents + :users (cond-> [] + (not= "internal" user-id) (conj user-id))} + (not (str/blank? name)) (assoc :name name) + (not (str/blank? description)) (assoc :description description))] + (-> (assoc request :body body) + throw-subgroups-limit-reached + add-impl)) + (catch Exception e + (or (ex-data e) (throw e)))))) ;; @@ -275,8 +283,9 @@ that start with 'nuvla-' are reserved for the server. (defmethod crud/query resource-type - [request] - (query-impl request)) + [original-request] + (let [request (auth/request-as-user original-request)] + (query-impl request))) ;; diff --git a/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj b/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj index 0c4e714b3..8ea09ad95 100644 --- a/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj +++ b/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj @@ -183,7 +183,6 @@ (deftest lifecycle-subgroup-creation - (let [app (ltu/ring-app) session-json (content-type (session app) "application/json") session-admin (header session-json authn-info-header "user/super group/nuvla-admin group/nuvla-user group/nuvla-anon group/nuvla-admin") From 2a2ad300353887142b72aec0f7aca8361b3fa704 Mon Sep 17 00:00:00 2001 From: khaled basbous Date: Thu, 24 Jul 2025 10:23:19 +0300 Subject: [PATCH 04/12] fix(session): Get all groups when admin allowing to see full hierarchy and switch to any group from admin --- .../com/sixsq/nuvla/server/resources/session.clj | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/code/src/com/sixsq/nuvla/server/resources/session.clj b/code/src/com/sixsq/nuvla/server/resources/session.clj index e947ea2c1..390009f6f 100644 --- a/code/src/com/sixsq/nuvla/server/resources/session.clj +++ b/code/src/com/sixsq/nuvla/server/resources/session.clj @@ -81,6 +81,7 @@ status, a 'set-cookie' header, and a 'location' header with the created (:require [clojure.set :as set] [clojure.string :as str] + [com.sixsq.nuvla.auth.acl-resource :as acl-resource] [com.sixsq.nuvla.auth.acl-resource :as a] [com.sixsq.nuvla.auth.cookies :as cookies] [com.sixsq.nuvla.auth.utils :as auth] @@ -401,8 +402,10 @@ status, a 'set-cookie' header, and a 'location' header with the created (defn resolve-user-groups - [{:keys [user] :as session}] - (let [root-groups (query-group (str "users='" user "'")) + [{:keys [user] :as session} request] + (let [root-groups (if (acl-resource/is-admin-request? request) + (query-group "parent=null") + (query-group (str "users='" user "'"))) subgroups (if (seq root-groups) (->> root-groups (mapv :id) @@ -443,7 +446,7 @@ status, a 'set-cookie' header, and a 'location' header with the created (-> request retrieve-session (a/throw-cannot-manage request) - resolve-user-groups + (resolve-user-groups request) :user-groups build-group-hierarchy r/json-response) @@ -470,7 +473,7 @@ status, a 'set-cookie' header, and a 'location' header with the created (-> (str resource-type "/" uuid) db/retrieve (a/throw-cannot-edit request) - (resolve-user-groups) + (resolve-user-groups request) (throw-switch-group-not-authorized request) (update-cookie-session request)) (catch Exception e @@ -483,7 +486,7 @@ status, a 'set-cookie' header, and a 'location' header with the created (let [{:keys [root-groups subgroups]} (-> request retrieve-session (a/throw-cannot-manage request) - resolve-user-groups + (resolve-user-groups request) :user-groups) filter-validated "validated=true" filter-emails (if (a/is-admin-request? request) From 1a30e47efe3ecfa2076c4012e60a9c86da498b4a Mon Sep 17 00:00:00 2001 From: Khaled Basbous Date: Wed, 30 Jul 2025 09:00:08 +0300 Subject: [PATCH 05/12] fix(callback join group): notify group manager when a user accept his invitation fix(email utils): Refactor and add unit tests --- .../server/resources/callback_join_group.clj | 4 + .../nuvla/server/resources/email/utils.clj | 171 +++++++----------- .../sixsq/nuvla/server/resources/group.clj | 4 +- .../server/resources/email/utils_test.clj | 19 ++ .../server/resources/group_lifecycle_test.clj | 4 +- 5 files changed, 98 insertions(+), 104 deletions(-) create mode 100644 code/test/com/sixsq/nuvla/server/resources/email/utils_test.clj diff --git a/code/src/com/sixsq/nuvla/server/resources/callback_join_group.clj b/code/src/com/sixsq/nuvla/server/resources/callback_join_group.clj index 8a3a9011e..918a1cdf9 100644 --- a/code/src/com/sixsq/nuvla/server/resources/callback_join_group.clj +++ b/code/src/com/sixsq/nuvla/server/resources/callback_join_group.clj @@ -13,6 +13,7 @@ visited, the email identifier is marked as validated. [com.sixsq.nuvla.server.resources.common.std-crud :as std-crud] [com.sixsq.nuvla.server.resources.common.utils :as u] [com.sixsq.nuvla.server.resources.credential-hashed-password :as hashed-password] + [com.sixsq.nuvla.server.resources.email.utils :as email-utils] [com.sixsq.nuvla.server.resources.user-template :as p] [com.sixsq.nuvla.server.resources.user-template-minimum :as user-minimum] [com.sixsq.nuvla.server.resources.user.utils :as user-utils] @@ -57,6 +58,7 @@ visited, the email identifier is marked as validated. [{callback-id :id {existing-user-id :user-id email :email + inviter-email :inviter-email redirect-url :redirect-url} :data {group-id :href} :target-resource :as _callback-resource} {{:keys [new-password]} :body :as _request}] @@ -67,6 +69,8 @@ visited, the email identifier is marked as validated. msg (format "'%s' successfully joined '%s'" user-id group-id)] (add-user-to-group group-id user-id) (utils/callback-succeeded! callback-id) + (when inviter-email + (email-utils/send-group-invitation-accepted group-id email inviter-email)) (if (and existing-user-id redirect-url) (r/map-response msg 303 callback-id redirect-url) (r/map-response msg 200))) diff --git a/code/src/com/sixsq/nuvla/server/resources/email/utils.clj b/code/src/com/sixsq/nuvla/server/resources/email/utils.clj index b174079b2..114fed1c4 100644 --- a/code/src/com/sixsq/nuvla/server/resources/email/utils.clj +++ b/code/src/com/sixsq/nuvla/server/resources/email/utils.clj @@ -10,6 +10,37 @@ [com.sixsq.nuvla.server.resources.email.sending :as sending] [com.sixsq.nuvla.server.util.response :as r])) +(defn send-email + [address generate-subject-fn generate-body-fn data] + (let [{:keys [smtp-username] :as nuvla-config} (crud/retrieve-by-id-as-admin config-nuvla/config-instance-url) + data (merge data (select-keys nuvla-config [:smtp-username :conditions-url :email-header-img-url])) + subject (generate-subject-fn nuvla-config data) + body (generate-body-fn nuvla-config data) + msg {:from (or smtp-username "administrator") + :to [address] + :subject subject + :body body}] + (sending/dispatch nuvla-config msg))) + +(defn create-callback [email-id base-uri] + (let [callback-request {:params {:resource-name callback/resource-type} + :body {:action email-callback/action-name + :target-resource {:href email-id}} + :nuvla/authn auth/internal-identity} + + {{:keys [resource-id]} :body status :status} (crud/add callback-request)] + (if (= 201 status) + (if-let [callback-resource (crud/set-operations + (crud/retrieve-by-id-as-admin resource-id) {})] + (if-let [validate-op (u/get-op callback-resource "execute")] + (str base-uri validate-op) + (let [msg "callback does not have execute operation"] + (throw (ex-info msg (r/map-response msg 500 resource-id))))) + (let [msg "cannot retrieve email validation callback"] + (throw (ex-info msg (r/map-response msg 500 resource-id))))) + (let [msg "cannot create email validation callback"] + (throw (ex-info msg (r/map-response msg 500 email-id))))))) + (def warning-initiate (str/join "\n" ["If you didn't initiate this request, do NOT click on any link and report" @@ -23,7 +54,7 @@ "\n %s\n"]))) (defn validation-email-body - [callback-url conditions-url email-header-img-url] + [{:keys [conditions-url email-header-img-url] :as _nuvla-config} {:keys [callback-url] :as _data}] [:alternative {:type "text/plain" :content (cond-> (format @@ -44,13 +75,13 @@ (defn invitation-email-body - [name set-password-url conditions-url email-header-img-url] + [{:keys [conditions-url email-header-img-url] :as _nuvla-config} {:keys [name-or-id set-password-url] :as _data}] [:alternative {:type "text/plain" :content (cond-> (format (str/join "\n" ["You have been invited by \"%s\" to use Nuvla." " To accept the invitation, follow this link:" - "\n %s\n"]) name set-password-url) + "\n %s\n"]) name-or-id set-password-url) conditions-url (str (conditions-acceptance conditions-url)))} {:type "text/html; charset=utf-8" :content (sending/render-content @@ -58,65 +89,13 @@ :button-text "Accept invitation" :button-url set-password-url :text-1 (str - (format "You have been invited by \"%s\" to use Nuvla. " name) + (format "You have been invited by \"%s\" to use Nuvla. " name-or-id) "To accept the invitation, click the following button:") :conditions-url conditions-url :header-img email-header-img-url})}]) - -(defn create-callback [email-id base-uri] - (let [callback-request {:params {:resource-name callback/resource-type} - :body {:action email-callback/action-name - :target-resource {:href email-id}} - :nuvla/authn auth/internal-identity} - - {{:keys [resource-id]} :body status :status} (crud/add callback-request)] - (if (= 201 status) - (if-let [callback-resource (crud/set-operations - (crud/retrieve-by-id-as-admin resource-id) {})] - (if-let [validate-op (u/get-op callback-resource "execute")] - (str base-uri validate-op) - (let [msg "callback does not have execute operation"] - (throw (ex-info msg (r/map-response msg 500 resource-id))))) - (let [msg "cannot retrieve email validation callback"] - (throw (ex-info msg (r/map-response msg 500 resource-id))))) - (let [msg "cannot create email validation callback"] - (throw (ex-info msg (r/map-response msg 500 email-id))))))) - - -(defn send-validation-email - [callback-url address] - (let [{:keys [smtp-username conditions-url email-header-img-url] - :as nuvla-config} (crud/retrieve-by-id-as-admin config-nuvla/config-instance-url) - - body (validation-email-body callback-url conditions-url email-header-img-url) - - msg {:from (or smtp-username "administrator") - :to [address] - :subject "Validation email for Nuvla service" - :body body}] - - (sending/dispatch nuvla-config msg))) - - -(defn send-invitation-email - [set-password-url address {:keys [name id] :as _user}] - (let [{:keys [smtp-username conditions-url email-header-img-url] - :as nuvla-config} (crud/retrieve-by-id-as-admin config-nuvla/config-instance-url) - - body (invitation-email-body (or name id) set-password-url - conditions-url email-header-img-url) - - msg {:from (or smtp-username "administrator") - :to [address] - :subject "Invitation by email for Nuvla service" - :body body}] - - (sending/dispatch nuvla-config msg))) - - (defn password-set-email-body - [set-password-url email-header-img-url] + [{:keys [email-header-img-url] :as _nuvla-config} {:keys [set-password-url] :as _data}] [:alternative {:type "text/plain" :content (format @@ -134,9 +113,8 @@ :warning-initiate true :header-img email-header-img-url})}]) - -(defn email-token-2fa - [token email-header-img-url] +(defn email-token-2fa-body + [{:keys [email-header-img-url] :as _nuvla-config} {:keys [token] :as _data}] [:alternative {:type "text/plain" :content (format @@ -153,9 +131,8 @@ :warning-initiate true :header-img email-header-img-url})}]) - (defn join-group-email-body - [group invited-by callback-url conditions-url email-header-img-url] + [{:keys [conditions-url email-header-img-url] :as _nuvla-config} {:keys [group invited-by callback-url] :as _data}] (let [msg (format "You have been invited by \"%s\" to join \"%s\" on Nuvla. " invited-by group) note "Note that you will be visible to all current and future members of this group. "] [:alternative @@ -180,52 +157,44 @@ :conditions-url conditions-url :header-img email-header-img-url})}])) +(defn group-invitation-email-body + [{:keys [email-header-img-url] :as _nuvla-config} {:keys [group invited-email] :as _data}] + (let [msg (format "Your invitation to group %s has been accepted by %s." group invited-email)] + [:alternative + {:type "text/plain" + :content msg} + {:type "text/html; charset=utf-8" + :content (sending/render-content + {:title (format "Invitation to group %s has been accepted" group) + :text-1 msg + :header-img email-header-img-url})}])) -(defn send-password-set-email - [set-password-url address] - (let [{:keys [smtp-username email-header-img-url] - :as nuvla-config} (crud/retrieve-by-id-as-admin - config-nuvla/config-instance-url) - - body (password-set-email-body set-password-url email-header-img-url) - - msg {:from (or smtp-username "administrator") - :to [address] - :subject "Set password for Nuvla service" - :body body}] +(defn send-validation-email + [callback-url address] + (send-email address (constantly "Validation email for Nuvla service") + validation-email-body {:callback-url callback-url})) - (sending/dispatch nuvla-config msg))) +(defn send-invitation-email + [set-password-url address {:keys [name id] :as _user}] + (send-email address (constantly "Invitation by email for Nuvla service") + invitation-email-body {:name-or-id (or name id) + :set-password-url set-password-url})) +(defn send-password-set-email + [set-password-url address] + (send-email address (constantly "Set password for Nuvla service") + password-set-email-body {:set-password-url set-password-url})) (defn send-join-group-email [group invited-by callback-url address] - (let [{:keys [smtp-username conditions-url email-header-img-url] - :as nuvla-config} (crud/retrieve-by-id-as-admin - config-nuvla/config-instance-url) - - body (join-group-email-body group invited-by callback-url - conditions-url email-header-img-url) - - msg {:from (or smtp-username "administrator") - :to [address] - :subject (format "You’re invited to join %s" group) - :body body}] - - (sending/dispatch nuvla-config msg))) + (send-email address (fn [_ {:keys [group] :as _data}] (format "You’re invited to join %s" group)) + join-group-email-body {:group group :invited-by invited-by :callback-url callback-url})) +(defn send-group-invitation-accepted + [group invited-email address] + (send-email address (fn [_ {:keys [group] :as _data}] (format "Invitation to group %s has been accepted" group)) + group-invitation-email-body {:group group :invited-email invited-email})) (defn send-email-token-2fa [token address] - (let [{:keys [smtp-username email-header-img-url] - :as nuvla-config} (crud/retrieve-by-id-as-admin - config-nuvla/config-instance-url) - - body (email-token-2fa token email-header-img-url) - - msg {:from (or smtp-username "administrator") - :to [address] - :subject "Nuvla authorization code" - :body body}] - - (sending/dispatch nuvla-config msg))) - + (send-email address (constantly "Nuvla authorization code") email-token-2fa-body {:token token})) diff --git a/code/src/com/sixsq/nuvla/server/resources/group.clj b/code/src/com/sixsq/nuvla/server/resources/group.clj index 9f538cd65..1e131af80 100644 --- a/code/src/com/sixsq/nuvla/server/resources/group.clj +++ b/code/src/com/sixsq/nuvla/server/resources/group.clj @@ -228,6 +228,7 @@ that start with 'nuvla-' are reserved for the server. _group (-> (crud/retrieve-by-id-as-admin id) (throw-cannot-manage request "invite") (throw-is-already-in-group user-id)) + inviter-email (-> request auth/current-user-id auth-password/user-id->email) invited-by (auth-password/invited-by request) email (if-let [email-address (some-> user-id auth-password/user-id->email)] email-address @@ -239,7 +240,8 @@ that start with 'nuvla-' are reserved for the server. :data (cond-> {:email email} user-id (assoc :user-id user-id) redirect-url (assoc :redirect-url redirect-url) - set-password-url (assoc :set-password-url set-password-url)) + set-password-url (assoc :set-password-url set-password-url) + inviter-email (assoc :inviter-email inviter-email)) :expires (u/ttl->timestamp 2592000)) ;; expire after one month invite-url (if (and (nil? user-id) set-password-url) (str set-password-url "?callback=" (gen-util/encode-uri-component callback-url) diff --git a/code/test/com/sixsq/nuvla/server/resources/email/utils_test.clj b/code/test/com/sixsq/nuvla/server/resources/email/utils_test.clj new file mode 100644 index 000000000..8e5b78592 --- /dev/null +++ b/code/test/com/sixsq/nuvla/server/resources/email/utils_test.clj @@ -0,0 +1,19 @@ +(ns com.sixsq.nuvla.server.resources.email.utils-test + (:require + [clojure.test :refer [deftest is]] + [com.sixsq.nuvla.server.resources.email.sending :as sending] + [com.sixsq.nuvla.server.resources.email.utils :as t])) + +(deftest send-msg + (let [result (atom nil)] + (with-redefs [sending/dispatch (fn [_nuvla-config email-data] (reset! result email-data))] + (t/send-email "some-address" (fn [_ {:keys [foo]}] (str "subject " foo)) (fn [_ {:keys [foo]}] (str "body " foo)) {:foo "bar"}) + (is (= {:body "body bar" + :from "administrator" + :subject "subject bar" + :to ["some-address"]} @result))))) + +(deftest send-group-invitation-accepted + (is (= {:content "Your invitation to group group/x has been accepted by invited-email@example.com." + :type "text/plain"} + (second (t/group-invitation-email-body {} {:group "group/x" :invited-email "invited-email@example.com"}))))) diff --git a/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj b/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj index 8ea09ad95..5f350131d 100644 --- a/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj +++ b/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj @@ -28,7 +28,6 @@ (deftest lifecycle - (let [app (ltu/ring-app) session-json (content-type (session app) "application/json") session-admin (header session-json authn-info-header "user/jane group/nuvla-admin group/nuvla-user group/nuvla-anon group/nuvla-admin") @@ -92,7 +91,8 @@ (ltu/is-status 403)) ;; test lifecycle of new group - (with-redefs [auth-password/invited-by (fn [_] "jane") + (with-redefs [auth-password/user-id->email (fn [_] "jane@example.com") + auth-password/invited-by (fn [_] "jane") postal/send-message (fn [_ _] {:code 0, :error :SUCCESS, :message "OK"})] (doseq [session [session-user session-admin]] (doseq [tpl [valid-create valid-create-no-href]] From 75662022c213c19834cd52742db2a190e084ff34 Mon Sep 17 00:00:00 2001 From: khaled basbous Date: Wed, 30 Jul 2025 12:14:46 +0300 Subject: [PATCH 06/12] feat(group): get pending invitations feat(group): revoke invitation --- .../sixsq/nuvla/server/resources/group.clj | 115 +++++++++++++----- .../server/resources/group_lifecycle_test.clj | 57 +++++++-- 2 files changed, 131 insertions(+), 41 deletions(-) diff --git a/code/src/com/sixsq/nuvla/server/resources/group.clj b/code/src/com/sixsq/nuvla/server/resources/group.clj index 1e131af80..13db02363 100644 --- a/code/src/com/sixsq/nuvla/server/resources/group.clj +++ b/code/src/com/sixsq/nuvla/server/resources/group.clj @@ -190,14 +190,19 @@ that start with 'nuvla-' are reserved for the server. (defmethod crud/set-operations resource-type [{:keys [id] :as resource} request] - (let [invite-op (u/action-map id :invite) - add-subgroup-op (u/action-map id :add-subgroup) - can-manage? (a/can-manage? resource request) - can-edit-data? (a/can-edit-data? resource request) - can-manage-edit? (and can-manage? can-edit-data?)] + (let [invite-op (u/action-map id :invite) + add-subgroup-op (u/action-map id :add-subgroup) + get-pending-invitations-op (u/action-map id :get-pending-invitations) + revoke-invitation-op (u/action-map id :revoke-invitation) + can-manage? (a/can-manage? resource request) + can-edit-data? (a/can-edit-data? resource request) + can-manage-edit? (and can-manage? can-edit-data?)] (cond-> (crud/set-standard-operations resource request) - can-manage-edit? (update :operations conj invite-op) - can-manage-edit? (update :operations conj add-subgroup-op)))) + can-manage-edit? (update :operations conj + invite-op + add-subgroup-op + get-pending-invitations-op + revoke-invitation-op)))) (defn throw-is-already-in-group @@ -223,31 +228,31 @@ that start with 'nuvla-' are reserved for the server. {uuid :uuid} :params :as request} (auth/request-as-user original-request)] (try (config-nuvla/throw-is-not-authorised-redirect-url redirect-url) - (let [id (str resource-type "/" uuid) - user-id (auth-password/identifier->user-id username) - _group (-> (crud/retrieve-by-id-as-admin id) - (throw-cannot-manage request "invite") - (throw-is-already-in-group user-id)) + (let [id (str resource-type "/" uuid) + user-id (auth-password/identifier->user-id username) + _group (-> (crud/retrieve-by-id-as-admin id) + (throw-cannot-manage request "invite") + (throw-is-already-in-group user-id)) inviter-email (-> request auth/current-user-id auth-password/user-id->email) - invited-by (auth-password/invited-by request) - email (if-let [email-address (some-> user-id auth-password/user-id->email)] - email-address - (if (s/valid? ::spec-core/email username) - username - (throw (r/ex-response (str "invalid email '" username "'") 400)))) - callback-url (callback-join-group/create-callback - base-uri id - :data (cond-> {:email email} - user-id (assoc :user-id user-id) - redirect-url (assoc :redirect-url redirect-url) - set-password-url (assoc :set-password-url set-password-url) - inviter-email (assoc :inviter-email inviter-email)) - :expires (u/ttl->timestamp 2592000)) ;; expire after one month - invite-url (if (and (nil? user-id) set-password-url) - (str set-password-url "?callback=" (gen-util/encode-uri-component callback-url) - "&type=" (gen-util/encode-uri-component "invitation") - "&username=" (gen-util/encode-uri-component email)) - callback-url)] + invited-by (auth-password/invited-by request) + email (if-let [email-address (some-> user-id auth-password/user-id->email)] + email-address + (if (s/valid? ::spec-core/email username) + username + (throw (r/ex-response (str "invalid email '" username "'") 400)))) + callback-url (callback-join-group/create-callback + base-uri id + :data (cond-> {:email email} + user-id (assoc :user-id user-id) + redirect-url (assoc :redirect-url redirect-url) + set-password-url (assoc :set-password-url set-password-url) + inviter-email (assoc :inviter-email inviter-email)) + :expires (u/ttl->timestamp 2592000)) ;; expire after one month + invite-url (if (and (nil? user-id) set-password-url) + (str set-password-url "?callback=" (gen-util/encode-uri-component callback-url) + "&type=" (gen-util/encode-uri-component "invitation") + "&username=" (gen-util/encode-uri-component email)) + callback-url)] (email-utils/send-join-group-email id invited-by invite-url email) (r/map-response (format "successfully invited to %s" id) 200 id)) (catch Exception e @@ -276,6 +281,54 @@ that start with 'nuvla-' are reserved for the server. (catch Exception e (or (ex-data e) (throw e)))))) +(defn get-pending-invitations + [group-id] + (let [filter-req (str/join " and " ["action='join-group'" + "state='WAITING'" + "expires>'now'" + (str "target-resource/href='" group-id "'")]) + options {:cimi-params {:filter (parser/parse-cimi-filter filter-req) + :select ["id", "data", "expires"] + :orderby [["expires" :desc]] + :last 10000}}] + + (->> options + (crud/query-as-admin "callback") + second))) + +(defmethod crud/do-action [resource-type "get-pending-invitations"] + [original-request] + (let [{{:keys [uuid]} :params :as request} (auth/request-as-user original-request)] + (try + (let [id (str resource-type "/" uuid) + _group (-> (crud/retrieve-by-id-as-admin id) + (throw-cannot-manage request "get-pending-invitations"))] + (->> (get-pending-invitations id) + (map (fn [{:keys [expires] {:keys [email inviter-email]} :data :as _callback}] + {:expires expires :invited-email email :inviter-email inviter-email})) + r/json-response)) + (catch Exception e + (or (ex-data e) (throw e)))))) + +(defmethod crud/do-action [resource-type "revoke-invitation"] + [original-request] + (let [{{invited-email :email} :body + {:keys [uuid]} :params :as request} (auth/request-as-user original-request)] + (try + (let [id (str resource-type "/" uuid) + _group (-> (crud/retrieve-by-id-as-admin id) + (throw-cannot-manage request "revoke-invitation")) + invitations (get-pending-invitations id) + existing-invitation (some (fn [{{:keys [email]} :data :as callback}] (when (= email invited-email) callback)) invitations)] + (if existing-invitation + (do + (crud/delete {:params (u/id->request-params (:id existing-invitation)) + :nuvla/authn auth/internal-identity}) + (r/map-response (str "Invitation revoked for " invited-email) 200)) + (throw (r/ex-response (str "Invitation not found for !" invited-email) 400)))) + (catch Exception e + (or (ex-data e) (throw e)))))) + ;; ;; collection diff --git a/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj b/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj index 5f350131d..74c61b524 100644 --- a/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj +++ b/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj @@ -92,8 +92,8 @@ ;; test lifecycle of new group (with-redefs [auth-password/user-id->email (fn [_] "jane@example.com") - auth-password/invited-by (fn [_] "jane") - postal/send-message (fn [_ _] {:code 0, :error :SUCCESS, :message "OK"})] + auth-password/invited-by (fn [_] "jane") + postal/send-message (fn [_ _] {:code 0, :error :SUCCESS, :message "OK"})] (doseq [session [session-user session-admin]] (doseq [tpl [valid-create valid-create-no-href]] (let [abs-uri (-> session @@ -128,14 +128,20 @@ (ltu/body->edn) (ltu/is-status 200)) - (let [response (-> session - (request abs-uri) - (ltu/body->edn)) + (let [response (-> session + (request abs-uri) + (ltu/body->edn)) {updated-users :users acl :acl} (ltu/body response) - invite-url (-> response - (ltu/is-operation-present :invite) - (ltu/get-op-url :invite))] + invite-url (-> response + (ltu/is-operation-present :invite) + (ltu/get-op-url :invite)) + pending-invitations-url (-> response + (ltu/is-operation-present :get-pending-invitations) + (ltu/get-op-url :get-pending-invitations)) + revoke-invitation-url (-> response + (ltu/is-operation-present :revoke-invitation) + (ltu/get-op-url :revoke-invitation))] (-> session (request invite-url @@ -153,18 +159,49 @@ (ltu/is-status 400) (ltu/message-matches "user already in group")) + (testing "no pending invitations" + (is (-> session + (request pending-invitations-url) + (ltu/body->edn) + (ltu/is-status 200) + ltu/body + count + zero?))) + (-> session (request invite-url - :request :put :body (j/write-value-as-string {:username "max@example.com"})) (ltu/body->edn) (ltu/is-status 200) (ltu/message-matches (str "successfully invited to " id))) + (testing "should be one pending invitations" + (is (-> session + (request pending-invitations-url) + (ltu/body->edn) + (ltu/is-status 200) + ltu/body + count + (= 1)))) + (-> session + (request revoke-invitation-url + :body (j/write-value-as-string {:email "max@example.com"})) + (ltu/body->edn) + (ltu/is-status 200)) + + + (testing "should be zero pending invitations" + (is (-> session + (request pending-invitations-url) + (ltu/body->edn) + (ltu/is-status 200) + ltu/body + count + zero?))) + (binding [config-nuvla/*authorized-redirect-urls* ["https://nuvla.io"]] (-> session (request invite-url - :request :put :body (j/write-value-as-string {:username "jane@example.com" :redirect-url "https://phishing.com"})) (ltu/body->edn) From 57689dc3493e9235491b28b500913cd6942e54a8 Mon Sep 17 00:00:00 2001 From: khaled basbous Date: Mon, 18 Aug 2025 15:33:51 +0200 Subject: [PATCH 07/12] feat(callback): Protect callback url from email scanners --- .../com/sixsq/nuvla/server/resources/callback/utils.clj | 9 ++++++++- code/src/com/sixsq/nuvla/server/resources/email.clj | 2 ++ code/src/com/sixsq/nuvla/server/resources/group.clj | 3 ++- .../sixsq/nuvla/server/resources/user_email_password.clj | 2 ++ .../sixsq/nuvla/server/resources/callback/utils_test.clj | 5 +++++ 5 files changed, 19 insertions(+), 2 deletions(-) diff --git a/code/src/com/sixsq/nuvla/server/resources/callback/utils.clj b/code/src/com/sixsq/nuvla/server/resources/callback/utils.clj index 762402a4e..9130a8b3d 100644 --- a/code/src/com/sixsq/nuvla/server/resources/callback/utils.clj +++ b/code/src/com/sixsq/nuvla/server/resources/callback/utils.clj @@ -1,8 +1,10 @@ (ns com.sixsq.nuvla.server.resources.callback.utils (:require + [clojure.string :as str] [com.sixsq.nuvla.db.impl :as db] [com.sixsq.nuvla.server.resources.common.crud :as crud] - [com.sixsq.nuvla.server.resources.common.utils :as u])) + [com.sixsq.nuvla.server.resources.common.utils :as u] + [com.sixsq.nuvla.server.util.general :as gen-util])) (defn executable? @@ -37,3 +39,8 @@ db/edit) (catch Exception e (or (ex-data e) (throw e))))) + +(defn callback-ui-url + [callback-url] + (let [base-uri (first (str/split callback-url #"api/callback"))] + (str base-uri "ui/callback?callback-url=" (gen-util/encode-uri-component callback-url)))) diff --git a/code/src/com/sixsq/nuvla/server/resources/email.clj b/code/src/com/sixsq/nuvla/server/resources/email.clj index f528141d2..d20e23a3f 100644 --- a/code/src/com/sixsq/nuvla/server/resources/email.clj +++ b/code/src/com/sixsq/nuvla/server/resources/email.clj @@ -15,6 +15,7 @@ address. When the callback is triggered, the `validated` flag is set to true. [com.sixsq.nuvla.server.resources.common.std-crud :as std-crud] [com.sixsq.nuvla.server.resources.common.utils :as u] [com.sixsq.nuvla.server.resources.email.utils :as email-utils] + [com.sixsq.nuvla.server.resources.callback.utils :as callback-utils] [com.sixsq.nuvla.server.resources.resource-metadata :as md] [com.sixsq.nuvla.server.resources.spec.email :as email] [com.sixsq.nuvla.server.util.metadata :as gen-md] @@ -131,6 +132,7 @@ address. When the callback is triggered, the `validated` flag is set to true. (if-not validated (try (-> (email-utils/create-callback id base-uri) + callback-utils/callback-ui-url (email-utils/send-validation-email address)) (r/map-response "check your mailbox for a validation message" 202) (catch Exception e diff --git a/code/src/com/sixsq/nuvla/server/resources/group.clj b/code/src/com/sixsq/nuvla/server/resources/group.clj index 44f02e025..31cd6ff5c 100644 --- a/code/src/com/sixsq/nuvla/server/resources/group.clj +++ b/code/src/com/sixsq/nuvla/server/resources/group.clj @@ -13,6 +13,7 @@ that start with 'nuvla-' are reserved for the server. [com.sixsq.nuvla.db.filter.parser :as parser] [com.sixsq.nuvla.db.impl :as db] [com.sixsq.nuvla.server.resources.callback-join-group :as callback-join-group] + [com.sixsq.nuvla.server.resources.callback.utils :as callback-utils] [com.sixsq.nuvla.server.resources.common.crud :as crud] [com.sixsq.nuvla.server.resources.common.std-crud :as std-crud] [com.sixsq.nuvla.server.resources.common.utils :as u] @@ -246,7 +247,7 @@ that start with 'nuvla-' are reserved for the server. (str set-password-url "?callback=" (gen-util/encode-uri-component callback-url) "&type=" (gen-util/encode-uri-component "invitation") "&username=" (gen-util/encode-uri-component email)) - callback-url)] + (callback-utils/callback-ui-url callback-url))] (email-utils/send-join-group-email id invited-by invite-url email) (r/map-response (format "successfully invited to %s" id) 200 id)) (catch Exception e diff --git a/code/src/com/sixsq/nuvla/server/resources/user_email_password.clj b/code/src/com/sixsq/nuvla/server/resources/user_email_password.clj index 3f9a837e2..db71f930f 100644 --- a/code/src/com/sixsq/nuvla/server/resources/user_email_password.clj +++ b/code/src/com/sixsq/nuvla/server/resources/user_email_password.clj @@ -6,6 +6,7 @@ address and password. (:require [com.sixsq.nuvla.server.resources.callback :as callback] [com.sixsq.nuvla.server.resources.callback-user-email-validation :as user-email-callback] + [com.sixsq.nuvla.server.resources.callback.utils :as callback-utils] [com.sixsq.nuvla.server.resources.common.utils :as u] [com.sixsq.nuvla.server.resources.email.utils :as email-utils] [com.sixsq.nuvla.server.resources.spec.user-template-email-password :as spec-email-password] @@ -61,6 +62,7 @@ address and password. :password password :customer customer) (-> (create-user-email-callback base-uri id :data callback-data) + callback-utils/callback-ui-url (email-utils/send-validation-email email))) (catch Exception e (user-utils/delete-user id) diff --git a/code/test/com/sixsq/nuvla/server/resources/callback/utils_test.clj b/code/test/com/sixsq/nuvla/server/resources/callback/utils_test.clj index bd9fd3d96..81dc5e0b2 100644 --- a/code/test/com/sixsq/nuvla/server/resources/callback/utils_test.clj +++ b/code/test/com/sixsq/nuvla/server/resources/callback/utils_test.clj @@ -19,3 +19,8 @@ false {:state "SUCCEEDED", :expires past} false {:state "SUCCEEDED"} false {}))) + +(deftest callback-ui-url + (are [expected arg] (= expected (t/callback-ui-url arg)) + "https://nuid.localhost/ui/callback?callback-url=https%3A%2F%2Fnuid.localhost%2Fapi%2Fcallback%2Fa1f08e95-7caf-4f84-8b62-dfa621eaad34%2Fexecute" "https://nuid.localhost/api/callback/a1f08e95-7caf-4f84-8b62-dfa621eaad34/execute" + "https://nuvla.io/ui/callback?callback-url=https%3A%2F%2Fnuvla.io%2Fapi%2Fcallback%2Fa1f08e95-7caf-4f84-8b62-dfa621eaad34%2Fexecute" "https://nuvla.io/api/callback/a1f08e95-7caf-4f84-8b62-dfa621eaad34/execute")) From 9e1a7467e585f6c806b8fe25dae2842054aeb07c Mon Sep 17 00:00:00 2001 From: khaled basbous Date: Tue, 19 Aug 2025 09:49:12 +0200 Subject: [PATCH 08/12] fix tests --- .../server/resources/email_lifecycle_test.clj | 8 ++-- .../hook_reset_password_lifecycle_test.clj | 7 +-- .../server/resources/lifecycle_test_utils.clj | 8 ++++ .../server/resources/session/utils_test.clj | 13 +----- .../session_password_lifecycle_test.clj | 7 +-- ...user_email_password_2fa_lifecycle_test.clj | 43 ++++++++----------- .../user_email_password_lifecycle_test.clj | 8 +--- 7 files changed, 36 insertions(+), 58 deletions(-) diff --git a/code/test/com/sixsq/nuvla/server/resources/email_lifecycle_test.clj b/code/test/com/sixsq/nuvla/server/resources/email_lifecycle_test.clj index 1b5582865..f503bd622 100644 --- a/code/test/com/sixsq/nuvla/server/resources/email_lifecycle_test.clj +++ b/code/test/com/sixsq/nuvla/server/resources/email_lifecycle_test.clj @@ -8,6 +8,7 @@ [com.sixsq.nuvla.server.resources.email.sending :as email-sending] [com.sixsq.nuvla.server.resources.lifecycle-test-utils :as ltu] [com.sixsq.nuvla.server.resources.resource-metadata :as md] + [com.sixsq.nuvla.server.util.general :as gen-util] [com.sixsq.nuvla.server.util.metadata-test-utils :as mdtu] [jsonista.core :as j] [peridot.core :refer [content-type header request session]] @@ -140,11 +141,8 @@ :pass "password"}) ;; WARNING: This is a fragile! Regex matching to recover callback URL. - postal/send-message (fn [_ {:keys [body]}] - (let [url (->> body second :content - (re-matches #"(?s).*visit:\n\n\s+(.*?)\n.*") - second)] - (reset! validation-link url)) + postal/send-message (fn [_ msg] + (reset! validation-link (ltu/extract-msg-callback-url msg)) {:code 0, :error :SUCCESS, :message "OK"})] (-> session-anon diff --git a/code/test/com/sixsq/nuvla/server/resources/hook_reset_password_lifecycle_test.clj b/code/test/com/sixsq/nuvla/server/resources/hook_reset_password_lifecycle_test.clj index 1be2f07fa..d8b48a196 100644 --- a/code/test/com/sixsq/nuvla/server/resources/hook_reset_password_lifecycle_test.clj +++ b/code/test/com/sixsq/nuvla/server/resources/hook_reset_password_lifecycle_test.clj @@ -8,12 +8,10 @@ [com.sixsq.nuvla.server.resources.hook-reset-password :as hrp] [com.sixsq.nuvla.server.resources.lifecycle-test-utils :as ltu] [com.sixsq.nuvla.server.resources.session-password-lifecycle-test :as password-test] - [com.sixsq.nuvla.server.resources.session-template :as st] [com.sixsq.nuvla.server.util.general :as gen-util] [jsonista.core :as j] [peridot.core :refer [content-type header request session]] - [postal.core :as postal] - [ring.util.codec :as codec])) + [postal.core :as postal])) (use-fixtures :once ltu/with-test-server-fixture) @@ -21,9 +19,6 @@ (def base-uri (str p/service-context t/resource-type "/" hrp/action)) - -(def session-template-base-uri (str p/service-context st/resource-type)) - (deftest lifecycle (let [reset-link (atom nil) diff --git a/code/test/com/sixsq/nuvla/server/resources/lifecycle_test_utils.clj b/code/test/com/sixsq/nuvla/server/resources/lifecycle_test_utils.clj index d7c4aa0f7..db4fd50cc 100644 --- a/code/test/com/sixsq/nuvla/server/resources/lifecycle_test_utils.clj +++ b/code/test/com/sixsq/nuvla/server/resources/lifecycle_test_utils.clj @@ -20,6 +20,7 @@ [com.sixsq.nuvla.server.middleware.logger :refer [wrap-logger]] [com.sixsq.nuvla.server.resources.common.dynamic-load :as dyn] [com.sixsq.nuvla.server.resources.event.utils :as event-utils] + [com.sixsq.nuvla.server.util.general :as gen-util] [com.sixsq.nuvla.server.util.kafka :as ka] [com.sixsq.nuvla.server.util.zookeeper :as uzk] [compojure.core :as cc] @@ -628,3 +629,10 @@ (is-event expected-event# actual-event#)) ~expected-events events#)))) + +(defn extract-msg-callback-url + [{:keys [body] :as _msg}] + (->> body second :content + (re-matches #"(?s).*visit:\n\n\s+.*callback-url=(.*?)\n.*") + second + gen-util/decode-uri-component)) diff --git a/code/test/com/sixsq/nuvla/server/resources/session/utils_test.clj b/code/test/com/sixsq/nuvla/server/resources/session/utils_test.clj index f0e87278c..64fe17385 100644 --- a/code/test/com/sixsq/nuvla/server/resources/session/utils_test.clj +++ b/code/test/com/sixsq/nuvla/server/resources/session/utils_test.clj @@ -15,16 +15,10 @@ [peridot.core :refer [content-type header request session]] [postal.core :as postal])) - (use-fixtures :once ltu/with-test-server-fixture) - (def base-uri (str p/service-context session/resource-type)) - -(def session-template-base-uri (str p/service-context st/resource-type)) - - (defn create-user [session-admin & {:keys [username password email activated?]}] (let [validation-link (atom nil) @@ -41,11 +35,8 @@ :user "admin" :pass "password"}) - ;; WARNING: This is a fragile! Regex matching to recover callback URL. - postal/send-message (fn [_ {:keys [body]}] - (let [url (->> body second :content - (re-matches #"(?s).*visit:\n\n\s+(.*?)\n.*") - second)] + postal/send-message (fn [_ msg] + (let [url (ltu/extract-msg-callback-url msg)] (reset! validation-link url)) {:code 0, :error :SUCCESS, :message "OK"})] diff --git a/code/test/com/sixsq/nuvla/server/resources/session_password_lifecycle_test.clj b/code/test/com/sixsq/nuvla/server/resources/session_password_lifecycle_test.clj index dda6ef283..489c5fbfd 100644 --- a/code/test/com/sixsq/nuvla/server/resources/session_password_lifecycle_test.clj +++ b/code/test/com/sixsq/nuvla/server/resources/session_password_lifecycle_test.clj @@ -48,11 +48,8 @@ :pass "password"}) ;; WARNING: This is a fragile! Regex matching to recover callback URL. - postal/send-message (fn [_ {:keys [body]}] - (let [url (->> body second :content - (re-matches #"(?s).*visit:\n\n\s+(.*?)\n.*") - second)] - (reset! validation-link url)) + postal/send-message (fn [_ msg] + (reset! validation-link (ltu/extract-msg-callback-url msg)) {:code 0, :error :SUCCESS, :message "OK"})] (let [user-id (-> session-admin diff --git a/code/test/com/sixsq/nuvla/server/resources/user_email_password_2fa_lifecycle_test.clj b/code/test/com/sixsq/nuvla/server/resources/user_email_password_2fa_lifecycle_test.clj index 389ebe85d..6efe9fe41 100644 --- a/code/test/com/sixsq/nuvla/server/resources/user_email_password_2fa_lifecycle_test.clj +++ b/code/test/com/sixsq/nuvla/server/resources/user_email_password_2fa_lifecycle_test.clj @@ -31,7 +31,7 @@ (deftest lifecycle-email - (let [email-body (atom nil) + (let [email-msg (atom nil) session (-> (ltu/ring-app) session (content-type "application/json")) @@ -46,8 +46,8 @@ :pass "password"}) ;; WARNING: This is a fragile Regex matching to recover callback URL. - postal/send-message (fn [_ {:keys [body]}] - (reset! email-body body) + postal/send-message (fn [_ msg] + (reset! email-msg msg) {:code 0, :error :SUCCESS, :message "OK"})] (let [href (str user-tpl/resource-type "/" email-password/registration-method) @@ -78,10 +78,9 @@ (ltu/is-operation-present :enable-2fa) (ltu/is-operation-absent :disable-2fa) (ltu/get-op-url :enable-2fa)) - - validation-link (->> @email-body second :content - (re-matches #"(?s).*visit:\n\n\s+(.*?)\n.*") - second) + validation-link (ltu/extract-msg-callback-url @email-msg) + get-user-token #(->> @email-msg :body second :content + (re-find #"\d+")) session-base-url (str p/service-context session/resource-type) valid-session-create {:template {:href (str st/resource-type "/password") @@ -152,8 +151,7 @@ (re-matches #"http.*(\/api.*)\/execute") second) callback-exec-url (str callback-url "/execute") - user-token (->> @email-body second :content - (re-find #"\d+"))] + user-token (get-user-token)] (-> session-admin (request callback-url) @@ -222,8 +220,7 @@ (re-matches #"http.*(\/api.*)\/execute") second) callback-exec-url (str callback-url "/execute") - user-token (->> @email-body second :content - (re-find #"\d+"))] + user-token (get-user-token)] (-> session-admin (request callback-url) @@ -295,8 +292,7 @@ (re-matches #"http.*(\/api.*)\/execute") second) callback-exec-url (str callback-url "/execute") - user-token (->> @email-body second :content - (re-find #"\d+"))] + user-token (get-user-token)] (-> session-admin (request callback-url) @@ -364,8 +360,7 @@ (re-matches #"http.*(\/api.*)\/execute") second) callback-exec-url (str callback-url "/execute") - user-token (->> @email-body second :content - (re-find #"\d+"))] + user-token (get-user-token)] (-> session-admin (request callback-url) @@ -424,7 +419,7 @@ (deftest lifecycle-totp - (let [email-body (atom nil) + (let [email-msg (atom nil) session (-> (ltu/ring-app) session (content-type "application/json")) @@ -439,8 +434,8 @@ :pass "password"}) ;; WARNING: This is a fragile Regex matching to recover callback URL. - postal/send-message (fn [_ {:keys [body]}] - (reset! email-body body) + postal/send-message (fn [_ msg] + (reset! email-msg msg) {:code 0, :error :SUCCESS, :message "OK"})] (let [href (str user-tpl/resource-type "/" email-password/registration-method) @@ -473,9 +468,7 @@ (ltu/is-operation-absent :disable-2fa) (ltu/get-op-url :enable-2fa)) - validation-link (->> @email-body second :content - (re-matches #"(?s).*visit:\n\n\s+(.*?)\n.*") - second) + validation-link (ltu/extract-msg-callback-url @email-msg) session-base-url (str p/service-context session/resource-type) valid-session-create {:template {:href (str st/resource-type "/password") @@ -782,10 +775,10 @@ (testing "credential totp should be delete at deactivation" (let [cred-totp (-> session-created-user - (request user-url) - (ltu/body->edn) - (ltu/is-status 200) - :credential-totp)] + (request user-url) + (ltu/body->edn) + (ltu/is-status 200) + :credential-totp)] (-> session-admin (request (str p/service-context cred-totp)) (ltu/body->edn) diff --git a/code/test/com/sixsq/nuvla/server/resources/user_email_password_lifecycle_test.clj b/code/test/com/sixsq/nuvla/server/resources/user_email_password_lifecycle_test.clj index 1c8aa30a5..4daf99523 100644 --- a/code/test/com/sixsq/nuvla/server/resources/user_email_password_lifecycle_test.clj +++ b/code/test/com/sixsq/nuvla/server/resources/user_email_password_lifecycle_test.clj @@ -46,12 +46,8 @@ :user "admin" :pass "password"}) - ;; WARNING: This is a fragile! Regex matching to recover callback URL. - postal/send-message (fn [_ {:keys [body]}] - (let [url (->> body second :content - (re-matches #"(?s).*visit:\n\n\s+(.*?)\n.*") - second)] - (reset! validation-link url)) + postal/send-message (fn [_ msg] + (reset! validation-link (ltu/extract-msg-callback-url msg)) {:code 0, :error :SUCCESS, :message "OK"})] (let [template (-> session-admin From b82116f553080eb7c2b36928d2277e16b7234dbd Mon Sep 17 00:00:00 2001 From: khaled basbous Date: Tue, 19 Aug 2025 15:49:35 +0200 Subject: [PATCH 09/12] fix(Group): Admin can update subgroup parents attribute --- .../com/sixsq/nuvla/server/resources/group.clj | 5 +++-- .../server/resources/group_lifecycle_test.clj | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/code/src/com/sixsq/nuvla/server/resources/group.clj b/code/src/com/sixsq/nuvla/server/resources/group.clj index 13db02363..5f32347d3 100644 --- a/code/src/com/sixsq/nuvla/server/resources/group.clj +++ b/code/src/com/sixsq/nuvla/server/resources/group.clj @@ -151,8 +151,9 @@ that start with 'nuvla-' are reserved for the server. (-> request (assoc-in [:body :acl] acl) (update-in [:body :acl :view-meta] (comp vec set concat) (conj users id)) - (update :body dissoc :parents) - (update-in [:cimi-params :select] disj "parents") + (cond-> (not (a/is-admin-request? request)) + (-> (update :body dissoc :parents) + (update-in [:cimi-params :select] disj "parents"))) (edit-impl)))) diff --git a/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj b/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj index 74c61b524..ad62c3c3b 100644 --- a/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj +++ b/code/test/com/sixsq/nuvla/server/resources/group_lifecycle_test.clj @@ -284,20 +284,34 @@ (ltu/is-key-value :parents ["group/a" "group/b"]) (ltu/is-operation-present :add-subgroup)) - (testing "parents field cannot be updated" - (-> session-admin + (testing "parents field can be updated only by admin" + (-> session-user (request abs-uri-grp-c :request-method :put :body (j/write-value-as-string {:parents ["change-not-allowed"]})) (ltu/body->edn) (ltu/is-status 200) (ltu/is-key-value :parents ["group/a" "group/b"])) + (-> session-user + (request (str abs-uri-grp-c "?select=parents") + :request-method :put + :body (j/write-value-as-string {})) + (ltu/body->edn) + (ltu/is-status 200) + (ltu/is-key-value :parents ["group/a" "group/b"])) (-> session-admin (request (str abs-uri-grp-c "?select=parents") :request-method :put :body (j/write-value-as-string {})) (ltu/body->edn) (ltu/is-status 200) + (ltu/is-key-value :parents nil)) + (-> session-admin + (request abs-uri-grp-c + :request-method :put + :body (j/write-value-as-string {:parents ["group/a" "group/b"]})) + (ltu/body->edn) + (ltu/is-status 200) (ltu/is-key-value :parents ["group/a" "group/b"]))))))))) (testing "A user should not be able to create the 19th group of a group" From a8bcd42f48c2d21febfe2221487cdd5541c19be7 Mon Sep 17 00:00:00 2001 From: Konstantin Skaburskas Date: Tue, 19 Aug 2025 16:32:23 +0200 Subject: [PATCH 10/12] use latest sonar-scanner version 7.2.0.5079 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9bbb5a452..8ef31f37e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,7 +63,7 @@ jobs: - name: Setup Sonar Scanner uses: warchant/setup-sonar-scanner@v7 with: - version: 4.6.2.2472 + version: 7.2.0.5079 - name: Run Sonar Scanner env: @@ -182,4 +182,4 @@ jobs: push: true tags: > ${{ inputs.DOCKER_REPO }}/api:${{ inputs.DOCKER_TAG }}, - ${{ inputs.DOCKER_REPO }}/api:latest \ No newline at end of file + ${{ inputs.DOCKER_REPO }}/api:latest From 0447dbaa3b3073bcd1efdece4ddad6bf4d17a674 Mon Sep 17 00:00:00 2001 From: Konstantin Skaburskas Date: Tue, 19 Aug 2025 17:00:43 +0200 Subject: [PATCH 11/12] sonar-scanner 5.0.2.4997 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ef31f37e..e55182335 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,7 +63,7 @@ jobs: - name: Setup Sonar Scanner uses: warchant/setup-sonar-scanner@v7 with: - version: 7.2.0.5079 + version: 5.0.2.4997 - name: Run Sonar Scanner env: From 4f7452f42d412611959395be2bdaf69fdda5d677 Mon Sep 17 00:00:00 2001 From: Konstantin Skaburskas Date: Wed, 20 Aug 2025 09:56:59 +0200 Subject: [PATCH 12/12] new sonar clj lang patterns --- code/sonar-project.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/code/sonar-project.properties b/code/sonar-project.properties index 326a7d1f0..f3d2a9087 100644 --- a/code/sonar-project.properties +++ b/code/sonar-project.properties @@ -3,6 +3,8 @@ sonar.projectKey=nuvla-api-server sonar.projectName=nuvla-api-server sonar.sources=src,project.clj sonar.inclusions=**/*.clj, **/*.cljc +sonar.clojure.file.suffixes=.clj,.cljc +sonar.lang.patterns.clj=**/*.clj,**/*.cljc sonar.tests=test sonar.testExecutionReportPaths=test-reports/sonar/testExecutions.xml sonar.clojure.sensors.timeout=3600