Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial implementation of bona-fide-bot #2456

Merged
merged 11 commits into from
Dec 1, 2020
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Changes since v2.13
- Deciders and reviewers can now be invited via email. (#2040)
- New `invite-decider` and `invite-reviewer` commands in the API & UI
- Commands are available to the handler on submitted applications. See [permission table](docs/application-permissions.md).
- Experimental bona fide bot for granting peer-verified ResearcherStatus visas. See [docs/bots.md](docs/bots.md).

## v2.13 "Etelätuulentie" 2020-09-17

Expand Down
23 changes: 23 additions & 0 deletions docs/bots.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,26 @@ Example of creating the bot user with the API.
```sh
curl -X POST -H "content-type: application/json" -H "x-rems-api-key: 42" -H "x-rems-user-id: owner" http://localhost:3000/api/users/create --data '{"userid": "rejecter-bot", "name": "Rejecter Bot", "email": null}'
```

## Bona Fide bot

User id: `bona-fide-bot`

The Bona Fide bot is used for granting peer-verified GA4GH
ResearcherStatus visas. See [ga4gh-visas.md](ga4gh-visas.md) for more
background.

The Bona Fide bot is designed to be used with
- a _default workflow_ that has the bot as a handler (and optionally some human handlers)
- a catalogue item that has a form that has an email field (in case of multiple email fields, the _first one_ is used)

When an application gets submitted for the catalogue item, the bot
sends a _decision request_ to the email address it extracts from the
application. Then the bot waits until the recipient of the request
logs in to rems and performs the _decide_ action. At this point:

- If the decider has a ResearcherStatus visa (with `"by": "so"` or
`"by": "system"`, see from their IDP, [ga4gh-visas.md](ga4gh-visas.md)):
- and if the decider posted an approve decision: the bot approves the application
- and if the decider posted a reject decision: the bot rejects the application
- If the decider doesn't have a ResearcherStatus visa, the bot rejects the application
2 changes: 2 additions & 0 deletions docs/ga4gh-visas.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@ is found in the passport, REMS sets the user attribute
If an applicant has `researcher-status-by` with value `"so"` or
`"system"`, REMS shows the handler a "Applicant researcher status"
checkbox in the applicant details.

See also Bona Fide bot in [bots.md](bots.md).
2 changes: 2 additions & 0 deletions src/clj/rems/api/services/command.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
[clojure.tools.logging :as log]
[rems.api.services.blacklist :as blacklist]
[rems.application.approver-bot :as approver-bot]
[rems.application.bona-fide-bot :as bona-fide-bot]
[rems.application.commands :as commands]
[rems.application.rejecter-bot :as rejecter-bot]
[rems.common.application-util :as application-util]
Expand Down Expand Up @@ -43,6 +44,7 @@
(entitlements/update-entitlements-for-events new-events)
(rejecter-bot/run-rejecter-bot new-events)
(approver-bot/run-approver-bot new-events)
(bona-fide-bot/run-bona-fide-bot new-events)
(event-notification/queue-notifications! new-events)
(delete-applications new-events)))

Expand Down
177 changes: 177 additions & 0 deletions src/clj/rems/application/bona_fide_bot.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
(ns rems.application.bona-fide-bot
"A bot that enables workflows where a user can ask another user to vouch
for their bona fide researcher status.

See also: docs/bots.md, docs/ga4gh-visas.md"
(:require [clojure.test :refer [deftest is testing]]
[clj-time.core :as time]
[rems.common.application-util :as application-util]
[rems.db.applications :as applications]
[rems.db.users :as users]
[rems.testing-util :refer [with-fixed-time]]))

(def bot-userid "bona-fide-bot")

(defn- find-email-address [application]
(some (fn [field]
(when (= :email (:field/type field))
(:field/value field)))
(mapcat :form/fields
(:application/forms application))))

(deftest test-find-email-address
(testing "no email field"
(is (nil?
(find-email-address {:application/forms [{:form/id 1
:form/fields [{:field/id 1
:field/type :text
:field/value "xxx"}
{:field/id 2
:field/type :date
:field/value "2020-01-01"}]}]}))))
(testing "unique email field"
(is (= "[email protected]"
(find-email-address {:application/forms [{:form/id 1
:form/fields [{:field/id 1
:field/type :text
:field/value "xxx"}
{:field/id 2
:field/type :email
:field/value "[email protected]"}
{:field/id 3
:field/type :date
:field/value "2020-01-01"}]}]}))))
(testing "multiple email fields"
(is (= "[email protected]"
(find-email-address {:application/forms [{:form/id 1
:form/fields [{:field/id 1
:field/type :text
:field/value "xxx"}
{:field/id 2
:field/type :email
:field/value "[email protected]"}
{:field/id 3
:field/type :date
:field/value "2020-01-01"}
{:field/id 4
:field/type :email
:field/value "[email protected]"}]}]}))))
(testing "multiple forms"
(is (= "[email protected]"
(find-email-address {:application/forms [{:form/id 1
:form/fields [{:field/id 1
:field/type :text
:field/value "xxx"}]}
{:form/id 2
:form/fields [{:field/id 2
:field/type :email
:field/value "[email protected]"}
{:field/id 3
:field/type :date
:field/value "2020-01-01"}]}
{:form/id 3
:form/fields [{:field/id 4
:field/type :email
:field/value "[email protected]"}]}]})))))


