Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ The application follows a layered Clojure web service architecture:
- Transactions via `apps.util.db/transaction`

5. **Client Layer** (`src/apps/clients/`)
- HTTP clients for external microservices (jex, data-info, metadata, permissions, notifications, iplant-groups)
- HTTP clients for external microservices (jex, data-info, metadata, permissions, notifications, groups)

### Multi-System App Client Pattern
- **Protocol-based abstraction** (`apps.protocols/Apps`): Defines all app operations as protocol methods.
Expand Down
1 change: 0 additions & 1 deletion project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@
[org.cyverse/common-cli "2.8.3"]
[org.cyverse/common-cfg "2.8.4"]
[org.cyverse/common-swagger-api "3.4.23"]
[org.cyverse/cyverse-groups-client "0.1.10"]
[org.cyverse/permissions-client "2.8.6"]
[org.cyverse/service-logging "2.8.6"]
[org.flatland/ordered "1.15.12"]
Expand Down
169 changes: 169 additions & 0 deletions src/apps/clients/groups.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
(ns apps.clients.groups
(:require [apps.util.config :as config]
[cemerick.url :as curl]
[clj-http.client :as http]
[clojure.string :as string]
[clojure.tools.logging :as log]
[slingshot.slingshot :refer [try+]]))

(def ^:private de-users-group "de-users")
(def ^:private workshop-users-group "workshop-users")

(defn- groups-url
[& components]
(str (apply curl/url (config/groups-base) components)))

;; clj-http applies no timeout of its own. This client sits on job submission
;; and both job listings, so a groups service that accepts connections without
;; answering would tie up every request thread until apps stops responding too.
(def ^:private timeouts
{:connection-timeout 5000
:socket-timeout 30000})

(defn- as-user
([user] (as-user user {}))
([user query-params]
(merge timeouts
{:query-params (assoc query-params :user user)
:as :json})))

(defn- as-de-grouper
([] (as-de-grouper {}))
([query-params]
(as-user (config/de-grouper-user) query-params)))

(defn user-source? [subject-source-id]
(= subject-source-id (config/grouper-user-source)))

(defn get-subject-type [subject-source-id]
(if (user-source? subject-source-id) "user" "group"))

(defn lookup-subject
"Retrieves user details for a single subject."
[user short-username]
(:body (http/get (groups-url "subjects" short-username) (as-user user))))

(defn lookup-subjects
"Looks up multiple subjects by subject ID, returning a map of ID to subject."
[subjects]
(->> (http/post (groups-url "subjects" "lookup")
(assoc (as-de-grouper)
:form-params {:subject_ids (vec (set subjects))}
:content-type :json))
:body
:subjects
(map (juxt :id identity))
(into {})))

(defn lookup-subject-groups
"Retrieves the groups that a subject belongs to."
[short-username]
(:body (http/get (groups-url "subjects" short-username "groups") (as-de-grouper))))

(defn- lookup-group
"Resolves a group's structured identity to the group itself, or nil if there is
no such group."
[group-type name]
(try+
(:body (http/get (groups-url "groups" "lookup")
(as-de-grouper {:group_type group-type :name name})))
(catch [:status 404] _ nil)))

(defn- get-group-by-id
[group-id]
(try+
(:body (http/get (groups-url "groups" group-id) (as-de-grouper)))
(catch [:status 404] _ nil)))

