Skip to content

Commit 78c7ed6

Browse files
authored
Merge pull request #310 from slr71/main
CORE-2086: added a service-account endpoint for sending emails.
2 parents 094ab49 + 84b8c6e commit 78c7ed6

File tree

8 files changed

+296
-95
lines changed

8 files changed

+296
-95
lines changed

CLAUDE.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
Terrain is the primary REST API gateway for the CyVerse Discovery Environment. It's a Clojure application that validates
8+
user authentication and coordinates calls to other web services.
9+
10+
## Code Formatting
11+
12+
- Please follow the [clojure community style guidelines][1] when generating new code.
13+
- Please try to avoid repeated code when possible.
14+
- Please try to keep line lenghts to 120 characters or fewer for readability.
15+
16+
## Common Development Commands
17+
18+
### Build and Run
19+
- `lein uberjar` - Build the standalone JAR
20+
- `lein ring server` - Start development server on port 31325
21+
- `lein run` - Run the main application
22+
23+
### Code Quality
24+
- `lein eastwood` - Run linter (configured to check for wrong-arity, wrong-ns-form, wrong-pre-post, wrong-tag,
25+
misplaced-docstrings)
26+
- `lein cljfmt check` - Check code formatting
27+
- `lein cljfmt fix` - Fix code formatting
28+
- `lein ancient` - Check for outdated dependencies
29+
30+
### Testing
31+
- `lein test` - Run all tests
32+
- `lein test :only terrain.test-namespace` - Run specific test namespace
33+
- `lein test2junit` - Run tests with JUnit XML output
34+
35+
### REPL Development
36+
- `lein repl` - Start REPL
37+
- nREPL server runs on port 7888 when application starts
38+
39+
## Architecture Overview
40+
41+
### Core Structure
42+
The application follows a typical Clojure web service architecture with clear separation of concerns:
43+
44+
1. **Entry Point** (`src/terrain/core.clj`)
45+
- Main application initialization
46+
- Configuration loading from `terrain.properties`
47+
- nREPL server setup
48+
- NATS message queue connection
49+
50+
2. **Routing Layer** (`src/terrain/routes.clj` and `src/terrain/routes/`)
51+
- Compojure-based routing
52+
- Routes organized by domain (apps, filesystem, metadata, etc.)
53+
- Authentication middleware wrapping
54+
- Separate admin routes for privileged operations
55+
56+
3. **Service Layer** (`src/terrain/services/`)
57+
- Business logic implementation
58+
- Key service modules:
59+
- `admin.clj` - Miscellaneous administrative operations
60+
- `bags.clj` - Bag management for bulk operations
61+
- `bootstrap.clj` - Operations for preparing a user's workspace
62+
- `coge.clj` - Integration for CoGe (Comparative Genomics)
63+
- `collaborator_lists.clj` - Operations for managing lists of collaborators
64+
- `communities.clj` - Operations for managing communities in the Discovery Environment
65+
- `fileio/` - iRODS file I/O operations
66+
- `filesystem/` - iRODS filesystem operations
67+
- `metadata/` - App and data metadata management
68+
- `oauth.clj` - OAuth integration
69+
- `permanent_id_requests.clj` - Operations for managing permanent ID requests
70+
- `qms.clj` - Quota Management System integration
71+
- `requests.clj` - Operations for managing administrative requests
72+
- `sharing.clj` - Operations for sharing data
73+
- `subjects.clj` - Operations for searching for users and groups
74+
- `teams.clj` - Operations for managing teams in the Discovery Environment
75+
- `user_info.clj` - User profile management
76+
- `user_prefs.clj` - User preference management
77+
- `user_sessions.clj` - User session management
78+
79+
4. **Client Layer** (`src/terrain/clients/`)
80+
- External service integrations
81+
- HTTP clients for microservices communication
82+
83+
5. **Middleware** (`src/terrain/middleware.clj`)
84+
- Request/response transformation
85+
- User context injection
86+
- Logging and monitoring
87+
- Authentication
88+
89+
### Key Dependencies
90+
- **Web Framework**: Compojure + Ring
91+
- **HTTP Client**: clj-http
92+
- **JSON**: Cheshire
93+
- **Database**: PostgreSQL via Kameleon library
94+
- **Message Queue**: NATS
95+
- **Storage**: iRODS (via clj-jargon)
96+
- **Authentication**: CyVerse custom auth + OAuth and Keycloak
97+
98+
### Configuration
99+
The application expects configuration in `terrain.properties`, loaded from (in order):
100+
1. `$IPLANT_CONF_DIR/terrain.properties`
101+
2. Current working directory
102+
3. Classpath resources
103+
4. Fails if not found
104+
105+
### Service Integration Pattern
106+
Terrain acts as an API gateway, delegating to specialized microservices:
107+
- Metadata service for app/data metadata
108+
- Permissions service for access control
109+
- Groups service for team management
110+
- Async tasks service for job management
111+
112+
Each integration typically follows the pattern:
113+
1. Route handler validates request
114+
2. Request and response bodies are validated by `plumatic/schema`
115+
3. Service layer orchestrates calls to one or more clients
116+
4. Clients make HTTP requests to external services
117+
5. Responses are transformed and returned to caller
118+
119+
[1]: https://guide.clojure.style/

