Skip to content

Commit

Permalink
Merge pull request #8 from OHSUCMP/develop
Browse files Browse the repository at this point in the history
All SDS Features to date
  • Loading branch information
mattStorer authored Jul 10, 2024
2 parents 85cf8bf + 4826133 commit c8c6aa1
Show file tree
Hide file tree
Showing 74 changed files with 6,747 additions and 33 deletions.
21 changes: 21 additions & 0 deletions dbsetup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# PostgreSQL database setup for RHEL systems

This document lists steps required to create the PostgreSQL database and roles necessary for the SDS to operate.

1. `sudo yum install postgresql-server`
2. `sudo postgresql-setup --initdb`
3. `sudo systemctl enable postgresql.service`
4. `sudo systemctl start postgresql.service`
5. `sudo -i -u postgres psql`
1. `CREATE ROLE ecpsds_admin WITH NOLOGIN NOSUPERUSER INHERIT NOCREATEDB NOCREATEROLE NOREPLICATION;`
2. `COMMENT ON ROLE ecpsds_admin IS 'eCarePlan Supplemental Data Store Administrator Role';`
3. `CREATE ROLE ecpsds_owner WITH LOGIN NOSUPERUSER INHERIT NOCREATEDB NOCREATEROLE NOREPLICATION PASSWORD 'change_ME!';`
4. `GRANT ecpsds_admin TO ecpsds_owner;`
5. `CREATE DATABASE ecpsds WITH OWNER = ecpsds_owner ENCODING = 'UTF8' TABLESPACE = pg_default CONNECTION LIMIT = -1 IS_TEMPLATE = False;`
6. `ALTER DEFAULT PRIVILEGES FOR ROLE postgres GRANT ALL ON TABLES TO ecpsds_admin;`
6. Enable password authentication for local socket access (see https://stackoverflow.com/questions/18664074/getting-error-peer-authentication-failed-for-user-postgres-when-trying-to-ge for details)
1. `sudo vim /var/lib/pgsql/data/pg_hba.conf`
2. Find the line `local all all peer` and change it to `local all all password`.
3. Save the file and quit
4. `sudo systemctl restart postgresql.service`
7. `psql -d ecpsds -U ecpsds_owner -W`
12 changes: 12 additions & 0 deletions introspect-proxy/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

FROM alpine:latest

EXPOSE 8181
WORKDIR /serve

RUN apk add --no-cache python3

COPY introspect-proxy.py /serve/introspect-proxy.py
COPY my_response_file.json /serve/response_file.json

ENTRYPOINT [ "/serve/introspect-proxy.py", "8181", "/serve/response_file.json" ]
29 changes: 29 additions & 0 deletions introspect-proxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# introspect-proxy

### *** FOR DEVELOPMENT PURPOSES ONLY ***

_introspect-proxy_ is a stub system that facilitates running the SDS in a development context in which an OAuth2
introspect endpoint isn't available.

### To run:

Install Docker and run the following commands:

```
docker build -t introspect .
docker compose up
```

Then configure the following property in _application.yaml_:

```
security:
oauth2:
resourceserver:
opaque-token:
introspection-uri: http://localhost:8181/introspect
```

This will instantiate a dummy _introspect_ endpoint that will always return the contents of the file
_my_response_file.json_, the `sub` element of which should contain the fully-qualified FHIR Patient ID
of a test _Patient_ resource that one is working with in a development context.
6 changes: 6 additions & 0 deletions introspect-proxy/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: "3.9"
services:
web:
image: introspect
ports:
- "8181:8181"
22 changes: 22 additions & 0 deletions introspect-proxy/introspect-proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env python3

from http.server import BaseHTTPRequestHandler, HTTPServer
import os
import logging
import sys


listen_port = int( sys.argv[1] )
response_filename = sys.argv[2]

class IntrospectHTTPRequestHandler(BaseHTTPRequestHandler):
def do_POST( self ):
self.send_response( 200 )
self.send_header('Content-type','application/json')
self.end_headers()
with open( response_filename, 'rb' ) as response_file:
contents = response_file.read()
self.wfile.write( contents )

with HTTPServer( ('', listen_port), IntrospectHTTPRequestHandler ) as server:
server.serve_forever()
6 changes: 6 additions & 0 deletions introspect-proxy/my_response_file.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"active": true,
"client_id": "5fa54c47-ed80-405b-a0b7-611eee5d0159",
"scope": "patient/* openid fhirUser",
"sub": "https://gw.interop.community/MCCStaging/data/Patient/5806e210-771a-4986-ae48-e88ae3e82997"
}
12 changes: 7 additions & 5 deletions local-config/application.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#Adds the option to go to eg. http://localhost:8080/actuator/health for seeing the running configuration
#see https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints
sds:
require-base-url: false
partition:
local-name: SDS-LOCAL
management:
Expand All @@ -16,6 +17,7 @@ spring:
oauth2:
resourceserver:
opaque-token:
# introspection-uri: http://localhost:8181/introspect
introspection-uri: https://epicmobile.ohsu.edu/FHIRDEV/oauth2/introspect
flyway:
enabled: false
Expand All @@ -24,7 +26,7 @@ spring:
datasource:
url: 'jdbc:postgresql://localhost:5432/ecpsds'
username: ecpsds_owner
password: ecpsds
password: change_ME!
driverClassName: org.postgresql.Driver