;; Group IDs are 32 hex digits: Grouper's own format for imported groups, and
;; what `subjects.subject_id` defaults to for ones created since.
(def ^:private group-id-pattern #"^[0-9a-f]{32}$")

(defn- community-short-name
"The short name from a legacy Grouper community path, or nil for anything
else. Tags written before the migration hold the full path; only a path
whose second-to-last segment is `communities` names a community, so taking
the last segment of any other path could capture an unrelated group's name."
[identifier]
(let [segments (string/split identifier #":")]
(when (and (<= 2 (count segments))
(= "communities" (nth segments (- (count segments) 2))))
(last segments))))

(defn lookup-community
"Resolves a community identifier to the community itself, or nil. Accepts a
group ID, a plain name, or a legacy colon-delimited Grouper community path,
so that a browser holding a stale bundle still names something real."
[identifier]
(let [group (if (re-matches group-id-pattern identifier)
(get-group-by-id identifier)
(if-let [short-name (community-short-name identifier)]
(do (log/warn "resolving community identifier" identifier "as legacy Grouper path;"
"the requesting browser is probably running a stale bundle")
(lookup-group "community" short-name))
(lookup-group "community" identifier)))]
(when (= "community" (:group_type group))
group)))

(defn- create-group
[group-type name]
(:body (http/post (groups-url "groups")
(assoc (as-de-grouper)
:form-params {:group_type group-type :name name}
:content-type :json))))

(defn- get-or-create-group
[group-type name]
(or (lookup-group group-type name)
(create-group group-type name)))

;; The de-users group is created by the deployment rather than by apps, so a
;; missing one is a misconfiguration worth failing on rather than papering over.
(def de-users-group-id
(memoize (fn [] (:id (:body (http/get (groups-url "groups" "lookup")
(as-de-grouper {:group_type "system" :name de-users-group})))))))

(defn add-de-user
"Adds a user to the de-users group."
[subject-id]
(http/put (groups-url "groups" (de-users-group-id) "members" subject-id)
(as-de-grouper)))

(defn list-group-members-by-id
"Lists the members of the group with the given ID."
[user group-id]
(:body (http/get (groups-url "groups" group-id "members") (as-user user))))

(defn get-workshop-group
"Retrieves information about the workshop users group, creating it if necessary."
[]
(get-or-create-group "system" workshop-users-group))

(defn get-workshop-group-members
"Retrieves the list of workshop group members, creating the group if necessary."
[]
(list-group-members-by-id (config/de-grouper-user) (:id (get-workshop-group))))

(defn update-workshop-group-members
"Updates the list of workshop group members, creating the group if necessary."
[subject-ids]
(:body (http/put (groups-url "groups" (:id (get-workshop-group)) "members")
(assoc (as-de-grouper)
:form-params {:members subject-ids}
:content-type :json))))

;; Both `admin` and `own` count: the importer maps Grouper's `admins` to
;; `admin`, but a community created natively grants `own` to whoever created it,
;; and that person administers it too.
(def ^:private community-admin-levels #{"admin" "own"})

(defn list-community-admins
"Lists the administrators of the community with the given ID."
[community-id]
(->> (:permissions (:body (http/get (groups-url "groups" community-id "permissions") (as-de-grouper))))
(filter (comp community-admin-levels :level))
(map :subject)
(filter (comp (partial = "user") :subject_type))
(mapv (fn [{:keys [subject_id]}] {:id subject_id}))
(remove (comp (partial = (config/de-grouper-user)) :id))
(hash-map :members)))
153 changes: 0 additions & 153 deletions src/apps/clients/iplant_groups.clj

This file was deleted.

2 changes: 1 addition & 1 deletion src/apps/clients/notifications.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
(ns apps.clients.notifications
(:require [apps.clients.iplant-groups :as groups-client]
(:require [apps.clients.groups :as groups-client]
[apps.clients.notifications.app-sharing :as asn]
[apps.clients.notifications.job-sharing :as jsn]
[apps.clients.notifications.tool-sharing :as tool-notifications]
Expand Down
8 changes: 4 additions & 4 deletions src/apps/clients/notifications/common_sharing.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
(ns apps.clients.notifications.common-sharing
(:require [apps.clients.iplant-groups :as ipg]
(:require [apps.clients.groups :as groups-client]
[clojure-commons.template :refer [render]]))

(def grouping-threshold 10)
Expand Down Expand Up @@ -53,8 +53,8 @@

(defn notifications-for-sharee
[notifications-fn sharer {sharee :id subject-source-id :source_id} responses]
(if (ipg/user-source? subject-source-id)
(if (groups-client/user-source? subject-source-id)
(notifications-fn sharer sharee responses)
(->> (:members (ipg/list-group-members-by-id sharer sharee))
(filter (comp ipg/user-source? :source_id))
(->> (:members (groups-client/list-group-members-by-id sharer sharee))
(filter (comp groups-client/user-source? :source_id))
(mapcat (fn [{sharee :id}] (notifications-fn sharer sharee responses))))))
Loading