k8s/terrain.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ apiVersion: apps/v1
22
kind: Deployment
33
metadata:
44
name: terrain
5+
labels:
6+
app: cyverse-de
7+
de-app: terrain
58
spec:
69
replicas: 2
710
selector:
@@ -13,6 +16,7 @@ spec:
1316
template:
1417
metadata:
1518
labels:
19+
app: cyverse-de
1620
de-app: terrain
1721
spec:
1822
affinity:
@@ -136,6 +140,9 @@ apiVersion: v1
136140
kind: Service
137141
metadata:
138142
name: terrain
143+
labels:
144+
app: cyverse-de
145+
de-app: terrain
139146
spec:
140147
selector:
141148
de-app: terrain

src/terrain/routes.clj

Lines changed: 96 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,98 @@
11
(ns terrain.routes
2-
(:require [cheshire.core :as cheshire]
3-
[clojure-commons.lcase-params :refer [wrap-lcase-params]]
4-
[clojure-commons.query-params :refer [wrap-query-params]]
5-
[clojure.tools.logging :as log]
6-
[clojure-commons.exception :as cx]
7-
[common-swagger-api.schema :as schema]
8-
[compojure.route :as route]
9-
[compojure.api.core :refer [route-middleware]]
10-
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
11-
[service-logging.thread-context :as tc]
12-
[service-logging.middleware :refer [wrap-logging clean-context]]
13-
[terrain.auth.user-attributes :as user-attributes]
14-
[terrain.middleware :refer [wrap-context-path-adder wrap-create-workspace wrap-query-param-remover]]
15-
[terrain.routes.admin :refer [secured-admin-routes]]
16-
[terrain.routes.alerts :refer [alerts-routes admin-alerts-routes]]
17-
[terrain.routes.analyses :refer [analysis-routes quicklaunch-routes]]
18-
[terrain.routes.apps.admin.apps :refer [admin-apps-routes]]
19-
[terrain.routes.apps.admin.reference-genomes :refer [admin-reference-genomes-routes]]
20-
[terrain.routes.apps.admin.tools :refer [admin-tool-request-routes admin-tool-routes]]
21-
[terrain.routes.apps.categories :refer [app-category-routes app-community-routes app-ontology-routes]]
22-
[terrain.routes.apps.communities :refer [app-community-tag-routes]]
23-
[terrain.routes.apps.elements :refer [app-elements-routes]]
24-
[terrain.routes.apps.metadata :refer [app-avu-routes]]
25-
[terrain.routes.apps.pipelines :refer [app-pipeline-routes]]
26-
[terrain.routes.apps.reference-genomes :refer [reference-genomes-routes]]
27-
[terrain.routes.apps.tools :refer [tool-request-routes tool-routes]]
28-
[terrain.routes.apps.versions :refer [app-version-routes]]
29-
[terrain.routes.bags :refer [bag-routes]]
30-
[terrain.routes.bootstrap :refer [secured-bootstrap-routes]]
31-
[terrain.routes.callbacks :refer [callback-routes]]
32-
[terrain.routes.dashboard-aggregator :refer [dashboard-aggregator-routes]]
33-
[terrain.routes.data :refer [secured-data-routes]]
34-
[terrain.routes.fileio :refer [secured-fileio-routes]]
35-
[terrain.routes.filesystem :refer [admin-filesystem-metadata-routes
36-
secured-filesystem-metadata-routes
37-
secured-filesystem-routes]]
38-
[terrain.routes.filesystem.exists :refer [filesystem-existence-routes]]
39-
[terrain.routes.filesystem.navigation :refer [filesystem-navigation-routes]]
40-
[terrain.routes.filesystem.stats :refer [filesystem-stat-routes]]
41-
[terrain.routes.filesystem.tickets :refer [filesystem-ticket-routes]]
42-
[terrain.routes.groups :refer [admin-groups-routes]]
43-
[terrain.routes.instantlaunches :refer [admin-instant-launch-routes instant-launch-routes]]
44-
[terrain.routes.metadata :refer [admin-app-avu-routes
45-
admin-app-community-routes
46-
admin-category-routes
47-
admin-integration-data-routes
48-
admin-ontology-routes
49-
admin-workspace-routes
50-
apps-routes
51-
misc-metadata-routes]]
52-
[terrain.routes.misc :refer [unsecured-misc-routes]]
53-
[terrain.routes.notification :refer [admin-notification-routes
54-
secured-notification-routes
55-
unsecured-notification-routes]]
56-
[terrain.routes.permanent-id-requests :refer [admin-permanent-id-request-routes permanent-id-request-routes]]
57-
[terrain.routes.pref :refer [secured-pref-routes]]
58-
[terrain.routes.resource-usage-api :refer [resource-usage-api-routes]]
59-
[terrain.routes.data-usage-api :refer [data-usage-api-routes]]
60-
[terrain.routes.session :refer [secured-session-routes]]
61-
[terrain.routes.user-info :refer [admin-user-info-routes secured-user-info-routes]]
62-
[terrain.routes.collaborator :refer [admin-community-routes
63-
collaborator-list-routes
64-
community-routes
65-
subject-routes
66-
team-routes]]
67-
[terrain.routes.search :refer [secured-search-routes]]
68-
[terrain.routes.coge :refer [coge-routes]]
69-
[terrain.routes.oauth :refer [oauth-admin-routes oauth-routes secured-oauth-routes]]
70-
[terrain.routes.favorites :refer [secured-favorites-routes]]
71-
[terrain.routes.tags :refer [secured-tag-routes]]
72-
[terrain.routes.token :refer [admin-token-routes token-routes]]
73-
[terrain.routes.webhooks :refer [webhook-routes]]
74-
[terrain.routes.comments :refer [admin-app-comment-routes
75-
admin-comment-routes
76-
admin-data-comment-routes
77-
app-comment-routes
78-
data-comment-routes]]
79-
[terrain.routes.qms :refer [admin-qms-api-routes qms-api-routes service-account-qms-api-routes]]
80-
[terrain.routes.requests :refer [admin-request-routes admin-request-type-routes request-routes]]
81-
[terrain.routes.settings :refer [admin-setting-routes]]
82-
[terrain.routes.vice :refer [admin-vice-routes vice-routes]]
83-
[terrain.util :as util]
84-
[terrain.util.transformers :as transform]
85-
[terrain.util.service :as service]
86-
[terrain.util.config :as config]))
2+
(:require
3+
[cheshire.core :as cheshire]
4+
[clojure-commons.exception :as cx]
5+
[clojure-commons.lcase-params :refer [wrap-lcase-params]]
6+
[clojure-commons.query-params :refer [wrap-query-params]]
7+
[clojure.tools.logging :as log]
8+
[common-swagger-api.schema :as schema]
9+
[compojure.api.core :refer [route-middleware]]
10+
[compojure.route :as route]
11+
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
12+
[service-logging.middleware :refer [clean-context wrap-logging]]
13+
[service-logging.thread-context :as tc]
14+
[terrain.auth.user-attributes :as user-attributes]
15+
[terrain.middleware :refer [wrap-context-path-adder wrap-create-workspace
16+
wrap-query-param-remover]]
17+
[terrain.routes.admin :refer [secured-admin-routes]]
18+
[terrain.routes.alerts :refer [admin-alerts-routes alerts-routes]]
19+
[terrain.routes.analyses :refer [analysis-routes quicklaunch-routes]]
20+
[terrain.routes.apps.admin.apps :refer [admin-apps-routes]]
21+
[terrain.routes.apps.admin.reference-genomes :refer [admin-reference-genomes-routes]]
22+
[terrain.routes.apps.admin.tools :refer [admin-tool-request-routes
23+
admin-tool-routes]]
24+
[terrain.routes.apps.categories :refer [app-category-routes
25+
app-community-routes
26+
app-ontology-routes]]
27+
[terrain.routes.apps.communities :refer [app-community-tag-routes]]
28+
[terrain.routes.apps.elements :refer [app-elements-routes]]
29+
[terrain.routes.apps.metadata :refer [app-avu-routes]]
30+
[terrain.routes.apps.pipelines :refer [app-pipeline-routes]]
31+
[terrain.routes.apps.reference-genomes :refer [reference-genomes-routes]]
32+
[terrain.routes.apps.tools :refer [tool-request-routes tool-routes]]
33+
[terrain.routes.apps.versions :refer [app-version-routes]]
34+
[terrain.routes.bags :refer [bag-routes]]
35+
[terrain.routes.bootstrap :refer [secured-bootstrap-routes]]
36+
[terrain.routes.callbacks :refer [callback-routes]]
37+
[terrain.routes.coge :refer [coge-routes]]
38+
[terrain.routes.collaborator :refer [admin-community-routes
39+
collaborator-list-routes
40+
community-routes subject-routes
41+
team-routes]]
42+
[terrain.routes.comments :refer [admin-app-comment-routes
43+
admin-comment-routes
44+
admin-data-comment-routes
45+
app-comment-routes data-comment-routes]]
46+
[terrain.routes.dashboard-aggregator :refer [dashboard-aggregator-routes]]
47+
[terrain.routes.data :refer [secured-data-routes]]
48+
[terrain.routes.data-usage-api :refer [data-usage-api-routes]]
49+
[terrain.routes.email :refer [service-account-email-routes]]
50+
[terrain.routes.favorites :refer [secured-favorites-routes]]
51+
[terrain.routes.fileio :refer [secured-fileio-routes]]
52+
[terrain.routes.filesystem :refer [admin-filesystem-metadata-routes
53+
secured-filesystem-metadata-routes
54+
secured-filesystem-routes]]
55+
[terrain.routes.filesystem.exists :refer [filesystem-existence-routes]]
56+
[terrain.routes.filesystem.navigation :refer [filesystem-navigation-routes]]
57+
[terrain.routes.filesystem.stats :refer [filesystem-stat-routes]]
58+
[terrain.routes.filesystem.tickets :refer [filesystem-ticket-routes]]
59+
[terrain.routes.groups :refer [admin-groups-routes]]
60+
[terrain.routes.instantlaunches :refer [admin-instant-launch-routes
61+
instant-launch-routes]]
62+
[terrain.routes.metadata :refer [admin-app-avu-routes
63+
admin-app-community-routes
64+
admin-category-routes
65+
admin-integration-data-routes
66+
admin-ontology-routes
67+
admin-workspace-routes apps-routes
68+
misc-metadata-routes]]
69+
[terrain.routes.misc :refer [unsecured-misc-routes]]
70+
[terrain.routes.notification :refer [admin-notification-routes
71+
secured-notification-routes
72+
unsecured-notification-routes]]
73+
[terrain.routes.oauth :refer [oauth-admin-routes oauth-routes
74+
secured-oauth-routes]]
75+
[terrain.routes.permanent-id-requests :refer [admin-permanent-id-request-routes
76+
permanent-id-request-routes]]
77+
[terrain.routes.pref :refer [secured-pref-routes]]
78+
[terrain.routes.qms :refer [admin-qms-api-routes qms-api-routes
79+
service-account-qms-api-routes]]
80+
[terrain.routes.requests :refer [admin-request-routes
81+
admin-request-type-routes request-routes]]
82+
[terrain.routes.resource-usage-api :refer [resource-usage-api-routes]]
83+
[terrain.routes.search :refer [secured-search-routes]]
84+
[terrain.routes.session :refer [secured-session-routes]]
85+
[terrain.routes.settings :refer [admin-setting-routes]]
86+
[terrain.routes.tags :refer [secured-tag-routes]]
87+
[terrain.routes.token :refer [admin-token-routes token-routes]]
88+
[terrain.routes.user-info :refer [admin-user-info-routes
89+
secured-user-info-routes]]
90+
[terrain.routes.vice :refer [admin-vice-routes vice-routes]]
91+
[terrain.routes.webhooks :refer [webhook-routes]]
92+
[terrain.util :as util]
93+
[terrain.util.config :as config]
94+
[terrain.util.service :as service]
95+
[terrain.util.transformers :as transform]))
8796