# url: 'jdbc:h2:file:./target/database/h2'
Expand Down Expand Up @@ -104,7 +106,7 @@ hapi:
# allowed_bundle_types: COLLECTION,DOCUMENT,MESSAGE,TRANSACTION,TRANSACTIONRESPONSE,BATCH,BATCHRESPONSE,HISTORY,SEARCHSET
# allow_cascading_deletes: true
# allow_contains_searches: true
# allow_external_references: true
allow_external_references: true
# allow_multiple_delete: true
# allow_override_default_search_params: true
# auto_create_placeholder_reference_targets: false
Expand All @@ -125,11 +127,11 @@ hapi:
bulk_import_enabled: false
# enforce_referential_integrity_on_delete: false
# This is an experimental feature, and does not fully support _total and other FHIR features.
# enforce_referential_integrity_on_delete: false
# enforce_referential_integrity_on_write: false
enforce_referential_integrity_on_delete: false
enforce_referential_integrity_on_write: false
# etag_support_enabled: true
# expunge_enabled: true
# client_id_strategy: ALPHANUMERIC
client_id_strategy: ANY # support client-supplied IDs
# fhirpath_interceptor_enabled: false
# filter_search_enabled: true
# graphql_enabled: true
Expand Down
20 changes: 20 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<java.version>11</java.version>
<logback-classic.version>1.2.11</logback-classic.version>
<slf4j-api.version>1.7.25</slf4j-api.version>
<spring_version>5.3.26</spring_version>
</properties>

<prerequisites>
Expand Down Expand Up @@ -392,6 +393,25 @@
<version>9.25</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-netty-no-dependencies</artifactId>
<version>5.15.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-client-java-no-dependencies</artifactId>
<version>5.15.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mock-server</groupId>
<artifactId>mockserver-spring-test-listener-no-dependencies</artifactId>
<version>5.15.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
34 changes: 34 additions & 0 deletions src/main/java/ca/uhn/fhir/jpa/starter/AppSecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package ca.uhn.fhir.jpa.starter;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class AppSecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/**/metadata")
.anonymous()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS)
.anonymous()
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.oauth2ResourceServer( oauth2 -> oauth2.opaqueToken() );

return http.build();
}

}
5 changes: 5 additions & 0 deletions src/main/java/ca/uhn/fhir/jpa/starter/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import ca.uhn.fhir.jpa.subscription.match.config.WebsocketDispatcherConfig;
import ca.uhn.fhir.jpa.subscription.submit.config.SubscriptionSubmitterConfig;
import ca.uhn.fhir.rest.server.RestfulServer;
import edu.ohsu.cmp.ecp.sds.SupplementalDataStorePartitioningConfig;
import edu.ohsu.cmp.ecp.security.ApplicationOpaqueTokenIntrospector;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.boot.SpringApplication;
Expand All @@ -21,12 +24,14 @@
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Import;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