(defn- may-give-bona-fide-status? [user-attributes]
(contains? #{"so" "system"} (:researcher-status-by user-attributes)))

(defn- generate-commands [event actor-attributes application]
(when (application-util/is-handler? application bot-userid)
(case (:event/type event)
:application.event/submitted
(let [email (find-email-address application)]
(assert email (pr-str application))
[{:type :application.command/invite-decider
:time (time/now)
:application-id (:application/id event)
:actor bot-userid
:decider {:name "Referer"
:email email}}])
:application.event/decided
[{:type (if (and (may-give-bona-fide-status? actor-attributes)
(= :approved (:application/decision event)))
:application.command/approve
:application.command/reject)
:time (time/now)
:application-id (:application/id event)
:actor bot-userid}]

[])))

(deftest test-generate-commands
(with-fixed-time (time/date-time 2010)
(fn []
(testing "submitted event,"
(let [event {:event/type :application.event/submitted
:event/actor "applicant"
:application/id 1234}
applicant-attributes {:userid "applicant"
:email "[email protected]"
:name "An Applicant"}
forms [{:form/fields [{:field/type :text
:field/value "this is text"}
{:field/type :email
:field/value "[email protected]"}
{:field/type :date
:field/value "2020-01-01"}]}]]
(testing "bot not handler"
(is (empty? (generate-commands event
applicant-attributes
{:application/workflow {:workflow.dynamic/handlers [{:userid "handler"}]}
:application/forms forms}))))
(testing "bot is handler"
(is (= [{:type :application.command/invite-decider
:time (time/date-time 2010)
:application-id 1234
:actor "bona-fide-bot"
:decider {:name "Referer" :email "[email protected]"}}]
(generate-commands event
applicant-attributes
{:application/workflow {:workflow.dynamic/handlers [{:userid "handler"}
{:userid bot-userid}]}
:application/forms forms}))))))
(testing "decided event,"
(let [event {:event/type :application.event/decided
:event/actor "referer"
:application/decision :approved
:application/id 1234}
referer-attributes {:userid "referer"
:email "[email protected]"
:name "Ref Errer"}]
(testing "bot not handler"
(is (empty? (generate-commands event
referer-attributes
{:application/workflow {:workflow.dynamic/handlers [{:userid "handler"}]}}))))
(testing "bot is handler,"
(let [application {:application/workflow {:workflow.dynamic/handlers [{:userid bot-userid}]}}]
(testing "referer does not have researcher status"
(is (= [{:type :application.command/reject
:time (time/date-time 2010)
:application-id 1234
:actor "bona-fide-bot"}]
(generate-commands event referer-attributes application))))
(testing "referer has researcher status,"
(let [referer-attributes (assoc referer-attributes :researcher-status-by "so")]
(testing "referer approves"
(is (= [{:type :application.command/approve
:time (time/date-time 2010)
:application-id 1234
:actor "bona-fide-bot"}]
(generate-commands event referer-attributes application))))
(testing "referer rejects"
(is (= [{:type :application.command/reject
:time (time/date-time 2010)
:application-id 1234
:actor "bona-fide-bot"}]
(generate-commands (assoc event :application/decision :rejected) referer-attributes application)))))))))))))



(defn run-bona-fide-bot [new-events]
(doall (mapcat #(generate-commands %
(users/get-user (:event/actor %))
(applications/get-application (:application/id %)))
new-events)))
29 changes: 29 additions & 0 deletions src/clj/rems/db/test_data.clj
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,34 @@
:auto-approve auto-approve
:organization-owner organization-owner}))

(defn- create-bona-fide-catalogue-item! [users]
(let [owner (:owner users)
bot (:bona-fide-bot users)
res (create-resource! {:resource-ext-id "bona-fide"
:organization {:organization/id "default"}
:actor owner})
form (create-form! {:actor owner
:form/title "Bona Fide form"
:organization {:organization/id "default"}
:form/fields [{:field/type :email
:field/title {:fi "Suosittelijan sähköpostiosoite"
:en "Referer's email address"
:sv "sv"}
:field/optional false}]})
wf (create-workflow! {:actor owner
:organization {:organization/id "default"}
:title "Bona Fide workflow"
:type :workflow/default
:handlers [bot]})]
(create-catalogue-item! {:actor owner
:organization {:organization/id "default"}
:title {:en "Apply for Bona Fide researcher status"
:fi "Hae Bona Fide tutkija -statusta"
:sv "sv"}
:resource-id res
:form-id form
:workflow-id wf})))

(defn- create-disabled-applications! [catid applicant approver]
(create-draft! applicant [catid] "draft with disabled item")

Expand Down Expand Up @@ -937,6 +965,7 @@
:form-id form
:organization {:organization/id "nbn"}
:workflow-id (:auto-approve workflows)})
(create-bona-fide-catalogue-item! (merge users +bot-users+))
(let [thl-res (create-resource! {:resource-ext-id "thl"
:organization {:organization/id "thl"}
:actor owner})
Expand Down
3 changes: 3 additions & 0 deletions src/clj/rems/db/test_data_users.clj
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
(ns rems.db.test-data-users
(:require [clojure.test :refer :all]
[rems.application.approver-bot :as approver-bot]
[rems.application.bona-fide-bot :as bona-fide-bot]
[rems.application.rejecter-bot :as rejecter-bot]
[rems.testing-util :refer [with-user]]))

(def +bot-users+
{:approver-bot approver-bot/bot-userid
:bona-fide-bot bona-fide-bot/bot-userid
:rejecter-bot rejecter-bot/bot-userid})

(def +bot-user-data+
{approver-bot/bot-userid {:eppn approver-bot/bot-userid :commonName "Approver Bot"}
bona-fide-bot/bot-userid {:eppn bona-fide-bot/bot-userid :commonName "Bona Fide Bot"}
rejecter-bot/bot-userid {:eppn rejecter-bot/bot-userid :commonName "Rejecter Bot"}})

(def +fake-users+
Expand Down
Loading