8897
(defn- wrap-user-info
8998
[handler]
@@ -212,6 +221,7 @@
212221
[]
213222
(util/flagged-routes
214223
(service-account-qms-api-routes)
224+
(service-account-email-routes)
215225
(route/not-found (service/unrecognized-path-response))))
216226

217227
(defn unsecured-routes
@@ -352,6 +362,7 @@
352362
{:name "user-info", :description "User Information Endpoints"}
353363
{:name "webhooks", :description "Webhook Endpoints"}
354364
{:name "vice", :description "VICE Endpoints"}
365+
{:name "service-account-email" :description "Service Account Email Endpoints"}
355366
{:name "service-account-qms", :description "Service Account QMS Endpoints"}]
356367
:securityDefinitions security-definitions}})
357368
(route-middleware

src/terrain/routes/email.clj

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
(ns terrain.routes.email
2+
(:require
3+
[common-swagger-api.schema :refer [context POST]]
4+
[ring.util.http-response :refer [ok]]
5+
[terrain.auth.user-attributes :refer [require-service-account]]
6+
[terrain.routes.schemas.email :as s]
7+
[terrain.services.email :as email-service]
8+
[terrain.util :refer [optional-routes]]
9+
[terrain.util.config :as config]))
10+
11+
(declare body)
12+
(defn service-account-email-routes
13+
[]
14+
(optional-routes
15+
[config/email-api-routes-enabled]
16+
17+
(context "/email" []
18+
:tags ["service-account-email"]
19+
20+
(POST "/" []
21+
:middleware [[require-service-account ["cyverse-emailer"]]]
22+
:summary s/SendEmailSummary
23+
:description s/SendEmailDescription
24+
:body [body s/SendEmailRequestBody]
25+
:return s/SendEmailResponse
26+
(ok (email-service/send-email body))))))
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
(ns terrain.routes.schemas.email
2+
(:require
3+
[common-swagger-api.schema :as s]
4+
[schema.core :refer [Keyword]]))
5+
6+
(def SendEmailSummary "Send an Email")
7+
8+
(def SendEmailDescription "Submits a request to send an email to a user.")
9+
10+
(def SendEmailResponse
11+
{:status (s/describe s/NonBlankString "The status of the request")})
12+
13+
(def SendEmailRequestBody
14+
{:to (s/describe s/NonBlankString "The email address to send the message to")
15+
:from_addr (s/describe s/NonBlankString "The email address of the message sender")
16+
:from_name (s/describe s/NonBlankString "The name of the message sender")
17+
:subject (s/describe s/NonBlankString "The message subject")
18+
:template (s/describe s/NonBlankString "The name of the email template to use")
19+
:values (s/describe {Keyword s/NonBlankString} "The values to plug into the email template")})

0 commit comments

Comments
 (0)