@ServletComponentScan(basePackageClasses = {RestfulServer.class})
@ComponentScan(basePackageClasses = {Application.class, SupplementalDataStorePartitioningConfig.class, ApplicationOpaqueTokenIntrospector.class})
@SpringBootApplication(exclude = {ElasticsearchRestClientAutoConfiguration.class, ThymeleafAutoConfiguration.class})
@Import({
SubscriptionSubmitterConfig.class,
Expand Down
105 changes: 105 additions & 0 deletions src/main/java/edu/ohsu/cmp/ecp/SDS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Supplemental Data Store

## Implementation

### Partitioning

#### Partitioning - Configuration

Configure the name to use for the partition that holds local resources.

> ##### Example - Partitioning - Configuration
> In application.properties:
>
> `sds.partition.local-name: SDS-LOCAL`
>
#### Implementation

Implement a bean to read the configuration.

Implement an [interceptor](https://hapifhir.io/hapi-fhir/docs/interceptors/server_interceptors.html)
to [extract the partition name](https://hapifhir.io/hapi-fhir/docs/server_jpa_partitioning/partitioning.html)
from a custom header.
HAPI
provides [some examples](https://hapifhir.io/hapi-fhir/docs/server_jpa_partitioning/partition_interceptor_examples.html).

Implement a bean that configures partitioning and registers the interceptor.

> ##### Example - Partitioning - Implementation
>
> Add SupplementalDataStoreProperties.java to the project
> where it can be component-scanned.
>
> Add SupplementalDataStorePartitionInterceptor.java to the project.
>
> Add SupplementalDataStorePartitioningConfig.java to the project
> where it can be component-scanned.
>
### Authorization

#### Configuration

Configure the introspection endpoint for authenticating bearer tokens.

> ##### Example - Authorization - Configuration
> In application.properties:
>
> `spring.security.oauth2.resourceserver.opaque-token.introspection-uri: https://my-ehr.org/oauth2/introspect`
>
#### Dependencies

Include dependencies for Spring Boot support for OAuth2.

> ##### Example - Authorization - Dependencies
> In pom.xml:
>
> ```xml
> <dependency>
> <groupId>org.springframework.boot</groupId>
> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
> </dependency>
> <dependency>
> <groupId>com.nimbusds</groupId>
> <artifactId>oauth2-oidc-sdk</artifactId>
> <version>9.25</version>
> <scope>runtime</scope>
> </dependency>
> ```
>
#### Implementation - Authorizing the Introspection
Implement beans that cause the introspection process to use the token
that is being introspect-ed as the authorization to use
the introspect service.
> ##### Example - Authorization - Implementation - Introspect
>
> Add ApplicationOpaqueTokenIntrospector.java
> and IntrospectorReflexiveAuthenticationInterceptor.java to the project
> where they can be component-scanned.
>
#### Implementation - Supplemental Data Store Requirements
Implement an [interceptor](https://hapifhir.io/hapi-fhir/docs/interceptors/server_interceptors.html)
to setup rules based on
data [from a successful the authentication](https://hapifhir.io/hapi-fhir/docs/security/authorization_interceptor.html).
Implement a bean to extract the subject from the authentication.
Implement a bean to search Linkage resources in the local partition.
> ##### Example - Authorization - Implementation - SDS
>
> Add SupplementalDataStoreAuthorizationInterceptor.java to the project.
>
> Add SupplementalDataStoreAuth.java SupplementalDataStoreAuthR4.java to the project
> where it can be component-scanned.
>
> Add SupplementalDataStoreLinkage.java and SupplementalDataStoreLinkageR4.java to the project
> where it can be component-scanned.
>
Loading

0 comments on commit c8c6aa1

Please sign in to comment.