mirror of
https://github.com/IRS-Public/direct-file.git
synced 2025-08-17 10:10:53 +00:00
initial commit
This commit is contained in:
parent
2f3ebd6693
commit
e0d5c84451
3413 changed files with 794524 additions and 1 deletions
44
direct-file/state-api/.gitignore
vendored
Normal file
44
direct-file/state-api/.gitignore
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
HELP.md
|
||||
/src/main/resources/application-local.yaml
|
||||
target/
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
!**/src/main/resources/certs/
|
||||
*.jar
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### docker-related ###
|
||||
/docker/db/postgres/data
|
||||
|
||||
### Gradle ###
|
||||
.gradle/
|
||||
|
||||
### Spotbugs ###
|
||||
src/main/resources/spotbugs/
|
5
direct-file/state-api/.liquibase.properties
Normal file
5
direct-file/state-api/.liquibase.properties
Normal file
|
@ -0,0 +1,5 @@
|
|||
changeLogFile=db/changelog.yaml
|
||||
url=jdbc:postgresql://localhost:5433/stateapi
|
||||
username=postgres
|
||||
password=postgres
|
||||
changesetAuthor=directfile
|
19
direct-file/state-api/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
19
direct-file/state-api/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
wrapperVersion=3.3.2
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip
|
44
direct-file/state-api/Dockerfile-local
Normal file
44
direct-file/state-api/Dockerfile-local
Normal file
|
@ -0,0 +1,44 @@
|
|||
#syntax=docker/dockerfile:1.7-labs
|
||||
# build factgraph since it will be a dependency in share-libs-builder
|
||||
FROM sbtscala/scala-sbt:eclipse-temurin-alpine-21.0.2_13_1.9.9_3.3.3 AS factgraph-builder
|
||||
WORKDIR /build/
|
||||
COPY --from=factgraph-repo js/src/ js/src/
|
||||
COPY --from=factgraph-repo jvm/src/ jvm/src/
|
||||
COPY --from=factgraph-repo project/build.properties project/plugins.sbt project/
|
||||
COPY --from=factgraph-repo shared/ shared/
|
||||
COPY --from=factgraph-repo build.sbt .
|
||||
RUN sbt compile package publishM2
|
||||
|
||||
# build shared dependencies
|
||||
FROM eclipse-temurin:21-jdk-alpine AS shared-dependencies-builder
|
||||
COPY --from=factgraph-builder /root/.m2/repository/gov/irs/factgraph/fact-graph_3/ /root/.m2/repository/gov/irs/factgraph/fact-graph_3/
|
||||
ARG MAVEN_OPTS=""
|
||||
WORKDIR /build/
|
||||
COPY --from=config . ./config/
|
||||
COPY --from=boms . ./boms/
|
||||
WORKDIR /build/libs/
|
||||
COPY --from=shared-libs .mvn/wrapper/maven-wrapper.properties .mvn/wrapper/
|
||||
COPY --from=shared-libs mvnw ./
|
||||
COPY --from=shared-libs --parents **/pom.xml ./
|
||||
RUN ./mvnw dependency:resolve -P resolve
|
||||
COPY --from=shared-libs starters/ ./starters/
|
||||
COPY --from=shared-libs data-models/ ./data-models/
|
||||
RUN ./mvnw install
|
||||
|
||||
# build the application
|
||||
FROM shared-dependencies-builder AS state-api-builder
|
||||
ARG MAVEN_OPTS=""
|
||||
COPY --from=config . /config/
|
||||
WORKDIR /build/
|
||||
COPY mvnw pom.xml ./
|
||||
COPY .mvn/wrapper/maven-wrapper.properties .mvn/wrapper/
|
||||
RUN ./mvnw dependency:resolve
|
||||
COPY src/ src/
|
||||
RUN ./mvnw package
|
||||
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
COPY --from=state-api-builder /build/target/state-api-0.0.1-SNAPSHOT.jar /deployments/state-api.jar
|
||||
|
||||
RUN adduser --system --no-create-home jar-runner
|
||||
USER jar-runner
|
||||
CMD ["java", "-jar", "/deployments/state-api.jar"]
|
151
direct-file/state-api/README.md
Normal file
151
direct-file/state-api/README.md
Normal file
|
@ -0,0 +1,151 @@
|
|||
# Direct File State Tax API
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Local Development
|
||||
|
||||
This project relies on locally installed Maven packages in order to build; therefore, you can run the `/scripts/build-project.sh` which will install the shared dependencies
|
||||
|
||||
```sh
|
||||
../scripts/build-project.sh
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
To build state-api image, run
|
||||
|
||||
```
|
||||
docker compose build state-api
|
||||
```
|
||||
|
||||
To quickly set up the application and run the supporting services (localstack, db), run the following command:
|
||||
|
||||
```bash
|
||||
docker compose up state-api -d
|
||||
```
|
||||
|
||||
The `state-api` application will be accessible at http://localhost:8081, and the PostgreSQL database will be available on port 5433 with the default username and password `postgres`.
|
||||
|
||||
Health check: http://localhost:8081/actuator/health
|
||||
Swagger API: http://localhost:8081/swagger-ui/index.html
|
||||
|
||||
## Database migrations
|
||||
|
||||
This app uses the same Liquibase setup as the [backend](../backend) app.
|
||||
See the [backend README section for database migrations for more details](../backend/README.md#database-migrations).
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Save Authorization Code
|
||||
|
||||
Save the provided JSON data to the database.
|
||||
|
||||
**Endpoint:** `/state-api/authorization-code`
|
||||
|
||||
**Sample Data:**
|
||||
Ensure value of stateCode is in state_profie table
|
||||
```json
|
||||
{"taxReturnUuid": "4638655a-5798-4174-a5a0-37cc3b3cd9a0", "tin": "123456789", "taxYear": 2022, "stateCode":"FS", "submissionId":"12345678901234567890"}
|
||||
```
|
||||
|
||||
**cURL Command:**
|
||||
```bash
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"taxReturnUuid": "4638655a-5798-4174-a5a0-37cc3b3cd9a0", "tin": "123456789", "taxYear": 2022, "stateCode":"FS", "submissionId":"12345678901234567890"}' http://localhost:8081/state-api/authorization-code -i
|
||||
```
|
||||
|
||||
### V2: Generate New Authorization Token
|
||||
|
||||
Get a [JWT](https://jwt.io/) with symmetrically-encrypted metadata for future (time-limited) access to
|
||||
the V2 tax return export.
|
||||
|
||||
**Endpoint:** `state-api/v2/authorization-token`
|
||||
|
||||
### Export Tax Return Data
|
||||
|
||||
Retrieve tax return data based on provided headers.
|
||||
|
||||
**Endpoint:** `/state-api/export-return`
|
||||
|
||||
The request includes a JWT Bearer token signed with a private key, containing the state account ID and authorization code. Export an encrypted tax return for a particular taxpayer identified by the authorization code passed in the JWT Bearer token. If the operation is successful, it will return a status of 200. In case of an error, it will also return a status of 200 along with an error code, such as,
|
||||
|
||||
```
|
||||
E_BEARER_TOKEN_MISSING
|
||||
E_AUTHORZIATION_CODE_NOT_EXIST
|
||||
E_AUTHORIZATION_CODE_EXPIRED
|
||||
E_AUTHORIZATION_CODE_INVALID_FORMAT
|
||||
E_ACCOUNT_ID_NOT_EXIST
|
||||
E_JWT_VERIFICATION_FAILED
|
||||
E_CERTIFICATE_NOT_FOUND
|
||||
E_CERTIFICATE_EXPIRED
|
||||
E_INTERNAL_SERVER_ERROR
|
||||
```
|
||||
|
||||
Success return:
|
||||
```
|
||||
{
|
||||
"status": "success",
|
||||
"taxReturn": "encoded-encrypted-data"
|
||||
}
|
||||
```
|
||||
|
||||
encrypted taxReturn includes return status and xml data, submissionId, directFileData, and status can be "accepted", "rejected", "pending". Sample value:
|
||||
{
|
||||
"status": "accepted",
|
||||
"submissionId": "123456",
|
||||
"xml": "return-data in xml",
|
||||
"directFileData": "JSON formatted data collected by direct file"
|
||||
}
|
||||
|
||||
Error return:
|
||||
```
|
||||
{
|
||||
"status": "error",
|
||||
"error": "E_CERTIFICATE_EXPIRED"
|
||||
}
|
||||
```
|
||||
|
||||
Testing using cURL alone may not be straightforward. We recommend using Postman for this purpose.
|
||||
|
||||
PostMan setting:
|
||||
|
||||
GET http://localhost:8081/state-api/export-return
|
||||
|
||||
```
|
||||
Authorization: select JWT Bearer
|
||||
|
||||
Algorithm: RS256
|
||||
Private Key: copy content from src/test/resources/certificates/fakestate.key and paste here
|
||||
Payload: {"iss":"123456","sub":"cd19876a-328c-4173-b4e6-59b55f4bb99e","iat":1516239022} where "iss" for account-id, "sub" for authorization code, and "iat" is value of time.time() without quotes. Ensure value of "sub" is in authorization_code table.
|
||||
|
||||
Header Prefix: Bearer
|
||||
|
||||
```
|
||||
## Cleanup
|
||||
|
||||
When you're finished using the application, you can tear it down with the following command:
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
## Clean Start up on localhost
|
||||
```
|
||||
1. docker compose down
|
||||
2. docker container prune (select y)
|
||||
3. docker compose build
|
||||
4. delete docker/db/data folder (if you are not the first to run)
|
||||
5. docker compose up -d
|
||||
```
|
||||
endpoints:
|
||||
http://localhost:8081/state-api/authorization-code
|
||||
http://localhost:8081/state-api/v2/authorization-token
|
||||
http://localhost:8081/state-api/export-return
|
||||
|
||||
## Integration Tests
|
||||
Make sure all dependent docker containers up running locally before doing the integration tests.
|
||||
The State API app itself should not be running. The test class will spin up a properly configured instance on demand.
|
||||
```
|
||||
mvn test -DrunIntegrationTests -Dtest=StateApiAppTest
|
||||
or
|
||||
./integrationtest.sh
|
||||
```
|
2
direct-file/state-api/docker/.gitignore
vendored
Normal file
2
direct-file/state-api/docker/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
### docker-related ###
|
||||
/db/postgres/data
|
|
@ -0,0 +1 @@
|
|||
CREATE DATABASE stateapi;
|
34
direct-file/state-api/docker_build.sh
Executable file
34
direct-file/state-api/docker_build.sh
Executable file
|
@ -0,0 +1,34 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
|
||||
cd "$SCRIPT_DIR"
|
||||
echo "$SCRIPT_DIR"
|
||||
|
||||
api_build_image_tag="state-api"
|
||||
api_build_container_name="state-api-builder-container"
|
||||
api_jar_file_name="state-api-0.0.1-SNAPSHOT.jar"
|
||||
jar_output_path_component="target"
|
||||
|
||||
# build jar
|
||||
docker buildx build \
|
||||
--pull \
|
||||
--build-context factgraph-repo="../fact-graph-scala" \
|
||||
--build-context boms="../boms" \
|
||||
--build-context config="../config" \
|
||||
--build-context shared-libs="../libs" \
|
||||
--build-arg MAVEN_OPTS="$MAVEN_OPTS" \
|
||||
--tag "$api_build_image_tag" \
|
||||
--file Dockerfile-local \
|
||||
"$@" \
|
||||
"$SCRIPT_DIR"
|
||||
|
||||
# extract jar to `./target`
|
||||
mkdir -p "$SCRIPT_DIR/$jar_output_path_component"
|
||||
docker container rm --force "$api_build_container_name" &>/dev/null
|
||||
docker container create --name "$api_build_container_name" "$api_build_image_tag"
|
||||
docker cp "$api_build_container_name":/deployments/state-api.jar "$SCRIPT_DIR/$jar_output_path_component/$api_jar_file_name"
|
||||
docker container rm --force "$api_build_container_name"
|
||||
|
||||
printf "\njarfile: %s\n" "$SCRIPT_DIR/$jar_output_path_component/$api_jar_file_name"
|
1
direct-file/state-api/integrationtest.sh
Executable file
1
direct-file/state-api/integrationtest.sh
Executable file
|
@ -0,0 +1 @@
|
|||
./mvnw test -DrunIntegrationTests -Dtest=StateApiAppTest,StateApiAppNoOverrideTest
|
3
direct-file/state-api/localrun.sh
Executable file
3
direct-file/state-api/localrun.sh
Executable file
|
@ -0,0 +1,3 @@
|
|||
#cp -R ./app-config/* target/.
|
||||
cd target
|
||||
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 -jar state-api-0.0.1-SNAPSHOT.jar
|
259
direct-file/state-api/mvnw
vendored
Executable file
259
direct-file/state-api/mvnw
vendored
Executable file
|
@ -0,0 +1,259 @@
|
|||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
149
direct-file/state-api/mvnw.cmd
vendored
Normal file
149
direct-file/state-api/mvnw.cmd
vendored
Normal file
|
@ -0,0 +1,149 @@
|
|||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
|
||||
}
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
206
direct-file/state-api/pom.xml
Normal file
206
direct-file/state-api/pom.xml
Normal file
|
@ -0,0 +1,206 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>gov.irs.directfile.boot</groupId>
|
||||
<artifactId>irs-spring-boot-starter-parent</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<relativePath>../boms/irs-spring-boot-starter-parent</relativePath>
|
||||
</parent>
|
||||
|
||||
<groupId>gov.irs.directfile.stateapi</groupId>
|
||||
<artifactId>state-api</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>State API</name>
|
||||
<description>state api project to download federal tax return</description>
|
||||
|
||||
<properties>
|
||||
<!-- overrides for properties defined in irs-spring-boot-starter-parent -->
|
||||
<config-folder.path>${project.basedir}/../config</config-folder.path>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
<!-- spring-boot-starter-web required for swagger-ui -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-jose</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
</dependency>
|
||||
<!-- cannot use bcpkix-jdk15on, Bouncy Castle For Java LDAP injection vulnerability -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcpkix-jdk18on</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>sts</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.encryption.s3</groupId>
|
||||
<artifactId>amazon-s3-encryption-client-java</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>r2dbc-postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.logstash.logback</groupId>
|
||||
<artifactId>logstash-logback-encoder</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.r2dbc</groupId>
|
||||
<artifactId>r2dbc-h2</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>mockwebserver</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.spotbugs</groupId>
|
||||
<artifactId>spotbugs-annotations</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>gov.irs.directfile</groupId>
|
||||
<artifactId>data-models</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>gov.irs.boot</groupId>
|
||||
<artifactId>irs-spring-boot-starter-openfeature</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<propertyFile>.liquibase.properties</propertyFile>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<argLine>-XX:+EnableDynamicAgentLoading ${argLine}</argLine>
|
||||
<excludedEnvironmentVariables>
|
||||
<!-- exclude active profiles so tests work from cli even if this var is set -->
|
||||
<excludedEnvironmentVariable>SPRING_PROFILES_ACTIVE</excludedEnvironmentVariable>
|
||||
</excludedEnvironmentVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-site-plugin</artifactId>
|
||||
<!-- Configures mvn site:run to run on port 8485 -->
|
||||
<configuration>
|
||||
<port>8485</port>
|
||||
<tempWebappDirectory>${basedir}/target/site/tempdir</tempWebappDirectory>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<profiles>
|
||||
<profile>development</profile>
|
||||
<profile>debug</profile>
|
||||
</profiles>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -0,0 +1,42 @@
|
|||
package gov.irs.directfile.stateapi;
|
||||
|
||||
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
||||
import io.swagger.v3.oas.annotations.info.Info;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.servers.Server;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
import gov.irs.directfile.stateapi.configuration.CertificationOverrideProperties;
|
||||
import gov.irs.directfile.stateapi.configuration.DirectFileEndpointProperties;
|
||||
import gov.irs.directfile.stateapi.configuration.FeatureFlagsConfigurationProperties;
|
||||
import gov.irs.directfile.stateapi.configuration.S3ConfigurationProperties;
|
||||
import gov.irs.directfile.stateapi.configuration.XmlSanitizedConfigurationProperties;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableCaching
|
||||
@EnableScheduling
|
||||
@EnableConfigurationProperties({
|
||||
FeatureFlagsConfigurationProperties.class,
|
||||
XmlSanitizedConfigurationProperties.class,
|
||||
S3ConfigurationProperties.class,
|
||||
DirectFileEndpointProperties.class,
|
||||
CertificationOverrideProperties.class
|
||||
})
|
||||
@OpenAPIDefinition(
|
||||
info = @Info(title = "State Tax API", description = "The State Tax API", version = "1.0.1"),
|
||||
servers = {
|
||||
@Server(url = "https://df.services.irs.gov", description = "Prod Server"),
|
||||
@Server(url = "https://df.alt.services.irs.gov", description = "ATS Server"),
|
||||
@Server(url = "http://locahost:8081/state-api", description = "Local Development")
|
||||
},
|
||||
security = {@SecurityRequirement(name = "PreauthenticatedHeader")})
|
||||
public class StateApiApp {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(StateApiApp.class, args);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package gov.irs.directfile.stateapi.audit;
|
||||
|
||||
public enum AuditLogElement {
|
||||
cyberOnly,
|
||||
detail,
|
||||
eventErrorMessage,
|
||||
eventId,
|
||||
eventStatus,
|
||||
eventType,
|
||||
stateId,
|
||||
taxReturnId,
|
||||
responseStatusCode,
|
||||
system,
|
||||
taxPeriod,
|
||||
timestamp,
|
||||
userType;
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package gov.irs.directfile.stateapi.audit;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.slf4j.MDC;
|
||||
import org.slf4j.spi.LoggingEventBuilder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import gov.irs.directfile.stateapi.events.Event;
|
||||
import gov.irs.directfile.stateapi.events.EventStatus;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AuditService {
|
||||
|
||||
public void logEvent(Event event) {
|
||||
|
||||
if (event != null) {
|
||||
Map<String, String> contextMapCopy = MDC.getCopyOfContextMap();
|
||||
LoggingEventBuilder loggingEventBuilder =
|
||||
(event.getEventStatus() == EventStatus.FAILURE) ? log.atError() : log.atInfo();
|
||||
|
||||
try {
|
||||
if (event.getEventId() != null) {
|
||||
MDC.put(
|
||||
AuditLogElement.eventId.toString(),
|
||||
event.getEventId().toString());
|
||||
}
|
||||
if (event.getEventStatus() != null) {
|
||||
MDC.put(
|
||||
AuditLogElement.eventStatus.toString(),
|
||||
event.getEventStatus().toString());
|
||||
}
|
||||
if (event.getResponseStatusCode() != null) {
|
||||
MDC.put(AuditLogElement.responseStatusCode.toString(), event.getResponseStatusCode());
|
||||
}
|
||||
if (event.getEventErrorMessage() != null) {
|
||||
MDC.put(AuditLogElement.eventErrorMessage.toString(), event.getEventErrorMessage());
|
||||
}
|
||||
if (event.getTaxPeriod() != null) {
|
||||
MDC.put(AuditLogElement.taxPeriod.toString(), event.getTaxPeriod());
|
||||
}
|
||||
if (event.getStateId() != null) {
|
||||
MDC.put(AuditLogElement.stateId.toString(), event.getStateId());
|
||||
}
|
||||
if (event.getUserType() != null) {
|
||||
MDC.put(AuditLogElement.userType.toString(), event.getUserType());
|
||||
}
|
||||
if (event.getTaxReturnId() != null) {
|
||||
MDC.put(AuditLogElement.taxReturnId.toString(), event.getTaxReturnId());
|
||||
}
|
||||
if (event.getDetail() != null) {
|
||||
loggingEventBuilder = loggingEventBuilder.addKeyValue(
|
||||
AuditLogElement.detail.toString(), event.getDetail().getDetailMap());
|
||||
}
|
||||
|
||||
MDC.put(AuditLogElement.eventType.toString(), "STATE_API");
|
||||
MDC.put(AuditLogElement.cyberOnly.toString(), Boolean.TRUE.toString());
|
||||
|
||||
loggingEventBuilder.log();
|
||||
} finally {
|
||||
// reset the MDC context map
|
||||
MDC.setContextMap(contextMapCopy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package gov.irs.directfile.stateapi.audit;
|
||||
|
||||
import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
|
||||
import ch.qos.logback.classic.spi.IThrowableProxy;
|
||||
import ch.qos.logback.classic.spi.ThrowableProxyUtil;
|
||||
import ch.qos.logback.core.CoreConstants;
|
||||
|
||||
/*
|
||||
* https://logback.qos.ch/apidocs/src-html/ch/qos/logback/classic/pattern/RootCauseFirstThrowableProxyConverter.html
|
||||
*
|
||||
* This is a slightly modified version of the linked class
|
||||
* that omits the exception message from the stack trace.
|
||||
*/
|
||||
|
||||
@SuppressWarnings("PMD.AvoidReassigningParameters")
|
||||
public class NoMessageStackTraceConverter extends ThrowableProxyConverter {
|
||||
@Override
|
||||
protected String throwableProxyToString(IThrowableProxy tp) {
|
||||
StringBuilder buf = new StringBuilder(BUILDER_CAPACITY);
|
||||
recursiveAppendRootCauseFirst(buf, null, ThrowableProxyUtil.REGULAR_EXCEPTION_INDENT, tp);
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
protected void recursiveAppendRootCauseFirst(StringBuilder sb, String prefix, int indent, IThrowableProxy tp) {
|
||||
if (tp.getCause() != null) {
|
||||
recursiveAppendRootCauseFirst(sb, prefix, indent, tp.getCause());
|
||||
prefix = null;
|
||||
}
|
||||
ThrowableProxyUtil.indent(sb, indent - 1);
|
||||
if (prefix != null) {
|
||||
sb.append(prefix);
|
||||
}
|
||||
|
||||
sb.append(tp.getClassName());
|
||||
sb.append(CoreConstants.LINE_SEPARATOR);
|
||||
subjoinSTEPArray(sb, indent, tp);
|
||||
IThrowableProxy[] suppressed = tp.getSuppressed();
|
||||
if (suppressed != null) {
|
||||
for (IThrowableProxy current : suppressed) {
|
||||
recursiveAppendRootCauseFirst(
|
||||
sb, CoreConstants.SUPPRESSED, indent + ThrowableProxyUtil.SUPPRESSED_EXCEPTION_INDENT, current);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package gov.irs.directfile.stateapi.authorization;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record AuthorizationTokenClaims(String taxReturnUuid, String submissionId, String stateCode, int taxYear) {}
|
|
@ -0,0 +1,77 @@
|
|||
package gov.irs.directfile.stateapi.authorization;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nimbusds.jose.*;
|
||||
import com.nimbusds.jose.crypto.MACSigner;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import gov.irs.directfile.models.encryption.DataEncryptDecrypt;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AuthorizationTokenService {
|
||||
private final DataEncryptDecrypt dataEncryptDecrypt;
|
||||
public static final String EXPORT_CLAIM_KEY = "tax-return-export-metadata";
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
private final int authorizationCodeExpiresInterval;
|
||||
private final String signingKey;
|
||||
|
||||
public AuthorizationTokenService(
|
||||
DataEncryptDecrypt dataEncryptDecrypt,
|
||||
@Value("${authorization-token.signing-key}") String signingKey,
|
||||
@Value("${authorization-code.expires-interval-seconds: 600}") int authorizationCodeExpiresInterval) {
|
||||
this.dataEncryptDecrypt = dataEncryptDecrypt;
|
||||
this.signingKey = signingKey;
|
||||
this.authorizationCodeExpiresInterval = authorizationCodeExpiresInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JWT (json web token) containing tax return export metadata which is first signed and then encrypted.
|
||||
*/
|
||||
public Mono<String> generateAndEncrypt(AuthorizationTokenClaims claims) {
|
||||
return Mono.fromCallable(() -> {
|
||||
JWSSigner signer = new MACSigner(signingKey);
|
||||
Instant issuedAt = Instant.now();
|
||||
JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder()
|
||||
.claim(EXPORT_CLAIM_KEY, claims)
|
||||
.issueTime(Date.from(issuedAt))
|
||||
.expirationTime(Date.from(issuedAt.plusSeconds(authorizationCodeExpiresInterval)))
|
||||
.build();
|
||||
// create and sign a JWS (json web signature) containing the
|
||||
// claims as the payload
|
||||
JWSObject jwsObject = new JWSObject(
|
||||
new JWSHeader.Builder(JWSAlgorithm.HS256).build(), new Payload(jwtClaimsSet.toJSONObject()));
|
||||
jwsObject.sign(signer);
|
||||
// encrypt the token with kms encryption sdk
|
||||
return encryptToken(signedJWSToBytes(jwsObject.serialize()));
|
||||
});
|
||||
}
|
||||
|
||||
private byte[] signedJWSToBytes(String serializedJws) throws JsonProcessingException {
|
||||
// break the serialized JWSObject into its period-separated parts
|
||||
// before converting to byte array. We must do this prior to encryption to preserve
|
||||
// the separate parts of the token (header, signature, and payload)
|
||||
String[] serializedJWSParts = serializedJws.split("\\.");
|
||||
SignedJWSParts jwsParts =
|
||||
new SignedJWSParts(serializedJWSParts[0], serializedJWSParts[1], serializedJWSParts[2]);
|
||||
|
||||
return mapper.writeValueAsBytes(jwsParts);
|
||||
}
|
||||
|
||||
private String encryptToken(byte[] claims) {
|
||||
Map<String, String> encryptionContext = new HashMap<>();
|
||||
encryptionContext.put("system", "DIRECT-FILE");
|
||||
encryptionContext.put("type", "STATE-API");
|
||||
|
||||
byte[] ciphertext = dataEncryptDecrypt.encrypt(claims, encryptionContext);
|
||||
return Base64.getUrlEncoder().encodeToString(ciphertext);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package gov.irs.directfile.stateapi.authorization;
|
||||
|
||||
public record SignedJWSParts(String s1, String s2, String s3) {}
|
|
@ -0,0 +1,31 @@
|
|||
package gov.irs.directfile.stateapi.configuration;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
|
||||
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
|
||||
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(S3ConfigurationProperties.class)
|
||||
public class AwsCredentialsConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(
|
||||
name = "aws.s3.default-credentials-provider-chain-enabled",
|
||||
havingValue = "false",
|
||||
matchIfMissing = true)
|
||||
public AwsCredentialsProvider staticCredentialsProvider(S3ConfigurationProperties s3ConfigurationProperties) {
|
||||
return StaticCredentialsProvider.create(AwsBasicCredentials.create(
|
||||
s3ConfigurationProperties.getAccessKey(), s3ConfigurationProperties.getSecretKey()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "aws.s3.default-credentials-provider-chain-enabled", havingValue = "true")
|
||||
public AwsCredentialsProvider defaultAWSCredentialsProvider() {
|
||||
return DefaultCredentialsProvider.create();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package gov.irs.directfile.stateapi.configuration;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.caffeine.CaffeineCacheManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/***
|
||||
* Configures the cache used by Spring @Cacheable methods, such as those found in the CachedDataService
|
||||
*/
|
||||
@Configuration
|
||||
public class CacheConfiguration {
|
||||
|
||||
@Value("${spring.cache.TTL-minutes: 120}")
|
||||
private long cacheTTL;
|
||||
|
||||
@Bean
|
||||
public Caffeine<Object, Object> caffeineConfig() {
|
||||
return Caffeine.newBuilder()
|
||||
.expireAfterWrite(cacheTTL, TimeUnit.MINUTES)
|
||||
.maximumSize(100);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CacheManager cacheManager(Caffeine<Object, Object> caffeine) {
|
||||
var caffeineCacheManager = new CaffeineCacheManager();
|
||||
caffeineCacheManager.setCaffeine(caffeine);
|
||||
caffeineCacheManager.setCacheNames(List.of("publicKeyCache", "stateProfileCache"));
|
||||
caffeineCacheManager.setAsyncCacheMode(true);
|
||||
return caffeineCacheManager;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package gov.irs.directfile.stateapi.configuration;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
@Validated
|
||||
@ConfigurationProperties(prefix = "direct-file")
|
||||
public class CertificationOverrideProperties {
|
||||
private final String certLocationOverride;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package gov.irs.directfile.stateapi.configuration;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
@Validated
|
||||
@ConfigurationProperties(prefix = "direct-file")
|
||||
public class DirectFileEndpointProperties {
|
||||
@NonNull private String backendUrl;
|
||||
|
||||
@NonNull private String backendContextPath;
|
||||
|
||||
@NonNull private String backendApiVersion;
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package gov.irs.directfile.stateapi.configuration;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.NonNull;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.kms.KmsClient;
|
||||
import software.amazon.encryption.s3.materials.CryptographicMaterialsManager;
|
||||
import software.amazon.encryption.s3.materials.DefaultCryptoMaterialsManager;
|
||||
import software.amazon.encryption.s3.materials.KmsKeyring;
|
||||
|
||||
@Configuration
|
||||
@AllArgsConstructor
|
||||
@EnableConfigurationProperties({S3ConfigurationProperties.class, EncryptionConfigurationProperties.class})
|
||||
public class EncryptionClientConfiguration {
|
||||
private final EncryptionConfigurationProperties encryptionConfig;
|
||||
private final S3ConfigurationProperties s3ConfigurationProperties;
|
||||
private final AwsCredentialsProvider awsCredentialsProvider;
|
||||
|
||||
public CryptographicMaterialsManager kmsCrypto(@NonNull String kmsWrappingKeyArn) {
|
||||
return DefaultCryptoMaterialsManager.builder()
|
||||
.keyring(KmsKeyring.builder()
|
||||
.kmsClient(regionalKmsClient())
|
||||
.wrappingKeyId(kmsWrappingKeyArn)
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
public SecretKey getLocalAesWrappingKey() {
|
||||
return new SecretKeySpec(Base64.getDecoder().decode(encryptionConfig.getLocalWrappingKey()), "AES");
|
||||
}
|
||||
|
||||
public String getS3KmsWrappingKeyArn() {
|
||||
return encryptionConfig.getS3KmsWrappingKeyArn();
|
||||
}
|
||||
|
||||
protected KmsClient regionalKmsClient() {
|
||||
return KmsClient.builder()
|
||||
.region(Region.of(encryptionConfig.getRegion()))
|
||||
.credentialsProvider(awsCredentialsProvider)
|
||||
.endpointOverride(URI.create(encryptionConfig.getKmsEndpoint()))
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package gov.irs.directfile.stateapi.configuration;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@ConfigurationProperties
|
||||
public class EncryptionConfigurationProperties {
|
||||
@Value("${aws.kmsEndpoint:#{null}}")
|
||||
private String kmsEndpoint;
|
||||
|
||||
@Value("${aws.region:#{null}}")
|
||||
private String region;
|
||||
|
||||
@Value("${direct-file.local-encryption.local-wrapping-key:#{null}}")
|
||||
private String localWrappingKey;
|
||||
|
||||
@Value("${aws.s3.s3-kms-wrapping-key-arn:#{null}}")
|
||||
private String s3KmsWrappingKeyArn;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package gov.irs.directfile.stateapi.configuration;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@ConfigurationProperties(prefix = "feature-flags")
|
||||
public class FeatureFlagsConfigurationProperties {
|
||||
private final ExportReturnFlags exportReturn;
|
||||
|
||||
public interface Toggleable {
|
||||
boolean isEnabled();
|
||||
}
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public static class ExportReturnFlags implements Toggleable {
|
||||
private boolean enabled;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package gov.irs.directfile.stateapi.configuration;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
|
||||
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.s3.S3AsyncClient;
|
||||
import software.amazon.awssdk.services.s3.S3AsyncClientBuilder;
|
||||
import software.amazon.awssdk.services.sts.StsClient;
|
||||
import software.amazon.awssdk.services.sts.auth.StsAssumeRoleCredentialsProvider;
|
||||
import software.amazon.awssdk.services.sts.model.AssumeRoleRequest;
|
||||
import software.amazon.encryption.s3.S3AsyncEncryptionClient;
|
||||
|
||||
@Configuration
|
||||
@AllArgsConstructor
|
||||
public class S3ClientConfiguration {
|
||||
private final EncryptionClientConfiguration encryptionClientConfiguration;
|
||||
private final S3ConfigurationProperties s3ConfigurationProperties;
|
||||
private final AwsCredentialsProvider awsCredentialsProvider;
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
S3AsyncClient getS3AsyncClient() {
|
||||
return baseS3Client(s3ConfigurationProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Profile("aws")
|
||||
public S3AsyncEncryptionClient s3AsyncEncryptionClient() {
|
||||
return S3AsyncEncryptionClient.builder()
|
||||
.wrappedClient(baseS3Client(s3ConfigurationProperties))
|
||||
.cryptoMaterialsManager(
|
||||
encryptionClientConfiguration.kmsCrypto(encryptionClientConfiguration.getS3KmsWrappingKeyArn()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Profile("!aws")
|
||||
public S3AsyncEncryptionClient localS3AsyncEncryptionClient() {
|
||||
return S3AsyncEncryptionClient.builder()
|
||||
.wrappedClient(baseS3Client(s3ConfigurationProperties))
|
||||
.aesKey(encryptionClientConfiguration.getLocalAesWrappingKey())
|
||||
.build();
|
||||
}
|
||||
|
||||
private S3AsyncClient baseS3Client(S3ConfigurationProperties s3ConfigurationProperties) {
|
||||
S3AsyncClientBuilder builder = S3AsyncClient.builder().region(Region.of(s3ConfigurationProperties.getRegion()));
|
||||
|
||||
if (s3ConfigurationProperties.getAssumeRoleArn() != null
|
||||
&& StringUtils.isNotBlank(s3ConfigurationProperties.getAssumeRoleArn())) {
|
||||
builder.credentialsProvider(StsAssumeRoleCredentialsProvider.builder()
|
||||
.stsClient(StsClient.builder()
|
||||
.region(Region.of(s3ConfigurationProperties.getRegion()))
|
||||
.credentialsProvider(awsCredentialsProvider)
|
||||
.build())
|
||||
.refreshRequest(AssumeRoleRequest.builder()
|
||||
.roleArn(s3ConfigurationProperties.getAssumeRoleArn())
|
||||
.roleSessionName(s3ConfigurationProperties.getAssumeRoleSessionName())
|
||||
.durationSeconds(Integer.parseInt(s3ConfigurationProperties.getAssumeRoleDurationSeconds()))
|
||||
.build())
|
||||
.build());
|
||||
} else {
|
||||
builder.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(
|
||||
s3ConfigurationProperties.getAccessKey(), s3ConfigurationProperties.getSecretKey())));
|
||||
}
|
||||
|
||||
if (s3ConfigurationProperties.getEndPoint() != null
|
||||
&& StringUtils.isNotBlank(s3ConfigurationProperties.getEndPoint())) {
|
||||
builder.endpointOverride(URI.create(s3ConfigurationProperties.getEndPoint()))
|
||||
.forcePathStyle(true); // <-- this fixes the UnknonwnHost issue with localstack
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package gov.irs.directfile.stateapi.configuration;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@Validated
|
||||
@ConfigurationProperties(prefix = "aws.s3")
|
||||
public class S3ConfigurationProperties {
|
||||
private String accessKey;
|
||||
private String secretKey;
|
||||
private String region;
|
||||
private String assumeRoleArn;
|
||||
private String assumeRoleDurationSeconds;
|
||||
private String assumeRoleSessionName;
|
||||
private String endPoint;
|
||||
private String certBucketName;
|
||||
private String taxReturnXmlBucketName;
|
||||
private String prefix;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package gov.irs.directfile.stateapi.configuration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
@ConfigurationProperties(prefix = "xml-sanitized")
|
||||
public class XmlSanitizedConfigurationProperties {
|
||||
private final List<String> allowedHeaders;
|
||||
private final List<String> excludedTags;
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
package gov.irs.directfile.stateapi.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.ExampleObject;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import gov.irs.directfile.dto.AuthCodeResponse;
|
||||
import gov.irs.directfile.stateapi.model.AuthCodeRequest;
|
||||
import gov.irs.directfile.stateapi.model.ExportResponse;
|
||||
|
||||
@Tag(name = "State-Tax-API", description = "Direct File State Tax APIs")
|
||||
@SuppressWarnings("PMD.UnnecessaryModifier")
|
||||
public interface StateApi {
|
||||
@Operation(
|
||||
summary = "Create authorization code",
|
||||
description =
|
||||
"Create an authorization code for the given tax return UUID. This API will be called from Direct File service.")
|
||||
@ApiResponses({
|
||||
@ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "Authorization code created",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "text/plain",
|
||||
schema = @Schema(implementation = String.class),
|
||||
examples =
|
||||
@ExampleObject(
|
||||
name = "authorization code example",
|
||||
value = "cd19876a-328c-4173-b4e6-59b55f4bb99e"))),
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description =
|
||||
"Error occurred to create authorization code. For tax return not accepted, error message is E_TAX_RETURN_NOT_ACCEPTED; for tax return not found, error message is E_TAX_RETURN_NOT_FOUND",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType = "text/plain",
|
||||
examples = {
|
||||
@ExampleObject(
|
||||
name = "Internal server error",
|
||||
value = "An internal server error occurred.")
|
||||
})
|
||||
})
|
||||
})
|
||||
Mono<ResponseEntity<AuthCodeResponse>> createAuthorizationCode(
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
description = "Request payload for creating the authorization code",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = AuthCodeRequest.class),
|
||||
examples = @ExampleObject(value = AuthCodeRequest.docsExampleObject)))
|
||||
@Valid
|
||||
@RequestBody
|
||||
AuthCodeRequest ac,
|
||||
HttpServletRequest request);
|
||||
|
||||
@Operation(
|
||||
summary = "Export tax return",
|
||||
description =
|
||||
"Export an encrypted tax return for a particular taxpayer identified by the authorization code passed in the JWT Bearer token. If the operation is successful, it will return a status of 200. In case of an error, it will also return a status of 200 along with an error code,"
|
||||
+ "e.g. {\"status\": \"error\", \"error\": \"E_CERTIFICATE_EXPIRED\"}. This API will be called from state-app.")
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "export taxpayer's federal tax return",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ExportResponse.class),
|
||||
examples = @ExampleObject(value = ExportResponse.docsExampleObjectSuccess))
|
||||
})
|
||||
public Mono<ResponseEntity<ExportResponse>> exportReturn(
|
||||
@Parameter(
|
||||
description = "Authorization header",
|
||||
required = true,
|
||||
examples = {@ExampleObject(name = "JWT Bearer token", value = "Bearer token")})
|
||||
@RequestHeader("Authorization")
|
||||
String authorizationHeader,
|
||||
HttpServletRequest request);
|
||||
}
|
|
@ -0,0 +1,293 @@
|
|||
package gov.irs.directfile.stateapi.controller;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import dev.openfeature.sdk.Client;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import gov.irs.directfile.dto.AuthCodeResponse;
|
||||
import gov.irs.directfile.error.StateApiErrorCode;
|
||||
import gov.irs.directfile.stateapi.dto.StateProfileDTO;
|
||||
import gov.irs.directfile.stateapi.encryption.JwtVerifier;
|
||||
import gov.irs.directfile.stateapi.exception.StateApiException;
|
||||
import gov.irs.directfile.stateapi.exception.StateNotExistException;
|
||||
import gov.irs.directfile.stateapi.model.*;
|
||||
import gov.irs.directfile.stateapi.service.StateApiService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/state-api")
|
||||
@Validated
|
||||
@Slf4j
|
||||
@SuppressWarnings({"PMD.MissingOverride", "PMD.ExcessiveParameterList"})
|
||||
@RequiredArgsConstructor
|
||||
public class StateApiController implements StateApi {
|
||||
public static final String SESSION_KEY = "SESSION-KEY";
|
||||
public static final String INITIALIZATION_VECTOR = "INITIALIZATION-VECTOR";
|
||||
public static final String AUTHENTICATION_TAG = "AUTHENTICATION-TAG";
|
||||
|
||||
private final StateApiService svc;
|
||||
private final Client featureFlagClient;
|
||||
|
||||
private static final String X_HEADER = "x-header";
|
||||
|
||||
@PostMapping("/authorization-code")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public Mono<ResponseEntity<AuthCodeResponse>> createAuthorizationCode(
|
||||
@Valid @RequestBody AuthCodeRequest ac, HttpServletRequest request) {
|
||||
|
||||
log.info("enter createAuthorizationCode()...");
|
||||
|
||||
String taxReturnId = ac.getTaxReturnUuid().toString();
|
||||
String submissionId = ac.getSubmissionId();
|
||||
|
||||
return svc.createAuthorizationCode(ac)
|
||||
.map(uuid -> ResponseEntity.ok(
|
||||
AuthCodeResponse.builder().authCode(uuid).build()))
|
||||
.doOnSuccess(response -> {
|
||||
log.info(
|
||||
"createAuthorizationCode() succeeded for taxYear: {}, stateCode: {}, taxReturnId: {}",
|
||||
ac.getTaxYear(),
|
||||
ac.getStateCode(),
|
||||
taxReturnId);
|
||||
})
|
||||
.onErrorResume(e -> {
|
||||
log.error(
|
||||
"createAuthorizationCode() failed for taxYear: {}, stateCode: {}, taxReturnId: {}, submissionId: {}, {}, error: {}",
|
||||
ac.getTaxYear(),
|
||||
ac.getStateCode(),
|
||||
taxReturnId,
|
||||
submissionId,
|
||||
e.getClass().getName(),
|
||||
e.getMessage());
|
||||
if (e instanceof StateApiException stateApiException) {
|
||||
// TODO: Http Status should be dependent on the error code. For now, since
|
||||
// StateApiExceptionHandler previously returned 500 for any unhandled exception, this keeps the
|
||||
// status the same
|
||||
return Mono.just(ResponseEntity.internalServerError()
|
||||
.body(AuthCodeResponse.builder()
|
||||
.errorCode(stateApiException.getErrorCode())
|
||||
.build()));
|
||||
} else {
|
||||
return Mono.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
**
|
||||
* Prerequisite:**
|
||||
* The state will generate a key pair and send the public key (e.g., AZ.cer,
|
||||
* NY.cer) to the IRS.
|
||||
*
|
||||
* DF State-Tax-Api (Server):**
|
||||
* 1. Generate a secret (32 bytes).
|
||||
* 2. Generate an initialization vector (IV) value (12 bytes).
|
||||
* 3. Encrypt the tax return XML with the secret from step 1 and the IV from
|
||||
* step 2 using the AES-256 GCM algorithm (plus Authentication Tag).
|
||||
* 4. Base64 encode the encrypted tax return XML from step 3.
|
||||
* 5. Encrypt the 32-byte secret using the public key provided by the state
|
||||
* (RSA).
|
||||
* 6. Base64 encode the encrypted secret from step 5.
|
||||
* 7. Base64 encode the IV from step 2.
|
||||
* 8. Base64 encode the Authentication Tag from step 3.
|
||||
* 9. Send HTTP response to the State-App with:
|
||||
* a) Header: SESSION-KEY=value from step 6
|
||||
* b) Header: INITIALIZATION_VECTOR=value from step 7
|
||||
* c) Header: AUTHENTICATION-TAG=value from step 8
|
||||
* d) Body: value from step 4.
|
||||
*
|
||||
* State-App Side (Client):**
|
||||
* 1. Extract SESSION-KEY from the HTTP response header.
|
||||
* 2. Base64 decode the SESSION-KEY.
|
||||
* 3. Decrypt the decoded SESSION-KEY with its private key (RSA) (in byte[]).
|
||||
* 4. Extract INITIALIZATION_VECTOR (IV) from the HTTP response header.
|
||||
* 5. Base64 decode the IV.
|
||||
* 6. Extract AUTHENTICATION_TAG from the HTTP response header.
|
||||
* 7. Base64 decode AUTHENTICATION_TAG.
|
||||
* 6. Decrypt the XML data from the response body using the secret from step 3,
|
||||
* the IV from step 5 using the AES-256 GCM algorithm, verify with
|
||||
* Authentication Tag from step 7.
|
||||
*
|
||||
* @param authorizationHeader
|
||||
* the JWT Bearer token
|
||||
* @return the encrypted and BASE 64 encoded tax return xml along with two
|
||||
* headers in HTTP Response: SESSION-KEY, INITIALIZATION-VECTOR and
|
||||
* AUTHENTICATION-TAG.
|
||||
* http status code: 200 for all responses.
|
||||
*
|
||||
* if success, response as:
|
||||
* {"status": "success", "taxReturn": "encoded-encrypted-data"}
|
||||
* (taxReturn includes return status and xml data, status can be
|
||||
* "accepted", "rejected", "pending". Sample value:
|
||||
* {"status":"accepted", "xml":"return-data"})
|
||||
* if failed, response as:
|
||||
* {"status": "error", "error": "E_AUTHORIZATION_CODE_EXPIRED"}
|
||||
*/
|
||||
@GetMapping("/export-return")
|
||||
public Mono<ResponseEntity<ExportResponse>> exportReturn(
|
||||
@RequestHeader("Authorization") String authorizationHeader, HttpServletRequest request) {
|
||||
|
||||
log.info("Enter exportReturn()...");
|
||||
|
||||
AtomicInteger taxYear = new AtomicInteger();
|
||||
AtomicReference<String> stateCode = new AtomicReference<>();
|
||||
stateCode.set(getStateCode(request));
|
||||
AtomicReference<String> taxReturnId = new AtomicReference<>();
|
||||
AtomicReference<String> submissionId = new AtomicReference<>();
|
||||
taxReturnId.set("");
|
||||
submissionId.set("");
|
||||
|
||||
if (Boolean.FALSE.equals(this.featureFlagClient.getBooleanValue("export-return", Boolean.TRUE))) {
|
||||
log.info("Export return is disabled via configuration property. No action taken");
|
||||
return Mono.just(ResponseEntity.ok(handleErrors(
|
||||
null,
|
||||
taxYear.get(),
|
||||
stateCode.get(),
|
||||
taxReturnId.get(),
|
||||
submissionId.get(),
|
||||
new StateApiException(StateApiErrorCode.E_STATE_API_DISABLED))));
|
||||
}
|
||||
|
||||
String prefix = "Bearer ";
|
||||
|
||||
if (!authorizationHeader.startsWith(prefix)) {
|
||||
return Mono.just(ResponseEntity.ok(handleErrors(
|
||||
null,
|
||||
taxYear.get(),
|
||||
stateCode.get(),
|
||||
taxReturnId.get(),
|
||||
submissionId.get(),
|
||||
new StateApiException(StateApiErrorCode.E_BEARER_TOKEN_MISSING))));
|
||||
}
|
||||
|
||||
String jwtToken = authorizationHeader.substring(prefix.length());
|
||||
|
||||
final String accountId;
|
||||
|
||||
try {
|
||||
accountId = JwtVerifier.getAccountId(jwtToken);
|
||||
} catch (StateApiException e) {
|
||||
return Mono.just(ResponseEntity.ok(
|
||||
handleErrors(null, taxYear.get(), stateCode.get(), taxReturnId.get(), submissionId.get(), e)));
|
||||
}
|
||||
|
||||
return svc.verifyJwtSignature(jwtToken, accountId)
|
||||
.flatMap(saCode -> {
|
||||
stateCode.set(saCode.getStateCode());
|
||||
return svc.authorize(saCode).flatMap(entity -> {
|
||||
taxReturnId.set(entity.getTaxReturnUuid().toString());
|
||||
taxYear.set(entity.getTaxYear());
|
||||
return svc.retrieveTaxReturnXml(
|
||||
taxYear.get(), entity.getTaxReturnUuid(), entity.getSubmissionId())
|
||||
.flatMap(trXml -> {
|
||||
submissionId.set(trXml.submissionId());
|
||||
return svc.retrieveExportedFacts(
|
||||
entity.getSubmissionId(), stateCode.get(), accountId)
|
||||
.flatMap(exportedFacts -> {
|
||||
TaxReturnToExport trExpt = new TaxReturnToExport(
|
||||
trXml.status(),
|
||||
trXml.submissionId(),
|
||||
trXml.xml(),
|
||||
exportedFacts);
|
||||
return svc.encryptTaxReturn(trExpt, accountId)
|
||||
.map(ed -> ResponseEntity.ok()
|
||||
.header(SESSION_KEY, ed.encodedSecret())
|
||||
.header(INITIALIZATION_VECTOR, ed.encodedIV())
|
||||
.header(
|
||||
AUTHENTICATION_TAG,
|
||||
ed.encodedAuthenticationTag())
|
||||
.body(ExportResponse.builder()
|
||||
.status("success")
|
||||
.taxReturn(ed.encodedAndEncryptedData())
|
||||
.build()));
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.doOnSuccess(export -> {
|
||||
log.info(
|
||||
"exportReturn succeeded for taxYear: {}, accountId: {}, stateCode: {}, taxReturnId: {}, submissionId: {}",
|
||||
taxYear.get(),
|
||||
accountId,
|
||||
stateCode.get(),
|
||||
taxReturnId.get(),
|
||||
submissionId.get());
|
||||
})
|
||||
.onErrorResume(e -> Mono.just(ResponseEntity.ok(handleErrors(
|
||||
accountId, taxYear.get(), stateCode.get(), taxReturnId.get(), submissionId.get(), e))));
|
||||
}
|
||||
|
||||
@GetMapping("/state-profile")
|
||||
public Mono<ResponseEntity<StateProfileDTO>> getStateProfile(
|
||||
@RequestParam String stateCode, HttpServletRequest request) {
|
||||
|
||||
log.info("enter getStateProfile()...");
|
||||
|
||||
return svc.lookupStateProfile(stateCode)
|
||||
.map(ResponseEntity::ok)
|
||||
.doOnSuccess(export -> {
|
||||
log.info("getStateProfile completes successfully for stateCode {}", stateCode);
|
||||
})
|
||||
.onErrorResume(StateNotExistException.class, e -> {
|
||||
log.info("No state profile found for {}, {}", stateCode, e.getMessage());
|
||||
return Mono.just(ResponseEntity.noContent().build());
|
||||
})
|
||||
.onErrorResume(Mono::error);
|
||||
}
|
||||
|
||||
private ExportResponse handleErrors(
|
||||
String accountId, int taxYear, String stateCode, String taxReturnId, String submissionId, Throwable t) {
|
||||
StateApiErrorCode errorCode;
|
||||
|
||||
log.error(
|
||||
"exportReturn failed for taxYear: {}, accountId: {}, stateCode: {}, taxReturnId: {}, submissionId: {}, {}, error: {}",
|
||||
taxYear,
|
||||
accountId,
|
||||
stateCode,
|
||||
taxReturnId,
|
||||
submissionId,
|
||||
t.getClass().getName(),
|
||||
t.getMessage());
|
||||
|
||||
if (t instanceof StateApiException e) {
|
||||
// Hide specific error codes from the client, return internal server error
|
||||
// instead
|
||||
if (e.getErrorCode() == StateApiErrorCode.E_CERTIFICATE_NOT_FOUND
|
||||
|| e.getErrorCode() == StateApiErrorCode.E_TAX_RETURN_NOT_FOUND) {
|
||||
errorCode = StateApiErrorCode.E_INTERNAL_SERVER_ERROR;
|
||||
} else if (e.getErrorCode() == StateApiErrorCode.E_EXPORTED_FACTS_DISABLED) {
|
||||
errorCode = StateApiErrorCode.E_INTERNAL_SERVER_ERROR;
|
||||
log.info("{}: {}", e.getErrorCode(), e.getMessage());
|
||||
} else {
|
||||
errorCode = e.getErrorCode();
|
||||
}
|
||||
} else {
|
||||
errorCode = StateApiErrorCode.E_INTERNAL_SERVER_ERROR;
|
||||
}
|
||||
|
||||
return ExportResponse.builder().status("error").error(errorCode.name()).build();
|
||||
}
|
||||
|
||||
private String getStateCode(HttpServletRequest request) {
|
||||
|
||||
String stateInfo = request.getHeader(X_HEADER);
|
||||
String stateCd = "";
|
||||
// if (stateInfo != null && !stateInfo.isEmpty()) {
|
||||
if (StringUtils.isNotBlank(stateInfo)) {
|
||||
stateCd = stateInfo.substring(0, 2);
|
||||
} else {
|
||||
log.info(X_HEADER + " does not exist or is empty");
|
||||
}
|
||||
return stateCd;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package gov.irs.directfile.stateapi.dto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import gov.irs.directfile.stateapi.model.StateLanguage;
|
||||
import gov.irs.directfile.stateapi.model.StateProfile;
|
||||
import gov.irs.directfile.stateapi.model.StateRedirect;
|
||||
|
||||
public record StateProfileDTO(
|
||||
String stateCode,
|
||||
String taxSystemName,
|
||||
String landingUrl,
|
||||
String defaultRedirectUrl,
|
||||
String departmentOfRevenueUrl,
|
||||
String filingRequirementsUrl,
|
||||
String transferCancelUrl,
|
||||
String waitingForAcceptanceCancelUrl,
|
||||
List<String> redirectUrls,
|
||||
Map<String, String> languages,
|
||||
Boolean acceptedOnly,
|
||||
String customFilingDeadline,
|
||||
Boolean archived) {
|
||||
|
||||
public StateProfileDTO(StateProfile stateProfile) {
|
||||
this(stateProfile, new ArrayList<>(), new ArrayList<>());
|
||||
}
|
||||
|
||||
public StateProfileDTO(
|
||||
StateProfile stateProfile, List<StateRedirect> stateRedirects, List<StateLanguage> stateLanguages) {
|
||||
this(
|
||||
stateProfile.getStateCode(),
|
||||
stateProfile.getTaxSystemName(),
|
||||
stateProfile.getLandingUrl(),
|
||||
stateProfile.getDefaultRedirectUrl(),
|
||||
stateProfile.getDepartmentOfRevenueUrl(),
|
||||
stateProfile.getFilingRequirementsUrl(),
|
||||
stateProfile.getTransferCancelUrl(),
|
||||
stateProfile.getWaitingForAcceptanceCancelUrl(),
|
||||
stateRedirects.stream().map(StateRedirect::getRedirectUrl).toList(),
|
||||
stateLanguages.stream()
|
||||
.collect(Collectors.toMap(
|
||||
StateLanguage::getDfLanguageCode, StateLanguage::getStateLanguageCode)),
|
||||
stateProfile.getAcceptedOnly(),
|
||||
stateProfile.getCustomFilingDeadline(),
|
||||
stateProfile.getArchived());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package gov.irs.directfile.stateapi.encryption;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.bouncycastle.crypto.InvalidCipherTextException;
|
||||
import org.bouncycastle.crypto.engines.AESEngine;
|
||||
import org.bouncycastle.crypto.modes.GCMBlockCipher;
|
||||
import org.bouncycastle.crypto.modes.GCMModeCipher;
|
||||
import org.bouncycastle.crypto.params.AEADParameters;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
|
||||
/**
|
||||
* This class contains sample code for decrypting the tax return, which will be shared with the client to assist in the decryption process.
|
||||
*/
|
||||
@SuppressWarnings("PMD.ReturnEmptyCollectionRatherThanNull")
|
||||
public class DecryptionDemoCode {
|
||||
static int AES256_GCM_SECRET_LENGTH = 32;
|
||||
static int AES256_GCM_IV_LENGTH = 12;
|
||||
|
||||
/**
|
||||
* Decrypt data using AES 256 GCM.
|
||||
* @param ciphertext the data encrypted
|
||||
* @param encryptionKey the encryption secret
|
||||
* @param iv the initialization vector
|
||||
* @param authenticationTag the authentication tag
|
||||
* @return encrypted data in byte array or null if authentication tag is invalid
|
||||
* @throws InvalidCipherTextException
|
||||
*/
|
||||
public static byte[] aesGcmDecrypt(byte[] ciphertext, byte[] encryptionKey, byte[] iv, byte[] authenticationTag)
|
||||
throws InvalidCipherTextException {
|
||||
assert encryptionKey.length == AES256_GCM_SECRET_LENGTH
|
||||
: "AES 256 secret is not " + AES256_GCM_SECRET_LENGTH + " bytes";
|
||||
assert iv.length == AES256_GCM_IV_LENGTH : "AES 256 IV is not " + AES256_GCM_IV_LENGTH + " bytes";
|
||||
|
||||
// Create AES-GCM block cipher
|
||||
GCMModeCipher gcmCipher = GCMBlockCipher.newInstance(AESEngine.newInstance());
|
||||
|
||||
// Initialize the cipher for decryption
|
||||
gcmCipher.init(false, new AEADParameters(new KeyParameter(encryptionKey), 128, iv));
|
||||
|
||||
byte[] decrypted = new byte[gcmCipher.getOutputSize(ciphertext.length)];
|
||||
|
||||
int len = gcmCipher.processBytes(ciphertext, 0, ciphertext.length, decrypted, 0);
|
||||
|
||||
gcmCipher.doFinal(decrypted, len);
|
||||
|
||||
// To verify the authentication tag, compare it to the original authentication tag
|
||||
if (Arrays.equals(authenticationTag, gcmCipher.getMac())) {
|
||||
return decrypted;
|
||||
} else {
|
||||
return null; // Authentication tag verification failed
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package gov.irs.directfile.stateapi.encryption;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.bouncycastle.crypto.InvalidCipherTextException;
|
||||
import org.bouncycastle.crypto.engines.AESEngine;
|
||||
import org.bouncycastle.crypto.modes.GCMBlockCipher;
|
||||
import org.bouncycastle.crypto.modes.GCMModeCipher;
|
||||
import org.bouncycastle.crypto.params.AEADParameters;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
|
||||
import gov.irs.directfile.stateapi.model.AesGcmEncryptionResult;
|
||||
|
||||
@SuppressWarnings("PMD.SignatureDeclareThrowsException")
|
||||
public class Encryptor {
|
||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||
static int AES256_GCM_SECRET_LENGTH = 32;
|
||||
static int AES256_GCM_IV_LENGTH = 12;
|
||||
|
||||
public static byte[] generatePassword() throws NoSuchAlgorithmException {
|
||||
// Create a KeyGenerator for AES (Advanced Encryption Standard) with a 256-bit
|
||||
// key size
|
||||
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
|
||||
keyGenerator.init(AES256_GCM_SECRET_LENGTH * 8); // 256 bits = 32 bytes
|
||||
|
||||
// Generate the secret key
|
||||
SecretKey secretKey = keyGenerator.generateKey();
|
||||
|
||||
// Get the encoded bytes of the secret key
|
||||
return secretKey.getEncoded();
|
||||
}
|
||||
|
||||
public static byte[] generateIV() {
|
||||
// AES-256 GCM requires 12 bytes initialization vector
|
||||
byte[] iv = new byte[AES256_GCM_IV_LENGTH];
|
||||
|
||||
// Generate random bytes for the IV
|
||||
SECURE_RANDOM.nextBytes(iv);
|
||||
return iv;
|
||||
}
|
||||
|
||||
public static byte[] rsaEncryptWithPublicKey(byte[] secret, PublicKey publicKey) throws Exception {
|
||||
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||
|
||||
return cipher.doFinal(secret);
|
||||
}
|
||||
|
||||
public static AesGcmEncryptionResult aesGcmEncrypt(String plainText, byte[] aesKey, byte[] aesIV)
|
||||
throws InvalidCipherTextException {
|
||||
assert aesKey.length == AES256_GCM_SECRET_LENGTH
|
||||
: "AES 256 secret is not " + AES256_GCM_SECRET_LENGTH + " bytes";
|
||||
assert aesIV.length == AES256_GCM_IV_LENGTH : "AES 256 IV is not " + AES256_GCM_IV_LENGTH + " bytes";
|
||||
|
||||
// Plain text to be encrypted
|
||||
byte[] plaintextBytes = plainText.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// Create AES-GCM block cipher
|
||||
GCMModeCipher gcmCipher = GCMBlockCipher.newInstance(AESEngine.newInstance());
|
||||
|
||||
// Initialize with encryption mode, AES-256 key, and IV
|
||||
gcmCipher.init(true, new AEADParameters(new KeyParameter(aesKey), 128, aesIV));
|
||||
|
||||
// Create a byte array for the ciphertext and associated data
|
||||
byte[] ciphertext = new byte[gcmCipher.getOutputSize(plaintextBytes.length)];
|
||||
|
||||
// Encrypt the plaintext
|
||||
int len = gcmCipher.processBytes(plaintextBytes, 0, plaintextBytes.length, ciphertext, 0);
|
||||
gcmCipher.doFinal(ciphertext, len);
|
||||
|
||||
// Retrieve the authentication tag
|
||||
byte[] authenticationTag = gcmCipher.getMac();
|
||||
|
||||
return new AesGcmEncryptionResult(ciphertext, authenticationTag);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package gov.irs.directfile.stateapi.encryption;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import io.micrometer.common.util.StringUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtException;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
|
||||
import gov.irs.directfile.error.StateApiErrorCode;
|
||||
import gov.irs.directfile.stateapi.exception.StateApiException;
|
||||
import gov.irs.directfile.stateapi.model.ClientJwtClaim;
|
||||
|
||||
@Slf4j
|
||||
@SuppressWarnings({"PMD.AvoidUncheckedExceptionsInSignatures", "PMD.PreserveStackTrace"})
|
||||
public class JwtVerifier {
|
||||
|
||||
static ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
static {
|
||||
// Jwt treats 'iat' as Instant type, cannot convert claim map to Java Bean via convertValue without setting
|
||||
// JavaTimeModule
|
||||
JavaTimeModule module = new JavaTimeModule();
|
||||
objectMapper.registerModule(module);
|
||||
}
|
||||
|
||||
public static ClientJwtClaim verifyJwt(String jwtString, PublicKey publicKey)
|
||||
throws CertificateException, FileNotFoundException, JwtException, JsonMappingException,
|
||||
JsonProcessingException {
|
||||
|
||||
// assume publicKey is RSAPublicKey, if not, that's okay, let it throw error
|
||||
JwtDecoder jwtDecoder =
|
||||
NimbusJwtDecoder.withPublicKey((RSAPublicKey) publicKey).build();
|
||||
Jwt jwt = jwtDecoder.decode(jwtString);
|
||||
|
||||
return objectMapper.convertValue(jwt.getClaims(), ClientJwtClaim.class);
|
||||
}
|
||||
|
||||
// Note: use this method to do payload simple verification.
|
||||
public static String getAccountId(String jwtToken) {
|
||||
ClientJwtClaim claim;
|
||||
try {
|
||||
String[] parts = jwtToken.split("\\.");
|
||||
// Note: We need to use java.util.Base64's getUrlDecoder, instead of org.bouncycastle.util.encoders.Base64.
|
||||
// b/c here the encoded string is coming from http request.
|
||||
byte[] payload = Base64.getUrlDecoder().decode(parts[1]);
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
claim = objectMapper.readValue(payload, ClientJwtClaim.class);
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"getAccountId() failed, could not extract account id from JWT Bearer token, {}, error: {}",
|
||||
e.getClass(),
|
||||
e.getMessage());
|
||||
throw new StateApiException(StateApiErrorCode.E_BAD_JWT_BEARER_TOKEN);
|
||||
}
|
||||
|
||||
String accountId = claim.getAccountId();
|
||||
if (StringUtils.isBlank(accountId)) {
|
||||
log.error("Account id is missing from JWT Bearer token");
|
||||
throw new StateApiException(StateApiErrorCode.E_ACCOUNT_ID_MISSING_IN_JWT_TOKEN);
|
||||
} else {
|
||||
try {
|
||||
UUID.fromString(claim.getAuthorizationCode());
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Authorization code is not valid: {}", claim.getAuthorizationCode());
|
||||
throw new StateApiException(StateApiErrorCode.E_AUTHORIZATION_CODE_INVALID_FORMAT);
|
||||
}
|
||||
}
|
||||
|
||||
return accountId;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package gov.irs.directfile.stateapi.events;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public class Event {
|
||||
@NotNull private final Instant timestamp = Instant.now();
|
||||
|
||||
@NotNull private final EventStatus eventStatus;
|
||||
|
||||
@NotNull private final EventId eventId;
|
||||
|
||||
@NotNull private final String responseStatusCode;
|
||||
|
||||
@NotNull private String taxPeriod;
|
||||
|
||||
@NotNull private String userType;
|
||||
|
||||
@NotNull private String remoteAddress;
|
||||
|
||||
@NotNull private String stateId;
|
||||
|
||||
@NotNull private String taxReturnId;
|
||||
|
||||
private final String eventErrorMessage;
|
||||
private EventDetail detail;
|
||||
|
||||
@NotNull @Builder.Default
|
||||
private String eventType = "STATE_API";
|
||||
|
||||
@NotNull @Builder.Default
|
||||
private final boolean cyberOnly = true;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package gov.irs.directfile.stateapi.events;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class EventDetail {
|
||||
|
||||
private HashMap<String, String> detailMap;
|
||||
|
||||
public EventDetail() {
|
||||
detailMap = new HashMap<>();
|
||||
}
|
||||
|
||||
public void addDetail(String key, String val) {
|
||||
if (StringUtils.isNotEmpty(key) && StringUtils.isNotEmpty(val)) {
|
||||
detailMap.put(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, String> getDetailMap() {
|
||||
return detailMap;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package gov.irs.directfile.stateapi.events;
|
||||
|
||||
public enum EventId {
|
||||
CREATE_AUTHORIZATION_CODE,
|
||||
EXPORT_TAX_RETURN,
|
||||
CREATE_AUTHORIZATION_TOKEN
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package gov.irs.directfile.stateapi.events;
|
||||
|
||||
public enum EventStatus {
|
||||
SUCCESS("00"),
|
||||
FAILURE("01");
|
||||
|
||||
private final String value;
|
||||
|
||||
EventStatus(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package gov.irs.directfile.stateapi.events;
|
||||
|
||||
public enum UserType {
|
||||
REGT,
|
||||
SYS,
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package gov.irs.directfile.stateapi.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import gov.irs.directfile.error.StateApiErrorCode;
|
||||
|
||||
@Getter
|
||||
public class StateApiException extends RuntimeException {
|
||||
|
||||
private final StateApiErrorCode errorCode;
|
||||
|
||||
public StateApiException(StateApiErrorCode errorCode) {
|
||||
super(errorCode.name());
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public StateApiException(StateApiErrorCode errorCode, Throwable e) {
|
||||
super(errorCode.name(), e);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package gov.irs.directfile.stateapi.exception;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class StateApiExceptionHandler {
|
||||
@ExceptionHandler(StateApiException.class)
|
||||
public ResponseEntity<String> handleStateApiException(StateApiException ex) {
|
||||
return new ResponseEntity<>(ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package gov.irs.directfile.stateapi.exception;
|
||||
|
||||
import gov.irs.directfile.error.StateApiErrorCode;
|
||||
|
||||
public class StateApiExportedFactsDisabledException extends StateApiException {
|
||||
public StateApiExportedFactsDisabledException() {
|
||||
super(StateApiErrorCode.E_EXPORTED_FACTS_DISABLED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return getErrorCode().name() + "Encountered unexpected configuration mismatch - exported facts are disabled in"
|
||||
+ " the backend api, but not in the state-api";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package gov.irs.directfile.stateapi.exception;
|
||||
|
||||
import gov.irs.directfile.error.StateApiErrorCode;
|
||||
|
||||
public class StateNotExistException extends StateApiException {
|
||||
|
||||
public StateNotExistException(StateApiErrorCode errorCode) {
|
||||
super(errorCode);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package gov.irs.directfile.stateapi.model;
|
||||
|
||||
public record AesGcmEncryptionResult(byte[] ciphertext, byte[] authenticationTag) {}
|
|
@ -0,0 +1,42 @@
|
|||
package gov.irs.directfile.stateapi.model;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public class AuthCodeRequest {
|
||||
|
||||
@NotNull private UUID taxReturnUuid;
|
||||
|
||||
private String tin;
|
||||
|
||||
@NotNull private int taxYear;
|
||||
|
||||
@NotBlank
|
||||
private String stateCode;
|
||||
|
||||
@NotBlank
|
||||
private String submissionId;
|
||||
|
||||
@JsonIgnore
|
||||
public static final String docsExampleObject =
|
||||
"""
|
||||
{
|
||||
"taxReturnUuid": "ae019609-99e0-4ef5-85bb-ad90dc302e70",
|
||||
"tin": "123456789",
|
||||
"taxYear": 2022,
|
||||
"stateCode": "DC",
|
||||
"submissionId":"12345678901234567890"
|
||||
}
|
||||
""";
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package gov.irs.directfile.stateapi.model;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
|
||||
import gov.irs.directfile.error.StateApiErrorCode;
|
||||
import gov.irs.directfile.stateapi.exception.StateApiException;
|
||||
|
||||
@Data
|
||||
@Table(name = "authorization_code")
|
||||
@Entity
|
||||
@Slf4j
|
||||
@SuppressWarnings("PMD.PreserveStackTrace")
|
||||
public class AuthorizationCode {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank
|
||||
private UUID taxReturnUuid;
|
||||
|
||||
@NotBlank
|
||||
private String authorizationCode;
|
||||
|
||||
@NotNull private int taxYear;
|
||||
|
||||
@NotNull private Timestamp expiresAt;
|
||||
|
||||
@NotBlank
|
||||
private String stateCode;
|
||||
|
||||
private String submissionId;
|
||||
|
||||
public void setAuthorizationCode(UUID code) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(code.toString().getBytes(StandardCharsets.UTF_8));
|
||||
this.authorizationCode = new String(Hex.encode(hash), StandardCharsets.UTF_8);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
log.error("setAuthorizationCode() failed to hash UUID, could not find algorithm");
|
||||
throw new StateApiException(StateApiErrorCode.E_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package gov.irs.directfile.stateapi.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class ClientJwtClaim {
|
||||
@JsonProperty("iss")
|
||||
private String accountId;
|
||||
|
||||
@JsonProperty("sub")
|
||||
private String authorizationCode;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package gov.irs.directfile.stateapi.model;
|
||||
|
||||
public record EncryptData(
|
||||
String encodedSecret, String encodedIV, String encodedAndEncryptedData, String encodedAuthenticationTag) {}
|
|
@ -0,0 +1,13 @@
|
|||
package gov.irs.directfile.stateapi.model;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import gov.irs.directfile.error.StateApiErrorCode;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class ErrorResponse {
|
||||
private StateApiErrorCode errorCode;
|
||||
private String errorMessage;
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package gov.irs.directfile.stateapi.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class ExportResponse {
|
||||
@NotNull private String status;
|
||||
|
||||
private String taxReturn;
|
||||
private String error;
|
||||
|
||||
@JsonIgnore
|
||||
public static final String docsExampleObjectSuccess =
|
||||
"""
|
||||
{
|
||||
"status": "success",
|
||||
"taxReturn": "encoded-encrypted-data"
|
||||
}
|
||||
""";
|
||||
|
||||
public static final String docsExampleObjectError =
|
||||
"""
|
||||
{
|
||||
"status": "error",
|
||||
"error": "E_AUTHORIZATION_CODE_EXPIRED"
|
||||
}
|
||||
""";
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package gov.irs.directfile.stateapi.model;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
// TODO: Consolidate exported facts related classes/records between backend and state-api
|
||||
// in shared data-models dependency https://git.irslabs.org/irslabs-prototypes/direct-file/-/issues/9570
|
||||
public record GetStateExportedFactsResponse(Map<String, Object> exportedFacts) {}
|
|
@ -0,0 +1,16 @@
|
|||
package gov.irs.directfile.stateapi.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class StateAndAuthCode {
|
||||
|
||||
private String authorizationCode;
|
||||
private String stateCode;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package gov.irs.directfile.stateapi.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Table(name = "state_language")
|
||||
public class StateLanguage {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotNull private Long stateProfileId;
|
||||
|
||||
/**
|
||||
* The language code that DF understands, e.g. `es` or `en`
|
||||
*/
|
||||
@NotBlank
|
||||
private String dfLanguageCode;
|
||||
|
||||
/**
|
||||
* The corresponding language code that the state uses, e.g. `en`, `eng`, `english`, etc
|
||||
*/
|
||||
@NotBlank
|
||||
private String stateLanguageCode;
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package gov.irs.directfile.stateapi.model;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Table(name = "state_profile")
|
||||
public class StateProfile {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank
|
||||
private String accountId;
|
||||
|
||||
@NotBlank
|
||||
private String stateCode;
|
||||
|
||||
@NotBlank
|
||||
private String taxSystemName;
|
||||
|
||||
@NotBlank
|
||||
private String landingUrl;
|
||||
|
||||
private String defaultRedirectUrl;
|
||||
|
||||
private String departmentOfRevenueUrl;
|
||||
|
||||
private String filingRequirementsUrl;
|
||||
|
||||
private String transferCancelUrl;
|
||||
|
||||
private String waitingForAcceptanceCancelUrl;
|
||||
|
||||
private String certLocation;
|
||||
|
||||
@NotNull private Boolean acceptedOnly;
|
||||
|
||||
private OffsetDateTime certExpirationDate;
|
||||
|
||||
private String customFilingDeadline;
|
||||
|
||||
@NotNull private Boolean archived;
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package gov.irs.directfile.stateapi.model;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Table(name = "state_redirect")
|
||||
public class StateRedirect {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotNull private Long stateProfileId;
|
||||
|
||||
@NotBlank
|
||||
private String redirectUrl;
|
||||
|
||||
@NotNull private Instant createdAt;
|
||||
|
||||
private Instant expiresAt;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package gov.irs.directfile.stateapi.model;
|
||||
|
||||
public record TaxReturnStatus(String status, boolean exists) {}
|
|
@ -0,0 +1,23 @@
|
|||
package gov.irs.directfile.stateapi.model;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.USE_DEFAULTS)
|
||||
public class TaxReturnToExport {
|
||||
@NotNull private String status;
|
||||
|
||||
private String submissionId;
|
||||
private String xml;
|
||||
private Map<String, Object> directFileData;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package gov.irs.directfile.stateapi.model;
|
||||
|
||||
public record TaxReturnXml(String status, String submissionId, String xml) {}
|
|
@ -0,0 +1,13 @@
|
|||
package gov.irs.directfile.stateapi.repository;
|
||||
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import gov.irs.directfile.stateapi.model.AuthorizationCode;
|
||||
|
||||
@Repository
|
||||
public interface AuthorizationCodeRepository extends R2dbcRepository<AuthorizationCode, Integer> {
|
||||
Mono<AuthorizationCode> getByAuthorizationCode(@Param("authorizationCode") String authDigest);
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package gov.irs.directfile.stateapi.repository;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import gov.irs.directfile.error.StateApiErrorCode;
|
||||
import gov.irs.directfile.stateapi.configuration.DirectFileEndpointProperties;
|
||||
import gov.irs.directfile.stateapi.exception.StateApiException;
|
||||
import gov.irs.directfile.stateapi.exception.StateApiExportedFactsDisabledException;
|
||||
import gov.irs.directfile.stateapi.model.GetStateExportedFactsResponse;
|
||||
import gov.irs.directfile.stateapi.model.TaxReturnStatus;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class DirectFileBackendService {
|
||||
|
||||
private final WebClient webClient;
|
||||
private final String baseUri;
|
||||
|
||||
public DirectFileBackendService(DirectFileEndpointProperties dfEndpointProperties) {
|
||||
webClient = WebClient.builder()
|
||||
.defaultStatusHandler(
|
||||
HttpStatusCode::isError, resp -> resp.createException().flatMap(e -> {
|
||||
log.error(
|
||||
"DirectFileBackendService failed, {}, error: {}",
|
||||
e.getClass().getName(),
|
||||
e.getMessage());
|
||||
return Mono.just(new StateApiException(StateApiErrorCode.E_INTERNAL_SERVER_ERROR));
|
||||
}))
|
||||
.baseUrl(dfEndpointProperties.getBackendUrl())
|
||||
.build();
|
||||
|
||||
baseUri = dfEndpointProperties.getBackendContextPath() + "/" + dfEndpointProperties.getBackendApiVersion();
|
||||
}
|
||||
|
||||
public Mono<GetStateExportedFactsResponse> getExportedFacts(
|
||||
String submissionId, String stateCode, String accountId) {
|
||||
String efUri = baseUri + "/state-api/state-exported-facts/{submissionId}";
|
||||
return webClient
|
||||
.get()
|
||||
.uri(uriBuilder -> uriBuilder
|
||||
.path(efUri)
|
||||
.queryParam("stateCode", stateCode)
|
||||
.queryParam("accountId", accountId)
|
||||
.build(submissionId))
|
||||
.retrieve()
|
||||
.onStatus(
|
||||
HttpStatus.METHOD_NOT_ALLOWED::equals,
|
||||
clientResponse -> Mono.error(new StateApiExportedFactsDisabledException()))
|
||||
.bodyToMono(GetStateExportedFactsResponse.class);
|
||||
}
|
||||
|
||||
public Mono<TaxReturnStatus> getStatus(int taxYear, UUID taxReturnId, String submissionId) {
|
||||
String statusUri = baseUri + "/state-api/status/{taxFilingYear}/{taxReturnId}/{submissionId}";
|
||||
return webClient
|
||||
.get()
|
||||
.uri(uriBuilder -> uriBuilder.path(statusUri).build(taxYear, taxReturnId, submissionId))
|
||||
.retrieve()
|
||||
.bodyToMono(TaxReturnStatus.class);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package gov.irs.directfile.stateapi.repository;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import gov.irs.directfile.stateapi.model.TaxReturnStatus;
|
||||
|
||||
@SuppressWarnings("PMD.UnnecessaryModifier")
|
||||
public interface DirectFileClient {
|
||||
static String STATUS_ACCEPTED = "accepted";
|
||||
static String STATUS_REJECTED = "rejected";
|
||||
static String STATUS_PENDING = "pending";
|
||||
static String STATUS_ERROR = "error";
|
||||
|
||||
Mono<TaxReturnStatus> getStatus(int taxYear, UUID taxReturnId, String submissionId);
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package gov.irs.directfile.stateapi.repository;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import gov.irs.directfile.stateapi.configuration.DirectFileEndpointProperties;
|
||||
import gov.irs.directfile.stateapi.model.TaxReturnStatus;
|
||||
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "direct-file.status.mock", havingValue = "false", matchIfMissing = true)
|
||||
@Slf4j
|
||||
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
|
||||
public class DirectFileClientImpl implements DirectFileClient {
|
||||
|
||||
private DirectFileBackendService directFileBackendService;
|
||||
|
||||
public DirectFileClientImpl(DirectFileEndpointProperties dfEndpointProperties) {
|
||||
directFileBackendService = new DirectFileBackendService(dfEndpointProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<TaxReturnStatus> getStatus(int taxYear, UUID taxReturnId, String submissionId) {
|
||||
return directFileBackendService
|
||||
.getStatus(taxYear, taxReturnId, submissionId)
|
||||
.map(ts -> {
|
||||
if (!ts.exists()) {
|
||||
log.warn(
|
||||
"getStatus: E_TAX_RETURN_NOT_FOUND (possibly due to replication delay across regions, expecting retry from user). taxReturnId={}, submissionId={}, tax year={}",
|
||||
taxReturnId,
|
||||
submissionId,
|
||||
taxYear);
|
||||
}
|
||||
return new TaxReturnStatus(ts.status().toLowerCase(), ts.exists());
|
||||
})
|
||||
.doOnError(e -> {
|
||||
log.error(
|
||||
"getStatus() failed for taxYear={}, taxReturnId={}, submissionId={}. Exception: {}. Error: {}",
|
||||
taxYear,
|
||||
taxReturnId.toString(),
|
||||
submissionId,
|
||||
e.getClass().getName(),
|
||||
e.getMessage());
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package gov.irs.directfile.stateapi.repository;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import gov.irs.directfile.stateapi.model.TaxReturnStatus;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@ConditionalOnProperty(name = "direct-file.status.mock", havingValue = "true", matchIfMissing = false)
|
||||
public class DirectFileClientMock implements DirectFileClient {
|
||||
|
||||
@Override
|
||||
public Mono<TaxReturnStatus> getStatus(int taxYear, UUID taxReturnId, String submissionId) {
|
||||
log.info("Enter Mock getStatus submissionId={}, taxReturnId={}", submissionId, taxReturnId);
|
||||
|
||||
String idString = taxReturnId.toString();
|
||||
if (idString.startsWith("a")) return Mono.just(new TaxReturnStatus(STATUS_ACCEPTED, true));
|
||||
else if (idString.startsWith("b")) return Mono.just(new TaxReturnStatus(STATUS_REJECTED, true));
|
||||
else return Mono.just(new TaxReturnStatus(STATUS_PENDING, true));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package gov.irs.directfile.stateapi.repository;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.UUID;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@SuppressWarnings("PMD.UnnecessaryModifier")
|
||||
public interface StateApiS3Client {
|
||||
public Mono<InputStream> getCert(String certUrl);
|
||||
|
||||
public Mono<String> getTaxReturnXml(int taxYear, UUID taxReturnId, String submissionId);
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
package gov.irs.directfile.stateapi.repository;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
import software.amazon.awssdk.core.ResponseBytes;
|
||||
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
|
||||
import software.amazon.awssdk.services.s3.S3AsyncClient;
|
||||
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
|
||||
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
|
||||
import software.amazon.encryption.s3.S3AsyncEncryptionClient;
|
||||
|
||||
import gov.irs.directfile.error.StateApiErrorCode;
|
||||
import gov.irs.directfile.stateapi.configuration.S3ConfigurationProperties;
|
||||
import gov.irs.directfile.stateapi.configuration.XmlSanitizedConfigurationProperties;
|
||||
import gov.irs.directfile.stateapi.exception.StateApiException;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@SuppressWarnings({"PMD.AvoidReassigningLoopVariables"})
|
||||
public class StateApiS3ClientImpl implements StateApiS3Client {
|
||||
private final S3AsyncClient s3Client;
|
||||
private final S3AsyncEncryptionClient s3EncryptionClient;
|
||||
|
||||
private final S3ConfigurationProperties s3ConfigurationProperties;
|
||||
|
||||
private final XmlSanitizedConfigurationProperties xmlSanitizedConfigurationProperties;
|
||||
|
||||
private List<Pattern> excludedPatterns = new ArrayList<>();
|
||||
|
||||
@Autowired
|
||||
public StateApiS3ClientImpl(
|
||||
S3AsyncClient s3Client,
|
||||
S3AsyncEncryptionClient s3EncryptionClient,
|
||||
S3ConfigurationProperties s3ConfigurationProperties,
|
||||
XmlSanitizedConfigurationProperties xmlSanitizedConfigurationProperties) {
|
||||
this.s3Client = s3Client;
|
||||
this.s3EncryptionClient = s3EncryptionClient;
|
||||
this.s3ConfigurationProperties = s3ConfigurationProperties;
|
||||
this.xmlSanitizedConfigurationProperties = xmlSanitizedConfigurationProperties;
|
||||
|
||||
setExcludedPatterns();
|
||||
}
|
||||
|
||||
private void setExcludedPatterns() {
|
||||
log.info("xml headers allowed: {}", xmlSanitizedConfigurationProperties.getAllowedHeaders());
|
||||
var excludedTags = xmlSanitizedConfigurationProperties.getExcludedTags();
|
||||
log.info("xml tags excluded: {}", excludedTags);
|
||||
if (excludedTags != null) {
|
||||
for (String header : excludedTags) {
|
||||
String formattedHeader = header.trim();
|
||||
String regex = "<" + formattedHeader + ">[\\s\\S]*?</" + formattedHeader + ">";
|
||||
excludedPatterns.add(Pattern.compile(regex));
|
||||
}
|
||||
}
|
||||
|
||||
// remove all blank lines caused by xml node manipulation
|
||||
excludedPatterns.add(Pattern.compile("(?m)^[ \\t]*\\r?\\n"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<InputStream> getCert(String certName) {
|
||||
log.info("enter getCert()...for cert {}", certName);
|
||||
|
||||
String s3Prefix = s3ConfigurationProperties.getPrefix();
|
||||
String objectKey = (StringUtils.isNotBlank(s3Prefix) ? s3Prefix + "/" : "") + certName;
|
||||
|
||||
log.info("checking for cert at path {}", objectKey);
|
||||
|
||||
return getUnencryptedS3Object(
|
||||
s3ConfigurationProperties.getCertBucketName(),
|
||||
objectKey,
|
||||
StateApiErrorCode.E_CERTIFICATE_NOT_FOUND)
|
||||
.map(s -> new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> getTaxReturnXml(int taxYear, UUID taxReturnId, String submissionId) {
|
||||
String objectKey = generateSubmissionLocationObjectKey(taxYear, taxReturnId, submissionId);
|
||||
|
||||
return getEncryptedS3Object(
|
||||
s3ConfigurationProperties.getTaxReturnXmlBucketName(),
|
||||
objectKey,
|
||||
StateApiErrorCode.E_TAX_RETURN_NOT_FOUND)
|
||||
.flatMap(xmlString -> {
|
||||
log.info("getSanitizedXml for: fillingYear={}, taxReturnId={}", taxYear, taxReturnId);
|
||||
|
||||
try {
|
||||
String sanitizedXml = getXmlString(xmlString);
|
||||
return Mono.just(sanitizedXml);
|
||||
} catch (Exception e) {
|
||||
log.error("getSanitizedXml failed, {}, {}", e.getClass().getName(), e.getMessage(), e);
|
||||
return Mono.error(new StateApiException(StateApiErrorCode.E_INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String getXmlString(String ignoredXmlString) {
|
||||
return "<xml>";
|
||||
}
|
||||
|
||||
private String generateSubmissionLocationObjectKey(int taxFilingYear, UUID taxReturnId, String submissionId) {
|
||||
String objectPath = taxFilingYear + "/taxreturns/" + taxReturnId + "/submissions/" + submissionId + ".xml";
|
||||
|
||||
String s3Prefix = s3ConfigurationProperties.getPrefix();
|
||||
if (StringUtils.isNotBlank(s3Prefix)) {
|
||||
return s3Prefix + "/" + objectPath;
|
||||
}
|
||||
|
||||
return objectPath;
|
||||
}
|
||||
|
||||
private Mono<String> getEncryptedS3Object(String theBucketName, String objectKey, StateApiErrorCode ec) {
|
||||
return getS3Object(theBucketName, objectKey, ec, s3EncryptionClient);
|
||||
}
|
||||
|
||||
private Mono<String> getUnencryptedS3Object(String theBucketName, String objectKey, StateApiErrorCode ec) {
|
||||
return getS3Object(theBucketName, objectKey, ec, s3Client);
|
||||
}
|
||||
|
||||
private Mono<String> getS3Object(
|
||||
String theBucketName, String objectKey, StateApiErrorCode ec, S3AsyncClient s3Client) {
|
||||
log.info("enter getS3Object()...for bucket: {}, key: {}", theBucketName, objectKey);
|
||||
|
||||
GetObjectRequest objectRequest =
|
||||
GetObjectRequest.builder().bucket(theBucketName).key(objectKey).build();
|
||||
|
||||
CompletableFuture<ResponseBytes<GetObjectResponse>> futureGet =
|
||||
s3Client.getObject(objectRequest, AsyncResponseTransformer.toBytes());
|
||||
|
||||
return Mono.fromFuture(() -> futureGet)
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
log.error(
|
||||
"getS3Object() failed, cannot find s3 object, bucket: {}, key: {}",
|
||||
theBucketName,
|
||||
objectKey);
|
||||
return Mono.error(new StateApiException(ec));
|
||||
}))
|
||||
.onErrorMap(NoSuchKeyException.class, e -> {
|
||||
log.error(
|
||||
"getS3Object() failed, cannot find s3 object, bucket: {}, key: {}",
|
||||
theBucketName,
|
||||
objectKey);
|
||||
return new StateApiException(ec);
|
||||
})
|
||||
.onErrorMap(e -> !(e instanceof StateApiException), e -> {
|
||||
log.error(
|
||||
"getS3Object() failed, bucket: {}, key: {}, {}, error: {}",
|
||||
theBucketName,
|
||||
objectKey,
|
||||
e.getClass().getName(),
|
||||
e.getMessage());
|
||||
return new StateApiException(StateApiErrorCode.E_INTERNAL_SERVER_ERROR);
|
||||
})
|
||||
.map(responseBytes -> {
|
||||
ByteBuffer byteBuffer = responseBytes.asByteBuffer();
|
||||
byte[] byteArray = new byte[byteBuffer.remaining()];
|
||||
byteBuffer.get(byteArray);
|
||||
log.info(
|
||||
"object retrieved from S3, length: {}, bucket: {}, key: {}",
|
||||
byteArray.length,
|
||||
theBucketName,
|
||||
objectKey);
|
||||
return new String(byteArray, StandardCharsets.UTF_8);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package gov.irs.directfile.stateapi.repository;
|
||||
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import gov.irs.directfile.stateapi.model.StateLanguage;
|
||||
|
||||
public interface StateLanguageRepository extends R2dbcRepository<StateLanguage, Long> {
|
||||
Flux<StateLanguage> getAllByStateProfileId(Long stateProfileId);
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package gov.irs.directfile.stateapi.repository;
|
||||
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import gov.irs.directfile.stateapi.model.StateProfile;
|
||||
|
||||
@Repository
|
||||
public interface StateProfileRepository extends R2dbcRepository<StateProfile, Integer> {
|
||||
Mono<StateProfile> getByAccountId(String accountId);
|
||||
|
||||
Mono<StateProfile> getByStateCode(String stateCode);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package gov.irs.directfile.stateapi.repository;
|
||||
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import gov.irs.directfile.stateapi.model.StateRedirect;
|
||||
|
||||
public interface StateRedirectRepository extends R2dbcRepository<StateRedirect, Long> {
|
||||
Flux<StateRedirect> getAllByStateProfileId(Long stateProfileId);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package gov.irs.directfile.stateapi.repository.facts;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import gov.irs.directfile.stateapi.model.GetStateExportedFactsResponse;
|
||||
|
||||
public interface ExportedFactsClient {
|
||||
|
||||
Mono<GetStateExportedFactsResponse> getExportedFacts(String submissionId, String stateCode, String accountId);
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package gov.irs.directfile.stateapi.repository.facts;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import gov.irs.directfile.stateapi.configuration.DirectFileEndpointProperties;
|
||||
import gov.irs.directfile.stateapi.model.GetStateExportedFactsResponse;
|
||||
import gov.irs.directfile.stateapi.repository.DirectFileBackendService;
|
||||
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "direct-file.exported-facts.mock", havingValue = "false", matchIfMissing = true)
|
||||
@Slf4j
|
||||
public class ExportedFactsClientImpl implements ExportedFactsClient {
|
||||
|
||||
private DirectFileBackendService directFileBackendService;
|
||||
|
||||
public ExportedFactsClientImpl(DirectFileEndpointProperties dfEndpointProperties) {
|
||||
directFileBackendService = new DirectFileBackendService(dfEndpointProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<GetStateExportedFactsResponse> getExportedFacts(
|
||||
String submissionId, String stateCode, String accountId) {
|
||||
return directFileBackendService
|
||||
.getExportedFacts(submissionId, stateCode, accountId)
|
||||
.doOnSuccess(ef -> log.info(
|
||||
"getExportedFacts runs successfully for accountId={}, submissionId={}",
|
||||
accountId,
|
||||
submissionId))
|
||||
.doOnError(e -> {
|
||||
log.error(
|
||||
"getExportedFacts() failed for accountId={}, submissionId={}, {}, error: {}",
|
||||
accountId,
|
||||
submissionId,
|
||||
e.getClass().getName(),
|
||||
e.getMessage());
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
package gov.irs.directfile.stateapi.repository.facts;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import gov.irs.directfile.stateapi.model.GetStateExportedFactsResponse;
|
||||
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "direct-file.exported-facts.mock", havingValue = "true", matchIfMissing = false)
|
||||
@Slf4j
|
||||
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
|
||||
public class ExportedFactsClientMock implements ExportedFactsClient {
|
||||
|
||||
private static final String FILERS = "filers";
|
||||
private static final String FAMILY_AND_HH = "familyAndHousehold";
|
||||
private static final String FIRST_NAME = "firstName";
|
||||
public static final String MIDDLE_INITIAL = "middleInitial";
|
||||
public static final String LAST_NAME = "lastName";
|
||||
public static final String SUFFIX = "suffix";
|
||||
public static final String DATE_OF_BIRTH = "dateOfBirth";
|
||||
public static final String RELATIONSHIP = "relationship";
|
||||
public static final String ELIGIBLE_DEPENDENT = "eligibleDependent";
|
||||
public static final String IS_CLAIMED_DEPENDENT = "isClaimedDependent";
|
||||
public static final String IS_PRIMARY_FILER = "isPrimaryFiler";
|
||||
private static final String RESIDENCY_DURATION = "residencyDuration";
|
||||
private static final String MONTH_LIVE_WITHTP_IN_US = "monthsLivedWithTPInUS";
|
||||
private static final String SSN_NOT_VALID_FOR_EMPLOYMENT = "ssnNotValidForEmployment";
|
||||
private static final String QUALIFYING_CHILD = "qualifyingChild";
|
||||
private static final String EDUCATOR_EXPENSES = "educatorExpenses";
|
||||
private static final String HSA_TOTAL_DEDUCTIBLE_AMOUNT = "hsaTotalDeductibleAmount";
|
||||
private static final String INTEREST_REPORTS_TOTAL = "interestReportsTotal";
|
||||
private static final String FORM_1099Gs_TOTAL = "form1099GsTotal";
|
||||
private static final String IS_STUDENT = "isStudent";
|
||||
private static final String IS_DISABLED = "isDisabled";
|
||||
|
||||
// 1099-Int
|
||||
private static final String TIN_NUM = "tin";
|
||||
private static final String RECIPIENT_TIN = "recipientTin";
|
||||
private static final String INTEREST_REPORT = "interestReports";
|
||||
private static final String AMT_1099 = "1099Amount";
|
||||
private static final String TAX_EXEMPT_INTEREST = "taxExemptInterest";
|
||||
private static final String INTEREST_GOVERNMENT_BONDS = "interestOnGovernmentBonds";
|
||||
private static final String HAS_1099 = "has1099";
|
||||
private static final String NO_1099_AMOUNT = "no1099Amount";
|
||||
private static final String PAYER_KEY = "payer";
|
||||
private static final String PAYER_TIN = "payerTin";
|
||||
private static final String TAX_WITHHELD = "taxWithheld";
|
||||
private static final String CUSIP_NO = "taxExemptAndTaxCreditBondCusipNo";
|
||||
|
||||
// 1099-G
|
||||
private static final String FORM_1099Gs = "form1099Gs";
|
||||
private static final String HAS_1099G = "has1099";
|
||||
private static final String AMT_1099G = "amount";
|
||||
private static final String FED_TAX_WITHHELD = "federalTaxWithheld";
|
||||
private static final String STATE_ID_NUM = "stateIdNumber";
|
||||
private static final String STATE_TAX_WITHHELD = "stateTaxWithheld";
|
||||
private static final String AMT_PAID_FOR_BENEFITS = "amountPaidBackForBenefitsInTaxYear";
|
||||
private static final String SCHEDULE_EIC_LINE4A_YES = "scheduleEicLine4aYes";
|
||||
private static final String SCHEDULE_EIC_LINE4A_NO = "scheduleEicLine4aNo";
|
||||
private static final String SCHEDULE_EIC_LINE4B_YES = "scheduleEicLine4bYes";
|
||||
private static final String HOH_QUALIFYING_PERSON = "hohQualifyingPerson";
|
||||
|
||||
// FormW2s
|
||||
private static final String FORM_W2s = "formW2s";
|
||||
private static final String UNION_DUES_AMT = "unionDuesAmount";
|
||||
private static final String BOX14_NJ_UIHCWD = "BOX14_NJ_UIHCWD";
|
||||
private static final String BOX14_NJ_UIWFSWF = "BOX14_NJ_UIWFSWF";
|
||||
|
||||
// socialSecurityReports
|
||||
private static final String SOCIAL_SECURITY_RPT = "socialSecurityReports";
|
||||
private static final String NET_BENEFITS = "netBenefits";
|
||||
private static final String FORM_TYPE = "formType";
|
||||
|
||||
@Override
|
||||
public Mono<GetStateExportedFactsResponse> getExportedFacts(
|
||||
String submissionId, String stateCode, String accountId) {
|
||||
|
||||
log.info("Enter Mock getExportedFacts submissionId={}, accountId={}", submissionId, accountId);
|
||||
var exportedFacts = new HashMap<String, Object>();
|
||||
|
||||
var filers = new ArrayList<HashMap<String, Object>>();
|
||||
|
||||
var filer1 = new HashMap<String, Object>();
|
||||
filer1.put(FIRST_NAME, "Samuel");
|
||||
filer1.put(MIDDLE_INITIAL, null);
|
||||
filer1.put(LAST_NAME, "Smith");
|
||||
filer1.put(SUFFIX, "Jr");
|
||||
filer1.put(DATE_OF_BIRTH, "1985-09-29");
|
||||
filer1.put(IS_PRIMARY_FILER, Boolean.TRUE);
|
||||
filer1.put(TIN_NUM, "100-01-1234");
|
||||
filer1.put(SSN_NOT_VALID_FOR_EMPLOYMENT, false);
|
||||
filer1.put(EDUCATOR_EXPENSES, "200.00");
|
||||
filer1.put(HSA_TOTAL_DEDUCTIBLE_AMOUNT, "600.00");
|
||||
filer1.put(INTEREST_REPORTS_TOTAL, "3000.00");
|
||||
filer1.put(FORM_1099Gs_TOTAL, "15000.00");
|
||||
filer1.put(IS_STUDENT, false);
|
||||
filer1.put(IS_DISABLED, false);
|
||||
filers.add(filer1);
|
||||
|
||||
var filer2 = new HashMap<String, Object>();
|
||||
filer2.put(FIRST_NAME, "Judy");
|
||||
filer2.put(MIDDLE_INITIAL, null);
|
||||
filer2.put(LAST_NAME, "Johnson");
|
||||
filer2.put(SUFFIX, null);
|
||||
filer2.put(DATE_OF_BIRTH, "1985-10-18");
|
||||
filer2.put(IS_PRIMARY_FILER, Boolean.FALSE);
|
||||
filer2.put(TIN_NUM, "100-02-1234");
|
||||
filer2.put(SSN_NOT_VALID_FOR_EMPLOYMENT, false);
|
||||
filer2.put(EDUCATOR_EXPENSES, "100.00");
|
||||
filer2.put(HSA_TOTAL_DEDUCTIBLE_AMOUNT, "500.00");
|
||||
filer2.put(INTEREST_REPORTS_TOTAL, "300.00");
|
||||
filer2.put(FORM_1099Gs_TOTAL, "1500.00");
|
||||
filer2.put(IS_STUDENT, false);
|
||||
filer2.put(IS_DISABLED, false);
|
||||
filers.add(filer2);
|
||||
|
||||
exportedFacts.put(FILERS, filers);
|
||||
|
||||
var familyAndHousehold = new ArrayList<HashMap<String, Object>>();
|
||||
|
||||
var person1 = new HashMap<String, Object>();
|
||||
person1.put(FIRST_NAME, "Sammy");
|
||||
person1.put(MIDDLE_INITIAL, null);
|
||||
person1.put(LAST_NAME, "Smith");
|
||||
person1.put(SUFFIX, "I");
|
||||
person1.put(DATE_OF_BIRTH, "2013-01-21");
|
||||
person1.put(RELATIONSHIP, "biologicalChild");
|
||||
person1.put(ELIGIBLE_DEPENDENT, true);
|
||||
person1.put(IS_CLAIMED_DEPENDENT, true);
|
||||
person1.put(TIN_NUM, "200-01-1234");
|
||||
person1.put(RESIDENCY_DURATION, "allYear");
|
||||
person1.put(MONTH_LIVE_WITHTP_IN_US, "twelve");
|
||||
person1.put(SCHEDULE_EIC_LINE4A_YES, true);
|
||||
person1.put(SCHEDULE_EIC_LINE4A_NO, false);
|
||||
person1.put(SCHEDULE_EIC_LINE4B_YES, false);
|
||||
person1.put(HOH_QUALIFYING_PERSON, true);
|
||||
person1.put(SSN_NOT_VALID_FOR_EMPLOYMENT, false);
|
||||
person1.put(QUALIFYING_CHILD, true);
|
||||
familyAndHousehold.add(person1);
|
||||
|
||||
exportedFacts.put(FAMILY_AND_HH, familyAndHousehold);
|
||||
|
||||
var intReports = new ArrayList<HashMap<String, Object>>();
|
||||
var intRpt = new HashMap<String, Object>();
|
||||
intRpt.put(HAS_1099, Boolean.TRUE);
|
||||
intRpt.put(AMT_1099, "800.00");
|
||||
intRpt.put(INTEREST_GOVERNMENT_BONDS, "300");
|
||||
intRpt.put(TAX_EXEMPT_INTEREST, "200.00");
|
||||
intRpt.put(RECIPIENT_TIN, "123-45-6789");
|
||||
intRpt.put(NO_1099_AMOUNT, null);
|
||||
intRpt.put(PAYER_KEY, "JPM Bank");
|
||||
intRpt.put(PAYER_TIN, "01-1234567");
|
||||
intRpt.put(TAX_WITHHELD, "120");
|
||||
intRpt.put(CUSIP_NO, "01234567A");
|
||||
intReports.add(intRpt);
|
||||
|
||||
exportedFacts.put(INTEREST_REPORT, intReports);
|
||||
|
||||
// 1099-G
|
||||
var form1099Gs = new ArrayList<HashMap<String, Object>>();
|
||||
var form1099G = new HashMap<String, Object>();
|
||||
form1099G.put(HAS_1099G, Boolean.TRUE);
|
||||
form1099G.put(RECIPIENT_TIN, "123-45-6789");
|
||||
form1099G.put(PAYER_KEY, "State of california");
|
||||
form1099G.put(PAYER_TIN, "321-54-9876");
|
||||
form1099G.put(AMT_1099G, "100.00");
|
||||
form1099G.put(FED_TAX_WITHHELD, "20.00");
|
||||
form1099G.put(STATE_ID_NUM, "123456");
|
||||
form1099G.put(STATE_TAX_WITHHELD, "10.00");
|
||||
form1099G.put(AMT_PAID_FOR_BENEFITS, "25.00");
|
||||
form1099Gs.add(form1099G);
|
||||
|
||||
exportedFacts.put(FORM_1099Gs, form1099Gs);
|
||||
|
||||
// formW2
|
||||
var formW2s = new ArrayList<HashMap<String, Object>>();
|
||||
var formW2 = new HashMap<String, Object>();
|
||||
formW2.put(UNION_DUES_AMT, "40.00");
|
||||
formW2.put(BOX14_NJ_UIHCWD, "101.00");
|
||||
formW2.put(BOX14_NJ_UIWFSWF, "101.00");
|
||||
formW2s.add(formW2);
|
||||
|
||||
exportedFacts.put(FORM_W2s, formW2s);
|
||||
|
||||
// socialSecurityReports
|
||||
var ssRpts = new ArrayList<HashMap<String, Object>>();
|
||||
var ssRpt = new HashMap<String, Object>();
|
||||
ssRpt.put(RECIPIENT_TIN, "123-45-6789");
|
||||
ssRpt.put(NET_BENEFITS, "21000.00");
|
||||
ssRpt.put(FORM_TYPE, "SSA-1099");
|
||||
ssRpts.add(ssRpt);
|
||||
|
||||
exportedFacts.put(SOCIAL_SECURITY_RPT, ssRpts);
|
||||
|
||||
var response = new GetStateExportedFactsResponse(exportedFacts);
|
||||
|
||||
return Mono.just(response);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
package gov.irs.directfile.stateapi.service;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import gov.irs.directfile.error.StateApiErrorCode;
|
||||
import gov.irs.directfile.stateapi.dto.StateProfileDTO;
|
||||
import gov.irs.directfile.stateapi.exception.StateApiException;
|
||||
import gov.irs.directfile.stateapi.exception.StateNotExistException;
|
||||
import gov.irs.directfile.stateapi.model.StateLanguage;
|
||||
import gov.irs.directfile.stateapi.model.StateProfile;
|
||||
import gov.irs.directfile.stateapi.model.StateRedirect;
|
||||
import gov.irs.directfile.stateapi.repository.StateApiS3Client;
|
||||
import gov.irs.directfile.stateapi.repository.StateLanguageRepository;
|
||||
import gov.irs.directfile.stateapi.repository.StateProfileRepository;
|
||||
import gov.irs.directfile.stateapi.repository.StateRedirectRepository;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@SuppressWarnings("PMD.PreserveStackTrace")
|
||||
public class CachedDataService {
|
||||
@Autowired
|
||||
private StateApiS3Client s3Client;
|
||||
|
||||
@Autowired
|
||||
private StateProfileRepository spRepo;
|
||||
|
||||
@Autowired
|
||||
private StateRedirectRepository srRepo;
|
||||
|
||||
@Autowired
|
||||
private StateLanguageRepository slRepo;
|
||||
|
||||
@Value("${spring.cache.TTL-minutes: 120}")
|
||||
private long cacheTTL;
|
||||
|
||||
// Note: We are applying cache of cache to a Mono. The native Caffeine cache's 'expireAfterAccess' won't take
|
||||
// effect. For the sake of simplicity, we periodically evict the caches.
|
||||
@CacheEvict(
|
||||
value = {"publicKeyCache", "stateProfileCache"},
|
||||
allEntries = true)
|
||||
@Scheduled(fixedRateString = "${spring.cache.TTL-minutes}", timeUnit = TimeUnit.MINUTES)
|
||||
public void emptyCaches() {
|
||||
log.info("caches (publicKeyCache, stateProfileCache) were evicted after {} minutes", cacheTTL);
|
||||
}
|
||||
|
||||
// NOTE: the cert is cached and expiration won't apply during the cache duration
|
||||
@Cacheable(cacheNames = "publicKeyCache", key = "#certName")
|
||||
public Mono<PublicKey> retrievePublicKeyFromCert(String certName, OffsetDateTime enforcedExpirationDate) {
|
||||
log.info("enter retrievePublicKeyFromCert()...for {}", certName);
|
||||
|
||||
return s3Client.getCert(certName)
|
||||
.flatMap(is -> {
|
||||
X509Certificate cert;
|
||||
try {
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
||||
cert = (X509Certificate) certFactory.generateCertificate(is);
|
||||
} catch (CertificateException e) {
|
||||
log.error(
|
||||
"retrievePublicKeyFromCert failed, {}, {}",
|
||||
e.getClass().getName(),
|
||||
e.getMessage());
|
||||
throw new StateApiException(StateApiErrorCode.E_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
// Check if the certificate is expired
|
||||
Date currentDate = new Date();
|
||||
if (currentDate.after(cert.getNotAfter())) {
|
||||
log.error("The certificate {} has expired", certName);
|
||||
throw new StateApiException(StateApiErrorCode.E_CERTIFICATE_EXPIRED);
|
||||
}
|
||||
|
||||
// check IRS enforced expiration date
|
||||
if (enforcedExpirationDate != null) {
|
||||
OffsetDateTime currentDateTime = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
if (currentDateTime.isAfter(enforcedExpirationDate)) {
|
||||
log.error("The certificate {} has passed the IRS enforced expiration date", certName);
|
||||
|
||||
throw new StateApiException(StateApiErrorCode.E_CERTIFICATE_EXPIRED);
|
||||
}
|
||||
}
|
||||
return Mono.just(cert.getPublicKey());
|
||||
})
|
||||
.cache(); // This is the hack to make @Cacheable work #https://www.baeldung.com/spring-webflux-cacheable
|
||||
}
|
||||
|
||||
@Cacheable(cacheNames = "stateProfileCache", key = "#accountId")
|
||||
public Mono<StateProfile> getStateProfile(String accountId) {
|
||||
log.info("enter getStateProfile()...accountId={}", accountId);
|
||||
|
||||
return spRepo.getByAccountId(accountId)
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
log.error(
|
||||
"getStateProfile() failed, account id does not exist in state_profile table for account id: {}",
|
||||
accountId);
|
||||
return Mono.error(new StateApiException(StateApiErrorCode.E_ACCOUNT_ID_NOT_EXIST));
|
||||
}))
|
||||
.onErrorMap(e -> !(e instanceof StateApiException), e -> {
|
||||
log.error(
|
||||
"getStateProfile failed for account id: {}, {}, error: {}",
|
||||
accountId,
|
||||
e.getClass().getName(),
|
||||
e.getMessage());
|
||||
return new StateApiException(StateApiErrorCode.E_INTERNAL_SERVER_ERROR);
|
||||
})
|
||||
.cache();
|
||||
}
|
||||
|
||||
@Cacheable(cacheNames = "stateProfileCache", key = "#stateCode")
|
||||
public Mono<StateProfileDTO> getStateProfileByStateCode(String stateCode) {
|
||||
log.info("enter getStateProfileByStateCode()...stateCode={}", stateCode);
|
||||
|
||||
return spRepo.getByStateCode(stateCode)
|
||||
.flatMap(this::loadRelations)
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
log.info("No StateProfile returns, state code {} does not exist in state_profile table", stateCode);
|
||||
return Mono.error(new StateNotExistException(StateApiErrorCode.E_STATE_NOT_EXIST));
|
||||
}))
|
||||
.onErrorMap(e -> !(e instanceof StateApiException), e -> {
|
||||
log.error(
|
||||
"getStateProfileByStateCode() failed for state code: {}, {}, error: {}",
|
||||
stateCode,
|
||||
e.getClass().getName(),
|
||||
e.getMessage());
|
||||
|
||||
return new StateApiException(StateApiErrorCode.E_INTERNAL_SERVER_ERROR);
|
||||
})
|
||||
.cache();
|
||||
}
|
||||
|
||||
private Mono<StateProfileDTO> loadRelations(final StateProfile stateProfile) {
|
||||
var stateProfileId = stateProfile.getId();
|
||||
|
||||
// Load the redirect urls
|
||||
Mono<List<StateRedirect>> redirectUrls =
|
||||
srRepo.getAllByStateProfileId(stateProfileId).collectList();
|
||||
// Load the languages
|
||||
Mono<List<StateLanguage>> stateLanguages =
|
||||
slRepo.getAllByStateProfileId(stateProfileId).collectList();
|
||||
|
||||
return redirectUrls
|
||||
.zipWith(stateLanguages)
|
||||
.map((urlsAndLanguagesTuple) -> new StateProfileDTO(
|
||||
stateProfile, urlsAndLanguagesTuple.getT1(), urlsAndLanguagesTuple.getT2()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package gov.irs.directfile.stateapi.service;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import gov.irs.directfile.stateapi.dto.StateProfileDTO;
|
||||
import gov.irs.directfile.stateapi.model.*;
|
||||
|
||||
public interface StateApiService {
|
||||
|
||||
Mono<UUID> createAuthorizationCode(AuthCodeRequest ac);
|
||||
|
||||
Mono<String> generateAuthorizationToken(AuthCodeRequest ac);
|
||||
|
||||
Mono<StateAndAuthCode> verifyJwtSignature(String jwt, String accountId);
|
||||
|
||||
Mono<AuthorizationCode> authorize(StateAndAuthCode stateAndAuthCode);
|
||||
|
||||
Mono<TaxReturnXml> retrieveTaxReturnXml(int taxYear, UUID taxReturnUuid, String submissionId);
|
||||
|
||||
Mono<EncryptData> encryptTaxReturn(TaxReturnToExport taxReturn, String accountId);
|
||||
|
||||
Mono<StateProfileDTO> lookupStateProfile(String stateCode);
|
||||
|
||||
Mono<StateProfile> getStateProfile(String accountId);
|
||||
|
||||
Mono<Map<String, Object>> retrieveExportedFacts(String submissionId, String stateCode, String accountId);
|
||||
}
|
|
@ -0,0 +1,437 @@
|
|||
package gov.irs.directfile.stateapi.service;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Security;
|
||||
import java.security.cert.CertificateExpiredException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nimbusds.jose.shaded.gson.Gson;
|
||||
import com.nimbusds.jose.shaded.gson.GsonBuilder;
|
||||
import io.micrometer.common.util.StringUtils;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import gov.irs.directfile.error.StateApiErrorCode;
|
||||
import gov.irs.directfile.stateapi.authorization.AuthorizationTokenClaims;
|
||||
import gov.irs.directfile.stateapi.authorization.AuthorizationTokenService;
|
||||
import gov.irs.directfile.stateapi.configuration.CertificationOverrideProperties;
|
||||
import gov.irs.directfile.stateapi.dto.StateProfileDTO;
|
||||
import gov.irs.directfile.stateapi.encryption.JwtVerifier;
|
||||
import gov.irs.directfile.stateapi.exception.StateApiException;
|
||||
import gov.irs.directfile.stateapi.model.*;
|
||||
import gov.irs.directfile.stateapi.repository.*;
|
||||
import gov.irs.directfile.stateapi.repository.facts.ExportedFactsClient;
|
||||
|
||||
import static gov.irs.directfile.stateapi.encryption.Encryptor.*;
|
||||
import static gov.irs.directfile.stateapi.repository.DirectFileClient.STATUS_ERROR;
|
||||
import static gov.irs.directfile.stateapi.repository.DirectFileClient.STATUS_REJECTED;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@SuppressWarnings({
|
||||
"PMD.UnusedPrivateMethod",
|
||||
"PMD.LiteralsFirstInComparisons",
|
||||
"PMD.UselessParentheses",
|
||||
"PMD.PreserveStackTrace"
|
||||
})
|
||||
public class StateApiServiceImpl implements StateApiService {
|
||||
@Value("${authorization-code.expires-interval-seconds: 600}")
|
||||
private int authorizationCodeExpiresInterval;
|
||||
|
||||
@Value("${direct-file.exported-facts.enabled: false}")
|
||||
private boolean exported_facts_enabled;
|
||||
|
||||
@Autowired
|
||||
private AuthorizationCodeRepository acRepo;
|
||||
|
||||
@Autowired
|
||||
private AuthorizationTokenService authorizationTokenService;
|
||||
|
||||
@Autowired
|
||||
private DirectFileClient dfClient;
|
||||
|
||||
@Autowired
|
||||
private ExportedFactsClient efClient;
|
||||
|
||||
@Autowired
|
||||
private CachedDataService cachedDS;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper mapper;
|
||||
|
||||
@Autowired
|
||||
private StateApiS3Client stateApiS3Client;
|
||||
|
||||
@Autowired
|
||||
CertificationOverrideProperties certProperties;
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<UUID> createAuthorizationCode(AuthCodeRequest acq) {
|
||||
log.info("enter createAuthorizationCode()...");
|
||||
|
||||
// The `dfClient.getStatus()` function returns the status along with an indicator of whether the XML exists in
|
||||
// the S3 bucket. However, it's important to consider the issue of eventual consistency, which refers to the
|
||||
// delay in propagating S3 objects. While we may verify the existence of the object in the west region, there's
|
||||
// a possibility that when performing the `export-tax-return` operation, retrieval occurs from the east region,
|
||||
// potentially resulting in a 'tax-return-not-found' error.
|
||||
return getAcceptedOnlyFlag(acq.getStateCode())
|
||||
.zipWith(
|
||||
dfClient.getStatus(acq.getTaxYear(), acq.getTaxReturnUuid(), acq.getSubmissionId()),
|
||||
(acceptedOnly, status) -> {
|
||||
final boolean isAccepted =
|
||||
status.status().equalsIgnoreCase(DirectFileClient.STATUS_ACCEPTED);
|
||||
final boolean isPending = status.status().equalsIgnoreCase(DirectFileClient.STATUS_PENDING);
|
||||
|
||||
final boolean submissionOk =
|
||||
status.exists() && (isAccepted || (!acceptedOnly && isPending));
|
||||
if (submissionOk) {
|
||||
final AuthorizationCode ac = new AuthorizationCode();
|
||||
final UUID code = UUID.randomUUID();
|
||||
ac.setAuthorizationCode(code);
|
||||
ac.setTaxReturnUuid(acq.getTaxReturnUuid());
|
||||
ac.setTaxYear(acq.getTaxYear());
|
||||
Timestamp expireAt =
|
||||
Timestamp.from(Instant.now().plusSeconds(authorizationCodeExpiresInterval));
|
||||
ac.setExpiresAt(expireAt);
|
||||
ac.setStateCode(acq.getStateCode());
|
||||
ac.setSubmissionId(acq.getSubmissionId());
|
||||
|
||||
return acRepo.save(ac).thenReturn(code).onErrorMap(e -> {
|
||||
log.error(
|
||||
"createAuthorizationCode() failed for taxReturnId={}, submissionId={}, taxYear={}. Exception: {}. Error: {}",
|
||||
acq.getTaxReturnUuid(),
|
||||
acq.getSubmissionId(),
|
||||
acq.getTaxYear(),
|
||||
e.getClass().getName(),
|
||||
e.getMessage());
|
||||
throw new StateApiException(StateApiErrorCode.E_INTERNAL_SERVER_ERROR);
|
||||
});
|
||||
} else {
|
||||
final StateApiErrorCode errorCode;
|
||||
if (!status.exists()) {
|
||||
errorCode = StateApiErrorCode.E_TAX_RETURN_NOT_FOUND;
|
||||
} else if (acceptedOnly) {
|
||||
errorCode = StateApiErrorCode.E_TAX_RETURN_NOT_ACCEPTED;
|
||||
} else {
|
||||
errorCode = StateApiErrorCode.E_TAX_RETURN_NOT_ACCEPTED_OR_PENDING;
|
||||
}
|
||||
|
||||
log.error(
|
||||
"createAuthorizationCode() failed for taxReturnId={}, submissionId={}, taxYear={}. Error: {}",
|
||||
acq.getTaxReturnUuid(),
|
||||
acq.getSubmissionId(),
|
||||
acq.getTaxYear(),
|
||||
errorCode.name());
|
||||
throw new StateApiException(errorCode);
|
||||
}
|
||||
})
|
||||
.flatMap(uuidMono -> uuidMono.map(uuid -> uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<String> generateAuthorizationToken(AuthCodeRequest acRequest) {
|
||||
log.info("Generating authorization token");
|
||||
|
||||
// The `dfClient.getStatus()` function returns the status along with an indicator of whether the XML exists in
|
||||
// the S3 bucket. However, it's important to consider the issue of eventual consistency, which refers to the
|
||||
// delay in propagating S3 objects. While we may verify the existence of the object in the west region, there's
|
||||
// a possibility that when performing the `export-tax-return` operation, retrieval occurs from the east region,
|
||||
// potentially resulting in a 'tax-return-not-found' error.
|
||||
return getAcceptedOnlyFlag(acRequest.getStateCode())
|
||||
.zipWith(
|
||||
dfClient.getStatus(
|
||||
acRequest.getTaxYear(), acRequest.getTaxReturnUuid(), acRequest.getSubmissionId()),
|
||||
(acceptedOnly, status) -> {
|
||||
var isAccepted = status.status().equalsIgnoreCase(DirectFileClient.STATUS_ACCEPTED);
|
||||
var isPending = status.status().equalsIgnoreCase(DirectFileClient.STATUS_PENDING);
|
||||
|
||||
boolean submissionOk = status.exists() && (isAccepted || (!acceptedOnly && isPending));
|
||||
if (submissionOk) {
|
||||
return authorizationTokenService
|
||||
.generateAndEncrypt(
|
||||
mapper.convertValue(acRequest, AuthorizationTokenClaims.class))
|
||||
.onErrorMap(e -> {
|
||||
log.error(
|
||||
"generateAuthorizationToken() failed for taxReturnId={}, submissionId={}, taxYear={}. Exception: {}. Error: {}",
|
||||
acRequest.getTaxReturnUuid(),
|
||||
acRequest.getSubmissionId(),
|
||||
acRequest.getTaxYear(),
|
||||
e.getClass().getName(),
|
||||
e.getMessage());
|
||||
throw new StateApiException(StateApiErrorCode.E_INTERNAL_SERVER_ERROR);
|
||||
});
|
||||
} else {
|
||||
StateApiErrorCode errorCode;
|
||||
if (!status.exists()) {
|
||||
errorCode = StateApiErrorCode.E_TAX_RETURN_NOT_FOUND;
|
||||
} else if (acceptedOnly) {
|
||||
errorCode = StateApiErrorCode.E_TAX_RETURN_NOT_ACCEPTED;
|
||||
} else {
|
||||
errorCode = StateApiErrorCode.E_TAX_RETURN_NOT_ACCEPTED_OR_PENDING;
|
||||
}
|
||||
|
||||
log.error(
|
||||
"generateAuthorizationToken() failed for taxReturnId={}, submissionId={}, taxYear={}. Error: {}",
|
||||
acRequest.getTaxReturnUuid(),
|
||||
acRequest.getSubmissionId(),
|
||||
acRequest.getTaxYear(),
|
||||
errorCode.name());
|
||||
throw new StateApiException(errorCode);
|
||||
}
|
||||
})
|
||||
.flatMap(jwtMono -> jwtMono.map(token -> token));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<StateAndAuthCode> verifyJwtSignature(String jwtToken, String accountId) {
|
||||
log.info("enter verifyJwtSignature()...");
|
||||
|
||||
return getStateProfile(accountId)
|
||||
.zipWhen(sp -> {
|
||||
return retrievePublicKeyFromCert(sp);
|
||||
})
|
||||
.map(tuple -> {
|
||||
try {
|
||||
ClientJwtClaim claim = JwtVerifier.verifyJwt(jwtToken, tuple.getT2());
|
||||
return new StateAndAuthCode(
|
||||
claim.getAuthorizationCode(), tuple.getT1().getStateCode());
|
||||
} catch (FileNotFoundException e) {
|
||||
log.error(
|
||||
"verifyJwtSignature() failed, cannot locate the public key file, {}, error: {}",
|
||||
e.getClass(),
|
||||
e.getMessage());
|
||||
throw new StateApiException(StateApiErrorCode.E_CERTIFICATE_NOT_FOUND);
|
||||
} catch (CertificateExpiredException e) {
|
||||
log.error(
|
||||
"verifyJwtSignature() failed, the certificate ({}) for {} has expired, {}, error: {}",
|
||||
tuple.getT1().getCertLocation(),
|
||||
tuple.getT1().getAccountId(),
|
||||
e.getClass(),
|
||||
e.getMessage());
|
||||
throw new StateApiException(StateApiErrorCode.E_CERTIFICATE_EXPIRED);
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"verifyJwtSignature() failed, {}, error: {}",
|
||||
e.getClass().getName(),
|
||||
e.getMessage());
|
||||
throw new StateApiException(StateApiErrorCode.E_JWT_VERIFICATION_FAILED);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<AuthorizationCode> authorize(StateAndAuthCode saCode) {
|
||||
log.info("enter authorize()...");
|
||||
|
||||
UUID authorizationCode;
|
||||
|
||||
try {
|
||||
authorizationCode = UUID.fromString(saCode.getAuthorizationCode());
|
||||
} catch (IllegalArgumentException exception) {
|
||||
log.error("authorize() failed, authorization code is not in a valid UUID format");
|
||||
throw new StateApiException(StateApiErrorCode.E_AUTHORIZATION_CODE_INVALID_FORMAT);
|
||||
}
|
||||
|
||||
// Returns authorization-code entity if authorizationCode exists and is valid
|
||||
// otherwise exception
|
||||
return getAuthorizationCode(authorizationCode).flatMap(ac -> {
|
||||
// The state code used to generate the authorization code must match the state
|
||||
// code associated with
|
||||
// the requester's accountId
|
||||
String authorizationCodeStateCode = ac.getStateCode();
|
||||
String stateProfileStateCode = saCode.getStateCode();
|
||||
if (!stateProfileStateCode.equals(authorizationCodeStateCode)) {
|
||||
log.error(
|
||||
"authorize() failed, mismatched state code, state_profile state code : {}, authorization_code state code: {}",
|
||||
stateProfileStateCode,
|
||||
authorizationCodeStateCode);
|
||||
throw new StateApiException(StateApiErrorCode.E_MISMATCHED_STATE_CODE);
|
||||
}
|
||||
|
||||
// The authorization code must not be expired
|
||||
Timestamp expiresAt = ac.getExpiresAt();
|
||||
if (expiresAt.getTime() < System.currentTimeMillis()) {
|
||||
log.error("authorize() failed, authorization code has expired");
|
||||
throw new StateApiException(StateApiErrorCode.E_AUTHORIZATION_CODE_EXPIRED);
|
||||
}
|
||||
|
||||
return Mono.just(ac);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<TaxReturnXml> retrieveTaxReturnXml(int taxYear, UUID taxReturnUuid, String submissionId) {
|
||||
log.info("enter retrieveTaxReturnXml()...taxReturnUuid={}", taxReturnUuid);
|
||||
|
||||
return dfClient.getStatus(taxYear, taxReturnUuid, submissionId).flatMap(taxReturnStatus -> {
|
||||
if (STATUS_REJECTED.equalsIgnoreCase(taxReturnStatus.status())) {
|
||||
log.error("Requested tax return has status of 'rejected'");
|
||||
return Mono.error(new StateApiException(StateApiErrorCode.E_TAX_RETURN_NOT_ACCEPTED_OR_PENDING));
|
||||
}
|
||||
|
||||
if (STATUS_ERROR.equalsIgnoreCase(taxReturnStatus.status())) {
|
||||
log.error("Requested tax return has status of 'error'");
|
||||
throw new StateApiException(StateApiErrorCode.E_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
return stateApiS3Client
|
||||
.getTaxReturnXml(taxYear, taxReturnUuid, submissionId)
|
||||
.map(xmlString -> new TaxReturnXml(taxReturnStatus.status().toLowerCase(), submissionId, xmlString))
|
||||
.onErrorMap(e -> {
|
||||
log.error(
|
||||
"getTaxReturnXml() failed for taxYear={}, taxReturnId={}, submissionId={}, {}, error: {}",
|
||||
taxYear,
|
||||
taxReturnUuid.toString(),
|
||||
submissionId,
|
||||
e.getClass().getName(),
|
||||
e.getMessage());
|
||||
return new StateApiException(StateApiErrorCode.E_INTERNAL_SERVER_ERROR);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Map<String, Object>> retrieveExportedFacts(String submissionId, String stateCode, String accountId) {
|
||||
log.info(
|
||||
"enter retrieveExportedFacts()...submissionId={}, acctId={}, enabled={}",
|
||||
submissionId,
|
||||
accountId,
|
||||
exported_facts_enabled);
|
||||
if (exported_facts_enabled) {
|
||||
return efClient.getExportedFacts(submissionId, stateCode, accountId)
|
||||
.map(GetStateExportedFactsResponse::exportedFacts);
|
||||
}
|
||||
return Mono.just(new HashMap<>());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<EncryptData> encryptTaxReturn(TaxReturnToExport taxReturn, String accountId) {
|
||||
log.info("enter encryptTaxReturn()...accountId={}", accountId);
|
||||
|
||||
// Compared AES 256 CBC, CCM and GCM, we chose GCM for combining data
|
||||
// confidentiality and integrity/authentication in a highly efficient manner.
|
||||
return getStateProfile(accountId)
|
||||
.flatMap(sp -> {
|
||||
return retrievePublicKeyFromCert(sp);
|
||||
})
|
||||
.map(publicKey -> {
|
||||
try {
|
||||
// 1. generate secret
|
||||
byte[] secret = generatePassword();
|
||||
|
||||
// 2. generate a random initialization vector for AES CBC
|
||||
byte[] iv = generateIV();
|
||||
|
||||
// 3. encrypt the xml data with AES 256 GCM
|
||||
var gsonBuilder = new GsonBuilder();
|
||||
gsonBuilder.serializeNulls();
|
||||
Gson gson = gsonBuilder.create();
|
||||
AesGcmEncryptionResult encryptedResult = aesGcmEncrypt(gson.toJson(taxReturn), secret, iv);
|
||||
|
||||
// 4. Encode the encrypted xml
|
||||
String encodedAndEncryptedData = Base64.toBase64String(encryptedResult.ciphertext());
|
||||
|
||||
// 5. encrypt the secret with state's public key and encode with base64
|
||||
byte[] encryptedSecret = rsaEncryptWithPublicKey(secret, publicKey);
|
||||
|
||||
String encodedSecret = Base64.toBase64String(encryptedSecret);
|
||||
|
||||
// 6. encode iv
|
||||
String encodedIV = Base64.toBase64String(iv);
|
||||
|
||||
// 7. encode authentication tag
|
||||
String encodedAuthenticationTag = Base64.toBase64String(encryptedResult.authenticationTag());
|
||||
|
||||
return new EncryptData(
|
||||
encodedSecret, encodedIV, encodedAndEncryptedData, encodedAuthenticationTag);
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"encryptTaxReturnXml() failed, {}, error: {}",
|
||||
e.getClass().getName(),
|
||||
e.getMessage());
|
||||
throw new StateApiException(StateApiErrorCode.E_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Boolean> getAcceptedOnlyFlag(String stateCode) {
|
||||
log.info("enter getAcceptedOnlyFlag...");
|
||||
|
||||
return lookupStateProfile(stateCode)
|
||||
.doOnSuccess(s -> log.info("getAcceptedOnlyFlag completes successfully, flag: {}", s.acceptedOnly()))
|
||||
.doOnError(er -> log.error("getAcceptedOnlyFlag fails: {}", er.getMessage()))
|
||||
.flatMap(sp -> Mono.just(sp.acceptedOnly()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<StateProfileDTO> lookupStateProfile(String stateCode) {
|
||||
return cachedDS.getStateProfileByStateCode(stateCode).map(dto -> {
|
||||
if (dto.archived()) {
|
||||
log.error("State {} is archived", stateCode);
|
||||
throw new StateApiException(StateApiErrorCode.E_ACCOUNT_ARCHIVED);
|
||||
}
|
||||
return dto;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<StateProfile> getStateProfile(String accountId) {
|
||||
return cachedDS.getStateProfile(accountId).map(sp -> {
|
||||
if (sp.getArchived()) {
|
||||
log.error("State {} (account_id={}) is archived", sp.getStateCode(), accountId);
|
||||
throw new StateApiException(StateApiErrorCode.E_ACCOUNT_ARCHIVED);
|
||||
}
|
||||
return sp;
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<AuthorizationCode> getAuthorizationCode(UUID authorizationCode) {
|
||||
log.info("enter getAuthorizationCode()...");
|
||||
|
||||
AuthorizationCode code = new AuthorizationCode();
|
||||
code.setAuthorizationCode(authorizationCode);
|
||||
return acRepo.getByAuthorizationCode(code.getAuthorizationCode())
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
log.error(
|
||||
"getAuthorizationCode() failed, authorization code {} does not exist in authorization_code table",
|
||||
authorizationCode);
|
||||
return Mono.error(new StateApiException(StateApiErrorCode.E_AUTHORIZATION_CODE_NOT_EXIST));
|
||||
}))
|
||||
.onErrorMap(e -> !(e instanceof StateApiException), e -> {
|
||||
log.error(
|
||||
"getAuthorizationCode() failed, {}, error: {}",
|
||||
e.getClass().getName(),
|
||||
e.getMessage());
|
||||
return new StateApiException(StateApiErrorCode.E_INTERNAL_SERVER_ERROR);
|
||||
});
|
||||
}
|
||||
|
||||
// Check if using default cert in lower env
|
||||
private Mono<PublicKey> retrievePublicKeyFromCert(StateProfile sp) {
|
||||
String certOverride = certProperties.getCertLocationOverride();
|
||||
String cert = (StringUtils.isBlank(certOverride)) ? sp.getCertLocation() : certOverride;
|
||||
OffsetDateTime expDate = (StringUtils.isBlank(certOverride))
|
||||
? sp.getCertExpirationDate()
|
||||
: OffsetDateTime.now().plusYears(1);
|
||||
log.info("Use CertOverride={} to retrieve public key.", certOverride);
|
||||
return cachedDS.retrievePublicKeyFromCert(cert, expDate);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package gov.irs.directfile.stateapi.utils;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class IPAddress {
|
||||
private static String X_FORWARDED_FOR = "X-Forwarded-For";
|
||||
|
||||
public String getClientIpAddress(HttpServletRequest request) {
|
||||
String addr = request.getHeader(X_FORWARDED_FOR);
|
||||
if (addr == null || addr.isEmpty()) {
|
||||
addr = request.getRemoteAddr();
|
||||
} else {
|
||||
String[] addrs = addr.split(",");
|
||||
addr = addrs[0];
|
||||
}
|
||||
return addr;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
# set debug: true to log CONDITIONS EVALUATION REPORT
|
||||
debug: false
|
||||
logging:
|
||||
config: classpath:logback-debug.xml
|
||||
file:
|
||||
name: spring-boot-debug.log
|
||||
clean-history-on-start: true
|
||||
management:
|
||||
endpoints:
|
||||
enabled-by-default: true
|
||||
web:
|
||||
discovery:
|
||||
enabled: true
|
||||
exposure:
|
||||
include: "*"
|
||||
endpoint:
|
||||
env:
|
||||
show-values: ALWAYS
|
||||
configprops:
|
||||
show-values: ALWAYS
|
|
@ -0,0 +1,20 @@
|
|||
spring:
|
||||
liquibase:
|
||||
contexts: dev
|
||||
|
||||
authorization-token:
|
||||
signing-key: GTc+SlI7C7ECPHAhAvIWqn2yAvzAGMVj
|
||||
|
||||
aws:
|
||||
enabled: false
|
||||
s3:
|
||||
accessKey: accessKey
|
||||
secretKey: secretKey
|
||||
region: us-west-2
|
||||
certBucketName: cert-bucket
|
||||
endPoint: http://s3.localhost.localstack.cloud:4566/
|
||||
|
||||
direct-file:
|
||||
local-encryption:
|
||||
local-wrapping-key: ${LOCAL_WRAPPING_KEY}
|
||||
cert-location-override: fakestate.cer
|
|
@ -0,0 +1,19 @@
|
|||
server:
|
||||
port: 8080
|
||||
|
||||
spring:
|
||||
r2dbc:
|
||||
url: r2dbc:postgresql://state-api-db:5432/stateapi
|
||||
liquibase:
|
||||
url: jdbc:postgresql://state-api-db:5432/stateapi
|
||||
contexts: docker
|
||||
|
||||
aws:
|
||||
s3:
|
||||
accessKey: accessKey
|
||||
secretKey: secretKey
|
||||
region: us-west-2
|
||||
endPoint: http://s3.localhost.localstack.cloud:4566
|
||||
|
||||
direct-file:
|
||||
backend-url: http://api:${DF_API_PORT:8080}/
|
91
direct-file/state-api/src/main/resources/application.yaml
Normal file
91
direct-file/state-api/src/main/resources/application.yaml
Normal file
|
@ -0,0 +1,91 @@
|
|||
server:
|
||||
port: ${STATEAPI_PORT:8081}
|
||||
shutdown: graceful
|
||||
|
||||
logging:
|
||||
config: classpath:logback.xml
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
username: postgres
|
||||
password: postgres
|
||||
r2dbc:
|
||||
url: r2dbc:postgresql://localhost:${STATEAPI_DB_PORT:5433}/stateapi
|
||||
username: ${spring.datasource.username}
|
||||
password: ${spring.datasource.password}
|
||||
liquibase:
|
||||
change-log: classpath:db/changelog.yaml
|
||||
url: jdbc:postgresql://localhost:${STATEAPI_DB_PORT:5433}/stateapi
|
||||
user: ${spring.datasource.username}
|
||||
password: ${spring.datasource.password}
|
||||
# "If you do not specify any contexts in the CLI at runtime, every changeset in your changelog runs, even if they
|
||||
# have contextFilters attached"
|
||||
# Setting contexts in the base application.yaml ensures that contextFilters overridden in other profiles are respected
|
||||
contexts: base
|
||||
cache:
|
||||
TTL-minutes: 120
|
||||
type: caffeine # Enable caching with Caffeine, use "none" to disable caching
|
||||
|
||||
aws:
|
||||
default-credentials-provider-chain-enabled: false
|
||||
s3:
|
||||
default-credentials-provider-chain-enabled: false
|
||||
accessKey: accessKey
|
||||
secretKey: secretKey
|
||||
region: us-east-1
|
||||
endPoint: http://localhost:4566
|
||||
certBucketName: cert-bucket
|
||||
taxReturnXmlBucketName: direct-file-taxreturns
|
||||
prefix:
|
||||
|
||||
authorization-code:
|
||||
expires-interval-seconds: 600
|
||||
|
||||
# now we are pointing to direct-file-status app to get status and also xml file, and backend to get exported facts
|
||||
direct-file:
|
||||
backend-url: http://localhost:8080/
|
||||
backend-context-path: /df/file/api
|
||||
backend-api-version: v1
|
||||
status:
|
||||
mock: false
|
||||
url: ${direct-file.backend-url}
|
||||
exported-facts:
|
||||
enabled: true
|
||||
mock: false
|
||||
url: ${direct-file.backend-url}
|
||||
management:
|
||||
endpoint:
|
||||
health:
|
||||
enabled: true
|
||||
endpoints:
|
||||
enabled-by-default: false
|
||||
web:
|
||||
discovery:
|
||||
enabled: false
|
||||
exposure:
|
||||
include: health
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /api-docs
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
supported-submit-methods: []
|
||||
|
||||
feature-flags:
|
||||
export-return:
|
||||
variants:
|
||||
on: true
|
||||
off: false
|
||||
default-variant: on
|
||||
|
||||
openfeature-starter:
|
||||
s3-provider:
|
||||
environment-prefix:
|
||||
bucket: direct-file-taxreturns
|
||||
object: "feature-flags.json"
|
||||
expiration: 15m
|
||||
|
||||
xml-sanitized:
|
||||
allowed-headers:
|
||||
excluded-tags:
|
|
@ -0,0 +1,19 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIC/TCCAeWgAwIBAgIUb0j9n0LLrRKshU09iqiA3XOJ7V4wDQYJKoZIhvcNAQEL
|
||||
BQAwJzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkZTMQswCQYDVQQHDAJEQzAeFw0y
|
||||
NDA4MzAxNTIwNTNaFw0yNTA4MzAxNTIwNTNaMCcxCzAJBgNVBAYTAlVTMQswCQYD
|
||||
VQQIDAJGUzELMAkGA1UEBwwCREMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
|
||||
AoIBAQDd8FAPO2dC0iLtMXb7TGEzUHoG9f5fhExyXfoZe25Xx/RH3aBzRTdb2RDG
|
||||
AVENfV6u6rubZD3NVs6EVw5AYZ3+KDcZgSoogAU4t3LxASmX246QSeqjZs34WBYj
|
||||
PG+yZ5ndxGF1QV1pGjuEoK+dk/KgKhduiJLYcw3fyYcreClAGsfONTW+AeBpms6V
|
||||
Zu6awh8XGLwxJ/CLJa3OEIXGbM/l3pXt0Uu0GVOJ2a3qf5lJXD2t/umNtI6fOASB
|
||||
d1lEUMsBK7wrehgK8vgkBFDGZKLpvN3U/c9vK9obm8rUAOcd6KYOqIiK/ly1/1aJ
|
||||
leSGo/3Jv9NdaqRNPpUfODZxDAZ1AgMBAAGjITAfMB0GA1UdDgQWBBTdXF467ChC
|
||||
IEY8hh+sn0kQXH+ndzANBgkqhkiG9w0BAQsFAAOCAQEAVPD0lS3k8A4IhqHAPTTG
|
||||
ScTNW6E2bMrzkKc6YBvxGfcqXyJgrH0eyiqf5RG3G+2iM9RSiDBIP3IPshEcRYO8
|
||||
+auN8TjLnm24mLIQlgtwEx2n8/nvUDxuE979XGIu4G5+xdHhajpsKR7B9yWIZQcm
|
||||
nxkTZaI9tTgFu6/6kjdQocPBH1V0pFaQTIeiFA6TTnhhM9weQf/zLpg5azMV8Oqc
|
||||
QPuI1NKoWF611iMxo2MHudYEWDW6OPXzEfNJr/LI03PvIFoeQGWTW10PdnC5W+yn
|
||||
RgYgKQWHegXkRhvHYcnIXtatEeicFqtVbpwT8+zJv2wEE23FXOe5anOuF3Lm7Jhs
|
||||
TQ==
|
||||
-----END CERTIFICATE-----
|
|
@ -0,0 +1,7 @@
|
|||
databaseChangeLog:
|
||||
- preConditions:
|
||||
onFail: HALT
|
||||
onError: HALT
|
||||
- includeAll:
|
||||
path: migrations/
|
||||
relativeToChangelogFile: true
|
|
@ -0,0 +1,136 @@
|
|||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: initial-schema-1
|
||||
author: abcde
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: authorization_code
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: serial
|
||||
constraints:
|
||||
nullable: false
|
||||
primaryKey: true
|
||||
primaryKeyName: authorization_code_pkey
|
||||
- column:
|
||||
name: tax_return_uuid
|
||||
type: uuid
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: tax_year
|
||||
type: int
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: authorization_code
|
||||
type: char(64)
|
||||
constraints:
|
||||
unique: true
|
||||
nullable: false
|
||||
- column:
|
||||
name: created_at
|
||||
type: timestamp
|
||||
defaultValueComputed: 'CURRENT_TIMESTAMP'
|
||||
- column:
|
||||
name: expires_at
|
||||
type: timestamp
|
||||
defaultValueComputed: 'CURRENT_TIMESTAMP + interval ''120 minutes'''
|
||||
- createTable:
|
||||
tableName: state_profile
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: serial
|
||||
constraints:
|
||||
nullable: false
|
||||
primaryKey: true
|
||||
primaryKeyName: state_profile_pkey
|
||||
- column:
|
||||
name: account_id
|
||||
type: varchar(10)
|
||||
constraints:
|
||||
unique: true
|
||||
nullable: false
|
||||
- column:
|
||||
name: state_code
|
||||
type: char(2)
|
||||
constraints:
|
||||
nullable: false
|
||||
unique: true
|
||||
- column:
|
||||
name: tax_system_name
|
||||
type: varchar
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: landing_url
|
||||
type: varchar
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: default_redirect_url
|
||||
type: varchar
|
||||
constraints:
|
||||
nullable: true
|
||||
- column:
|
||||
name: accepted_only
|
||||
type: boolean
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: cert_location
|
||||
type: text
|
||||
constraints:
|
||||
nullable: true
|
||||
- column:
|
||||
name: created_at
|
||||
type: timestamp
|
||||
defaultValueComputed: 'CURRENT_TIMESTAMP'
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: cert_expiration_date
|
||||
type: timestamp with time zone
|
||||
defaultValueComputed: (CURRENT_TIMESTAMP + INTERVAL '1 year')
|
||||
constraints:
|
||||
nullable: true
|
||||
- createTable:
|
||||
tableName: state_redirect
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: serial
|
||||
constraints:
|
||||
nullable: false
|
||||
primaryKey: true
|
||||
primaryKeyName: state_redirect_pkey
|
||||
- column:
|
||||
name: state_profile_id
|
||||
type: serial
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: redirect_url
|
||||
type: varchar(255)
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: created_at
|
||||
type: timestamp
|
||||
defaultValueComputed: 'CURRENT_TIMESTAMP'
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: expires_at
|
||||
type: timestamp
|
||||
- addForeignKeyConstraint:
|
||||
baseColumnNames: state_profile_id
|
||||
baseTableName: state_redirect
|
||||
constraintName: state_profile_id_fkey
|
||||
deferrable: false
|
||||
initiallyDeferred: false
|
||||
referencedColumnNames: id
|
||||
referencedTableName: state_profile
|
||||
validate: true
|
|
@ -0,0 +1,22 @@
|
|||
databaseChangeLog:
|
||||
- changeSet:
|
||||
# Note this change deletes the testing data from the
|
||||
# `load-test-authorization-codes` changeSet
|
||||
id: delete-all-preexisting-authorization-codes
|
||||
author: abcde
|
||||
changes:
|
||||
- delete:
|
||||
tableName: authorization_code
|
||||
where: 1=1
|
||||
- changeSet:
|
||||
id: authorization-code-addColumn-state-code
|
||||
author: bl
|
||||
changes:
|
||||
- addColumn:
|
||||
tableName: authorization_code
|
||||
columns:
|
||||
- column:
|
||||
name: state_code
|
||||
type: char(2)
|
||||
constraints:
|
||||
nullable: false
|
|
@ -0,0 +1,14 @@
|
|||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: state-profile-addColumn-archived
|
||||
author: abcde
|
||||
changes:
|
||||
- addColumn:
|
||||
tableName: state_profile
|
||||
columns:
|
||||
- column:
|
||||
name: archived
|
||||
type: boolean
|
||||
defaultValueBoolean: false
|
||||
constraints:
|
||||
nullable: false
|
|
@ -0,0 +1,28 @@
|
|||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: state-profile-add-cancel-url-columns
|
||||
author: abcde
|
||||
changes:
|
||||
- addColumn:
|
||||
tableName: state_profile
|
||||
columns:
|
||||
- column:
|
||||
name: transfer_cancel_url
|
||||
type: varchar
|
||||
constraints:
|
||||
nullable: true
|
||||
- addColumn:
|
||||
tableName: state_profile
|
||||
columns:
|
||||
- column:
|
||||
name: waiting_for_acceptance_cancel_url
|
||||
type: varchar
|
||||
constraints:
|
||||
nullable: true
|
||||
rollback:
|
||||
- dropColumn:
|
||||
tableName: state_profile
|
||||
columnName: transfer_cancel_url
|
||||
- dropColumn:
|
||||
tableName: state_profile
|
||||
columnName: waiting_for_acceptance_cancel_url
|
|
@ -0,0 +1,13 @@
|
|||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: auth-codes-add-submission-id-column
|
||||
author: abcde
|
||||
changes:
|
||||
- addColumn:
|
||||
tableName: authorization_code
|
||||
columns:
|
||||
- column:
|
||||
name: submission_id
|
||||
type: varchar(20)
|
||||
constraints:
|
||||
nullable: true
|
|
@ -0,0 +1,44 @@
|
|||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 202403081531-00-add-state-language-table
|
||||
author: abcde
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: state_language
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: serial
|
||||
constraints:
|
||||
nullable: false
|
||||
primaryKey: true
|
||||
primaryKeyName: state_language_pkey
|
||||
- column:
|
||||
name: state_profile_id
|
||||
type: serial
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: df_language_code
|
||||
type: char(2)
|
||||
constraints:
|
||||
nullable: false
|
||||
- column:
|
||||
name: state_language_code
|
||||
# Assuming this is enough characters if a state uses a full string for the language name in their URL
|
||||
# or something. Can always increase with a migration later
|
||||
type: varchar(50)
|
||||
constraints:
|
||||
nullable: false
|
||||
- addForeignKeyConstraint:
|
||||
baseColumnNames: state_profile_id
|
||||
baseTableName: state_language
|
||||
constraintName: state_profile_id_fkey
|
||||
deferrable: false
|
||||
initiallyDeferred: false
|
||||
referencedColumnNames: id
|
||||
referencedTableName: state_profile
|
||||
validate: true
|
||||
rollback:
|
||||
- dropTable:
|
||||
tableName: state_language
|
|
@ -0,0 +1,17 @@
|
|||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: state-profile-add-filing-requirements-url-column
|
||||
author: abcde
|
||||
changes:
|
||||
- addColumn:
|
||||
tableName: state_profile
|
||||
columns:
|
||||
- column:
|
||||
name: filing_requirements_url
|
||||
type: varchar
|
||||
constraints:
|
||||
nullable: true
|
||||
rollback:
|
||||
- dropColumn:
|
||||
tableName: state_profile
|
||||
columnName: filing_requirements_url
|
|
@ -0,0 +1,17 @@
|
|||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: state-profile-add-department-of-revenue-url-column
|
||||
author: abcde
|
||||
changes:
|
||||
- addColumn:
|
||||
tableName: state_profile
|
||||
columns:
|
||||
- column:
|
||||
name: department_of_revenue_url
|
||||
type: varchar
|
||||
constraints:
|
||||
nullable: true
|
||||
rollback:
|
||||
- dropColumn:
|
||||
tableName: state_profile
|
||||
columnName: department_of_revenue_url
|
11
direct-file/state-api/src/main/resources/logback-debug.xml
Normal file
11
direct-file/state-api/src/main/resources/logback-debug.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
|
||||
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
|
||||
<include resource="org/springframework/boot/logging/logback/file-appender.xml" />
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
<logger name="org.springframework.web" level="DEBUG" />
|
||||
</configuration>
|
40
direct-file/state-api/src/main/resources/logback.xml
Normal file
40
direct-file/state-api/src/main/resources/logback.xml
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE configuration>
|
||||
<configuration>
|
||||
<appender name="ConsoleJSON" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
|
||||
|
||||
<fieldNames>
|
||||
<timestamp>timestamp</timestamp>
|
||||
<version>[ignore]</version>
|
||||
<levelValue>[ignore]</levelValue>
|
||||
</fieldNames>
|
||||
<timestampPattern>yyyy-MM-dd'T'HH:mm:ss.SSS'Z'</timestampPattern>
|
||||
<timeZone>UTC</timeZone>
|
||||
<customFields>{"system":"DIRECTFILE","version":"${GIT_COMMIT_HASH}"}</customFields>
|
||||
|
||||
<!-- Only include the following fields from the Mapped Diagnostic Context (MDC). -->
|
||||
<!-- Other MDC fields will be excluded. -->
|
||||
|
||||
<includeMdcKeyName>eventId</includeMdcKeyName>
|
||||
<includeMdcKeyName>eventStatus</includeMdcKeyName>
|
||||
<includeMdcKeyName>eventType</includeMdcKeyName>
|
||||
<includeMdcKeyName>eventErrorMessage</includeMdcKeyName>
|
||||
<includeMdcKeyName>responseStatusCode</includeMdcKeyName>
|
||||
<includeMdcKeyName>system</includeMdcKeyName>
|
||||
<includeMdcKeyName>taxPeriod</includeMdcKeyName>
|
||||
<includeMdcKeyName>taxReturnType</includeMdcKeyName>
|
||||
<includeMdcKeyName>stateId</includeMdcKeyName>
|
||||
<includeMdcKeyName>userType</includeMdcKeyName>
|
||||
<includeMdcKeyName>cyberOnly</includeMdcKeyName>
|
||||
<includeMdcKeyName>taxReturnId</includeMdcKeyName>
|
||||
<includeMdcKeyName>detail</includeMdcKeyName>
|
||||
|
||||
<throwableConverter class="gov.irs.directfile.stateapi.audit.NoMessageStackTraceConverter" />
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="ConsoleJSON" />
|
||||
</root>
|
||||
</configuration>
|
|
@ -0,0 +1,115 @@
|
|||
package gov.irs.directfile.stateapi;
|
||||
|
||||
import java.security.Security;
|
||||
import java.util.*;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import software.amazon.encryption.s3.S3AsyncEncryptionClient;
|
||||
|
||||
import gov.irs.directfile.stateapi.configuration.S3ConfigurationProperties;
|
||||
import gov.irs.directfile.stateapi.encryption.JwtSigner;
|
||||
import gov.irs.directfile.stateapi.model.ExportResponse;
|
||||
import gov.irs.directfile.stateapi.repository.AuthorizationCodeRepository;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
|
||||
properties = {"server.port=8082"})
|
||||
@ActiveProfiles({"development", "test", "integration-test"})
|
||||
@Slf4j
|
||||
@SuppressWarnings("null")
|
||||
@EnabledIfSystemProperty(named = "runIntegrationTests", matches = "true")
|
||||
public class StateApiAppNoOverrideTest {
|
||||
|
||||
private static final String PRIVATE_KEY_PATH = "src/test/resources/certificates/fakestate.key";
|
||||
private static final String URL_EXPORT = "http://localhost:8082/state-api/export-return";
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
private final HttpHeaders headers = new HttpHeaders();
|
||||
private HttpEntity<Map<String, Object>> requestEntity;
|
||||
|
||||
@Autowired
|
||||
AuthorizationCodeRepository authorizationCodeRepository;
|
||||
|
||||
@Autowired
|
||||
S3AsyncEncryptionClient s3AsyncEncryptionClient;
|
||||
|
||||
@Autowired
|
||||
S3ConfigurationProperties s3ConfigurationProperties;
|
||||
|
||||
@DynamicPropertySource
|
||||
static void properties(DynamicPropertyRegistry registry) {
|
||||
log.info("Set cert-location-override=false");
|
||||
registry.add("direct-file.cert-location-override", () -> "");
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void integrationTests_CertificateExpired() throws Exception {
|
||||
|
||||
String ac = "d6e34be4-11df-4b5e-808f-bc48a9f4870b";
|
||||
String header = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}";
|
||||
String payload = "{\"iss\":\"112233\",\"sub\":\"" + ac + "\",\"iat\":1516239022}";
|
||||
|
||||
// Create the JWT
|
||||
String jwtToken = JwtSigner.createJwt(header, payload, PRIVATE_KEY_PATH);
|
||||
// Set the desired header(s)
|
||||
headers.set("account-id", "112233");
|
||||
headers.set("Authorization", "Bearer " + jwtToken);
|
||||
|
||||
requestEntity = new HttpEntity<>(headers);
|
||||
var response = restTemplate.exchange(URL_EXPORT, HttpMethod.GET, requestEntity, ExportResponse.class);
|
||||
|
||||
ExportResponse responseBody = response.getBody();
|
||||
|
||||
assertNotNull(responseBody);
|
||||
assertEquals("error", responseBody.getStatus());
|
||||
assertEquals("E_CERTIFICATE_EXPIRED", responseBody.getError());
|
||||
|
||||
log.info("Test succeeded with expected error: E_CERTIFICATE_EXPIRED");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void integrationTests_InternalServerError() throws Exception {
|
||||
|
||||
String ac = "d6e34be4-11df-4b5e-808f-bc48a9f4870b";
|
||||
String header = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}";
|
||||
String payload = "{\"iss\":\"112234\",\"sub\":\"" + ac + "\",\"iat\":1516239022}";
|
||||
|
||||
// Create the JWT
|
||||
String jwtToken = JwtSigner.createJwt(header, payload, PRIVATE_KEY_PATH);
|
||||
// Set the desired header(s)
|
||||
headers.set("account-id", "112234");
|
||||
headers.set("Authorization", "Bearer " + jwtToken);
|
||||
|
||||
requestEntity = new HttpEntity<>(headers);
|
||||
var response = restTemplate.exchange(URL_EXPORT, HttpMethod.GET, requestEntity, ExportResponse.class);
|
||||
|
||||
ExportResponse responseBody = response.getBody();
|
||||
|
||||
assertNotNull(responseBody);
|
||||
assertEquals("error", responseBody.getStatus());
|
||||
assertEquals("E_INTERNAL_SERVER_ERROR", responseBody.getError());
|
||||
|
||||
log.info("Test succeeded with expected error: E_INTERNAL_SERVER_ERROR");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,450 @@
|
|||
package gov.irs.directfile.stateapi;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.Security;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.time.Period;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import software.amazon.awssdk.services.s3.model.DeleteObjectResponse;
|
||||
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
|
||||
import software.amazon.encryption.s3.S3AsyncEncryptionClient;
|
||||
|
||||
import gov.irs.directfile.dto.AuthCodeResponse;
|
||||
import gov.irs.directfile.stateapi.configuration.S3ConfigurationProperties;
|
||||
import gov.irs.directfile.stateapi.encryption.JwtSigner;
|
||||
import gov.irs.directfile.stateapi.model.AuthCodeRequest;
|
||||
import gov.irs.directfile.stateapi.model.AuthorizationCode;
|
||||
import gov.irs.directfile.stateapi.model.ExportResponse;
|
||||
import gov.irs.directfile.stateapi.model.TaxReturnToExport;
|
||||
import gov.irs.directfile.stateapi.repository.AuthorizationCodeRepository;
|
||||
|
||||
import static gov.irs.directfile.stateapi.encryption.Decryptor.aesGcmDecrypt;
|
||||
import static gov.irs.directfile.stateapi.encryption.Decryptor.rsaDecryptWithPrivateKey;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
|
||||
properties = {"server.port=8081"})
|
||||
@ActiveProfiles({"development", "test", "integration-test"})
|
||||
@Slf4j
|
||||
@SuppressWarnings("null")
|
||||
@EnabledIfSystemProperty(named = "runIntegrationTests", matches = "true")
|
||||
public class StateApiAppTest {
|
||||
private static final String PRIVATE_KEY_PATH = "src/test/resources/certificates/fakestate.key";
|
||||
private static final String SUBMISSION_ID = "someSubmissionId";
|
||||
private static final String TAX_YEAR = "2022";
|
||||
private static final String TAX_RETURN_ID = "ae019609-99e0-4ef5-85bb-ad90dc302e70";
|
||||
private static final String XML_OBJECT_KEY =
|
||||
TAX_YEAR + "/taxreturns/" + TAX_RETURN_ID + "/submissions/" + SUBMISSION_ID + ".xml";
|
||||
private static final String AUTHORIZATION_CODE_URL = "http://localhost:8081/state-api/authorization-code";
|
||||
private static final String EXPORT_RETURN_URL = "http://localhost:8081/state-api/export-return";
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
private final HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
@Autowired
|
||||
AuthorizationCodeRepository authorizationCodeRepository;
|
||||
|
||||
@Autowired
|
||||
S3AsyncEncryptionClient s3AsyncEncryptionClient;
|
||||
|
||||
@Autowired
|
||||
S3ConfigurationProperties s3ConfigurationProperties;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
private void putEncryptedXml() {
|
||||
Path resourceDir = Paths.get("src/test/resources/xml/sample-export.xml");
|
||||
// Call putObject to encrypt the object and upload it to S3
|
||||
CompletableFuture<PutObjectResponse> futurePut = s3AsyncEncryptionClient.putObject(
|
||||
builder -> builder.bucket(s3ConfigurationProperties.getTaxReturnXmlBucketName())
|
||||
.key(XML_OBJECT_KEY)
|
||||
.build(),
|
||||
resourceDir);
|
||||
// Block on completion of the futurePut
|
||||
futurePut.join();
|
||||
}
|
||||
|
||||
private void deleteEncryptedXml() {
|
||||
CompletableFuture<DeleteObjectResponse> futureDelete = s3AsyncEncryptionClient.deleteObject(
|
||||
builder -> builder.bucket(s3ConfigurationProperties.getTaxReturnXmlBucketName())
|
||||
.key(XML_OBJECT_KEY)
|
||||
.build());
|
||||
futureDelete.join();
|
||||
}
|
||||
|
||||
private HttpEntity<AuthCodeRequest> createAuthCodeRequestBodyAndEntity(String uuid, int taxYear, String stateCode) {
|
||||
var tin = "213456789";
|
||||
|
||||
AuthCodeRequest requestObject =
|
||||
new AuthCodeRequest(UUID.fromString(uuid), tin, taxYear, stateCode, SUBMISSION_ID);
|
||||
|
||||
return new HttpEntity<>(requestObject, headers);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void integrationTests_successfulResponse() throws Exception {
|
||||
putEncryptedXml();
|
||||
var authCodeRequestEntity = createAuthCodeRequestBodyAndEntity(TAX_RETURN_ID, Integer.parseInt(TAX_YEAR), "FS");
|
||||
ResponseEntity<AuthCodeResponse> authCodeResponse =
|
||||
restTemplate.postForEntity(AUTHORIZATION_CODE_URL, authCodeRequestEntity, AuthCodeResponse.class);
|
||||
|
||||
UUID ac = authCodeResponse.getBody().getAuthCode();
|
||||
|
||||
String header = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}";
|
||||
String payload = "{\"iss\":\"123456\",\"sub\":\"" + ac.toString() + "\",\"iat\":1632736766}";
|
||||
|
||||
// Create the JWT
|
||||
String jwtToken = JwtSigner.createJwt(header, payload, PRIVATE_KEY_PATH);
|
||||
|
||||
// Set the desired header(s)
|
||||
headers.set("Authorization", "Bearer " + jwtToken);
|
||||
|
||||
var exportReturnRequestEntity = new HttpEntity<>(headers);
|
||||
var exportResponse =
|
||||
restTemplate.exchange(EXPORT_RETURN_URL, HttpMethod.GET, exportReturnRequestEntity, String.class);
|
||||
if (Objects.requireNonNull(exportResponse.getBody()).contains("error"))
|
||||
throw new AssertionError("Integration tests failed unexpectedly!");
|
||||
|
||||
String encodedSecret = exportResponse.getHeaders().get("SESSION-KEY").getFirst();
|
||||
String encodedIV =
|
||||
exportResponse.getHeaders().get("INITIALIZATION-VECTOR").getFirst();
|
||||
String encodedAuthenticationTag =
|
||||
exportResponse.getHeaders().get("AUTHENTICATION-TAG").getFirst();
|
||||
|
||||
String taxReturn = exportResponse.getBody();
|
||||
ObjectMapper om = new ObjectMapper();
|
||||
ExportResponse response = om.readValue(taxReturn.getBytes(), ExportResponse.class);
|
||||
|
||||
if (!response.getStatus().equals("success")) throw new AssertionError("Integration tests failed unexpectedly!");
|
||||
|
||||
byte[] secret = rsaDecryptWithPrivateKey(Base64.getDecoder().decode(encodedSecret), PRIVATE_KEY_PATH);
|
||||
|
||||
// decrypt the data using the secret and iv
|
||||
byte[] decryptedBytes = aesGcmDecrypt(
|
||||
Base64.getDecoder().decode(response.getTaxReturn()),
|
||||
secret,
|
||||
Base64.getDecoder().decode(encodedIV),
|
||||
Base64.getDecoder().decode(encodedAuthenticationTag));
|
||||
|
||||
TaxReturnToExport taxReturnData = om.readValue(decryptedBytes, TaxReturnToExport.class);
|
||||
|
||||
assertEquals(taxReturnData.getStatus(), "accepted");
|
||||
assertEquals(taxReturnData.getSubmissionId(), SUBMISSION_ID);
|
||||
assertNotNull(taxReturnData.getXml());
|
||||
assertFalse(taxReturnData.getDirectFileData().isEmpty());
|
||||
assertTrue(taxReturnData.getDirectFileData().containsKey("familyAndHousehold"));
|
||||
|
||||
var familyAndHousehold =
|
||||
(List<Map<String, Object>>) taxReturnData.getDirectFileData().get("familyAndHousehold");
|
||||
var expectedPerson = new HashMap<>();
|
||||
expectedPerson.put("firstName", "Sammy");
|
||||
expectedPerson.put("middleInitial", null);
|
||||
expectedPerson.put("lastName", "Smith");
|
||||
expectedPerson.put("suffix", "I");
|
||||
expectedPerson.put("dateOfBirth", "2013-01-21");
|
||||
|
||||
expectedPerson.put("tin", "200-01-1234");
|
||||
expectedPerson.put("relationship", "biologicalChild");
|
||||
expectedPerson.put("isClaimedDependent", true);
|
||||
expectedPerson.put("eligibleDependent", true);
|
||||
expectedPerson.put("residencyDuration", "allYear");
|
||||
expectedPerson.put("monthsLivedWithTPInUS", "twelve");
|
||||
expectedPerson.put("ssnNotValidForEmployment", false);
|
||||
expectedPerson.put("qualifyingChild", true);
|
||||
expectedPerson.put("hohQualifyingPerson", true);
|
||||
expectedPerson.put("scheduleEicLine4bYes", false);
|
||||
expectedPerson.put("scheduleEicLine4aYes", true);
|
||||
expectedPerson.put("scheduleEicLine4aNo", false);
|
||||
|
||||
assertEquals(1, familyAndHousehold.size());
|
||||
assertEquals(expectedPerson, familyAndHousehold.getFirst());
|
||||
|
||||
var filers =
|
||||
(List<Map<String, Object>>) taxReturnData.getDirectFileData().get("filers");
|
||||
|
||||
var expectedFiler1 = new HashMap<>();
|
||||
expectedFiler1.put("firstName", "Samuel");
|
||||
expectedFiler1.put("middleInitial", null);
|
||||
expectedFiler1.put("lastName", "Smith");
|
||||
expectedFiler1.put("suffix", "Jr");
|
||||
expectedFiler1.put("dateOfBirth", "1985-09-29");
|
||||
expectedFiler1.put("tin", "100-01-1234");
|
||||
expectedFiler1.put("isPrimaryFiler", true);
|
||||
expectedFiler1.put("ssnNotValidForEmployment", false);
|
||||
expectedFiler1.put("educatorExpenses", "200.00");
|
||||
expectedFiler1.put("hsaTotalDeductibleAmount", "600.00");
|
||||
expectedFiler1.put("isDisabled", false);
|
||||
expectedFiler1.put("isStudent", false);
|
||||
expectedFiler1.put("interestReportsTotal", "3000.00");
|
||||
expectedFiler1.put("form1099GsTotal", "15000.00");
|
||||
|
||||
var expectedFiler2 = new HashMap<>();
|
||||
expectedFiler2.put("firstName", "Judy");
|
||||
expectedFiler2.put("middleInitial", null);
|
||||
expectedFiler2.put("lastName", "Johnson");
|
||||
expectedFiler2.put("suffix", null);
|
||||
expectedFiler2.put("dateOfBirth", "1985-10-18");
|
||||
expectedFiler2.put("tin", "100-02-1234");
|
||||
expectedFiler2.put("isPrimaryFiler", false);
|
||||
expectedFiler2.put("ssnNotValidForEmployment", false);
|
||||
expectedFiler2.put("educatorExpenses", "100.00");
|
||||
expectedFiler2.put("hsaTotalDeductibleAmount", "500.00");
|
||||
expectedFiler2.put("isDisabled", false);
|
||||
expectedFiler2.put("isStudent", false);
|
||||
expectedFiler2.put("interestReportsTotal", "300.00");
|
||||
expectedFiler2.put("form1099GsTotal", "1500.00");
|
||||
assertEquals(2, filers.size());
|
||||
assertThat(filers, containsInAnyOrder(expectedFiler1, expectedFiler2));
|
||||
|
||||
var intReports =
|
||||
(List<Map<String, Object>>) taxReturnData.getDirectFileData().get("interestReports");
|
||||
var intRpt = new HashMap<String, Object>();
|
||||
intRpt.put("has1099", true);
|
||||
intRpt.put("1099Amount", "800.00");
|
||||
intRpt.put("interestOnGovernmentBonds", "300");
|
||||
intRpt.put("taxExemptInterest", "200.00");
|
||||
intRpt.put("recipientTin", "123-45-6789");
|
||||
intRpt.put("no1099Amount", null);
|
||||
intRpt.put("payer", "JPM Bank");
|
||||
intRpt.put("payerTin", "01-1234567");
|
||||
intRpt.put("taxWithheld", "120");
|
||||
intRpt.put("taxExemptAndTaxCreditBondCusipNo", "01234567A");
|
||||
assertEquals(1, intReports.size());
|
||||
assertEquals(intRpt, intReports.getFirst());
|
||||
|
||||
var form1099Gs =
|
||||
(List<Map<String, Object>>) taxReturnData.getDirectFileData().get("form1099Gs");
|
||||
var form1099G = new HashMap<String, Object>();
|
||||
form1099G.put("has1099", true);
|
||||
form1099G.put("recipientTin", "123-45-6789");
|
||||
form1099G.put("payer", "State of california");
|
||||
form1099G.put("payerTin", "321-54-9876");
|
||||
form1099G.put("amount", "100.00");
|
||||
form1099G.put("federalTaxWithheld", "20.00");
|
||||
form1099G.put("stateIdNumber", "123456");
|
||||
form1099G.put("stateTaxWithheld", "10.00");
|
||||
form1099G.put("amountPaidBackForBenefitsInTaxYear", "25.00");
|
||||
assertEquals(1, form1099Gs.size());
|
||||
assertEquals(form1099G, form1099Gs.getFirst());
|
||||
|
||||
// socialSecurityReports
|
||||
var ssRpts =
|
||||
(List<Map<String, Object>>) taxReturnData.getDirectFileData().get("socialSecurityReports");
|
||||
var ssRpt = new HashMap<String, Object>();
|
||||
ssRpt.put("recipientTin", "123-45-6789");
|
||||
ssRpt.put("netBenefits", "21000.00");
|
||||
ssRpt.put("formType", "SSA-1099");
|
||||
assertEquals(1, ssRpts.size());
|
||||
assertEquals(ssRpt, ssRpts.getFirst());
|
||||
|
||||
var formW2s =
|
||||
(List<Map<String, Object>>) taxReturnData.getDirectFileData().get("formW2s");
|
||||
assertEquals(1, formW2s.size());
|
||||
var expectedW2 = new HashMap<String, Object>();
|
||||
expectedW2.put("unionDuesAmount", "40.00");
|
||||
expectedW2.put("BOX14_NJ_UIHCWD", "101.00");
|
||||
expectedW2.put("BOX14_NJ_UIWFSWF", "101.00");
|
||||
assertEquals(expectedW2, formW2s.getFirst());
|
||||
|
||||
deleteEncryptedXml();
|
||||
log.info("Integration tests succeeded");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void integrationTests_AccountIdNotExist() throws Exception {
|
||||
|
||||
String ac = "d6e34be4-11df-4b5e-808f-bc48a9f4870b";
|
||||
String header = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}";
|
||||
String payload = "{\"iss\":\"12345\",\"sub\":\"" + ac + "\",\"iat\":1632736766}";
|
||||
|
||||
// Create the JWT
|
||||
String jwtToken = JwtSigner.createJwt(header, payload, PRIVATE_KEY_PATH);
|
||||
|
||||
// Set the desired header(s)
|
||||
headers.set("account-id", "12345");
|
||||
headers.set("Authorization", "Bearer " + jwtToken);
|
||||
var exportReturnRequestEntity = new HttpEntity<>(headers);
|
||||
|
||||
var response = restTemplate.exchange(
|
||||
EXPORT_RETURN_URL, HttpMethod.GET, exportReturnRequestEntity, ExportResponse.class);
|
||||
|
||||
ExportResponse responseBody = response.getBody();
|
||||
|
||||
assertNotNull(responseBody);
|
||||
assertEquals("error", responseBody.getStatus());
|
||||
assertEquals("E_ACCOUNT_ID_NOT_EXIST", responseBody.getError());
|
||||
|
||||
log.info("Test succeeded with expected error: E_ACCOUNT_ID_NOT_EXIST");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void integrationTests_BearerTokenMissing() throws Exception {
|
||||
String ac = "d6e34be4-11df-4b5e-808f-bc48a9f4870b";
|
||||
String header = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}";
|
||||
String payload = "{\"iss\":\"123456\",\"sub\":\"" + ac + "\",\"iat\":1632736766}";
|
||||
|
||||
// Create the JWT
|
||||
String jwtToken = JwtSigner.createJwt(header, payload, PRIVATE_KEY_PATH);
|
||||
// Set the desired header(s)
|
||||
headers.set("account-id", "123456");
|
||||
headers.set("Authorization", "NoBearer " + jwtToken);
|
||||
|
||||
var exportRequest = new HttpEntity<>(headers);
|
||||
var exportResponse = restTemplate.exchange(EXPORT_RETURN_URL, HttpMethod.GET, exportRequest, String.class);
|
||||
|
||||
String responseBody = exportResponse.getBody();
|
||||
|
||||
assertNotNull(responseBody);
|
||||
assertTrue(responseBody.startsWith("{\"status\":\"error\""));
|
||||
assertTrue(responseBody.contains("E_BEARER_TOKEN_MISSING"));
|
||||
|
||||
log.info("Test succeeded with expected error: E_BEARER_TOKEN_MISSING");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void integrationTests_AuthCodeExpired() throws Exception {
|
||||
String ac = "2511725b-094f-4d1f-a7f7-da6003a5f8e7";
|
||||
String header = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}";
|
||||
String payload = "{\"iss\":\"123456\",\"sub\":\"" + ac + "\",\"iat\":1516239022}";
|
||||
|
||||
// add test data if not exists
|
||||
AuthorizationCode acObject = new AuthorizationCode();
|
||||
acObject.setAuthorizationCode(UUID.fromString(ac));
|
||||
var existingAuthorizationCodeObject = authorizationCodeRepository
|
||||
.getByAuthorizationCode(acObject.getAuthorizationCode())
|
||||
.block();
|
||||
|
||||
if (existingAuthorizationCodeObject == null) {
|
||||
log.info("Adding test case data");
|
||||
acObject.setTaxReturnUuid(UUID.randomUUID());
|
||||
acObject.setTaxYear(2022);
|
||||
Timestamp expireAt = Timestamp.from(Instant.now().minus(Period.ofDays(30)));
|
||||
acObject.setExpiresAt(expireAt);
|
||||
acObject.setStateCode("FS");
|
||||
acObject.setSubmissionId("someSubmissionId");
|
||||
|
||||
authorizationCodeRepository.save(acObject).block();
|
||||
} else {
|
||||
log.info("Test case data already exists: {}", existingAuthorizationCodeObject);
|
||||
}
|
||||
|
||||
// Create the JWT
|
||||
String jwtToken = JwtSigner.createJwt(header, payload, PRIVATE_KEY_PATH);
|
||||
// Set the desired header(s)
|
||||
headers.set("account-id", "123456");
|
||||
headers.set("Authorization", "Bearer " + jwtToken);
|
||||
|
||||
var exportRequest = new HttpEntity<>(headers);
|
||||
var exportResponse = restTemplate.exchange(EXPORT_RETURN_URL, HttpMethod.GET, exportRequest, String.class);
|
||||
|
||||
String responseBody = exportResponse.getBody();
|
||||
|
||||
assertNotNull(responseBody);
|
||||
assertTrue(responseBody.startsWith("{\"status\":\"error\""));
|
||||
assertTrue(responseBody.contains("E_AUTHORIZATION_CODE_EXPIRED"));
|
||||
|
||||
log.info("Test succeeded with expected error: E_AUTHORIZATION_CODE_EXPIRED");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void integrationTests_AuthCodeInvalidFormat() throws Exception {
|
||||
String ac = "11df-4b5e-808f-bc48a9f4870b";
|
||||
String header = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}";
|
||||
String payload = "{\"iss\":\"123456\",\"sub\":\"" + ac + "\",\"iat\":1516239022}";
|
||||
|
||||
// Create the JWT
|
||||
String jwtToken = JwtSigner.createJwt(header, payload, PRIVATE_KEY_PATH);
|
||||
// Set the desired header(s)
|
||||
headers.set("account-id", "123456");
|
||||
headers.set("Authorization", "Bearer " + jwtToken);
|
||||
|
||||
var exportRequestEntity = new HttpEntity<>(headers);
|
||||
var exportResponse =
|
||||
restTemplate.exchange(EXPORT_RETURN_URL, HttpMethod.GET, exportRequestEntity, ExportResponse.class);
|
||||
|
||||
ExportResponse responseBody = exportResponse.getBody();
|
||||
|
||||
assertNotNull(responseBody);
|
||||
assertEquals("error", responseBody.getStatus());
|
||||
assertEquals("E_AUTHORIZATION_CODE_INVALID_FORMAT", responseBody.getError());
|
||||
|
||||
log.info("Test succeeded with expected error: E_AUTHORIZATION_CODE_INVALID_FORMAT");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void integrationTests_AuthCodeNotExist() throws Exception {
|
||||
String ac = "d6e34be4-11df-4b5e-808f-bc48a9f4870c";
|
||||
String header = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}";
|
||||
String payload = "{\"iss\":\"123456\",\"sub\":\"" + ac + "\",\"iat\":1516239022}";
|
||||
|
||||
// Create the JWT
|
||||
String jwtToken = JwtSigner.createJwt(header, payload, PRIVATE_KEY_PATH);
|
||||
// Set the desired header(s)
|
||||
headers.set("account-id", "123456");
|
||||
headers.set("Authorization", "Bearer " + jwtToken);
|
||||
|
||||
var exportRequestEntity = new HttpEntity<>(headers);
|
||||
var exportResponse =
|
||||
restTemplate.exchange(EXPORT_RETURN_URL, HttpMethod.GET, exportRequestEntity, ExportResponse.class);
|
||||
|
||||
ExportResponse responseBody = exportResponse.getBody();
|
||||
|
||||
assertNotNull(responseBody);
|
||||
assertEquals("error", responseBody.getStatus());
|
||||
assertEquals("E_AUTHORIZATION_CODE_NOT_EXIST", responseBody.getError());
|
||||
|
||||
log.info("Test succeeded with expected error: E_AUTHORIZATION_CODE_NOT_EXIST");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void integrationTests_JWTVerificationFailed() throws Exception {
|
||||
|
||||
String ac = "d6e34be4-11df-4b5e-808f-bc48a9f4870c";
|
||||
String header = "{\"alg\":\"RS256\",\"typ\":\"JWT\"}";
|
||||
String payload = "{\"iss\":\"123456\",\"sub\":\"" + ac + "\",\"iat\":1516239022}";
|
||||
var badPrivateKeyPath = "src/test/resources/certificates/fakestate_bad.key";
|
||||
|
||||
// Create the JWT
|
||||
String jwtToken = JwtSigner.createJwt(header, payload, badPrivateKeyPath);
|
||||
// Set the desired header(s)
|
||||
headers.set("account-id", "123456");
|
||||
headers.set("Authorization", "Bearer " + jwtToken);
|
||||
|
||||
var exportRequestEntity = new HttpEntity<>(headers);
|
||||
var exportResponse =
|
||||
restTemplate.exchange(EXPORT_RETURN_URL, HttpMethod.GET, exportRequestEntity, ExportResponse.class);
|
||||
|
||||
ExportResponse responseBody = exportResponse.getBody();
|
||||
|
||||
assertNotNull(responseBody);
|
||||
assertEquals("error", responseBody.getStatus());
|
||||
assertEquals("E_JWT_VERIFICATION_FAILED", responseBody.getError());
|
||||
|
||||
log.info("Test succeeded with expected error: E_JWT_VERIFICATION_FAILED");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package gov.irs.directfile.stateapi.audit;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.LoggerContext;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.read.ListAppender;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import gov.irs.directfile.stateapi.events.Event;
|
||||
import gov.irs.directfile.stateapi.events.EventId;
|
||||
import gov.irs.directfile.stateapi.events.EventStatus;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class AuditServiceTest {
|
||||
|
||||
private AuditService auditSvc = new AuditService();
|
||||
private Logger auditLogger;
|
||||
private ListAppender<ILoggingEvent> listAppender;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
|
||||
auditLogger = (Logger) LoggerFactory.getLogger(AuditService.class);
|
||||
listAppender = new ListAppender<>();
|
||||
listAppender.setContext((LoggerContext) LoggerFactory.getILoggerFactory());
|
||||
listAppender.start();
|
||||
auditLogger.addAppender(listAppender);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
auditLogger.detachAppender(listAppender);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogEvent_success() throws Exception {
|
||||
|
||||
Event event = createAuditEvent(
|
||||
EventId.CREATE_AUTHORIZATION_CODE, EventStatus.SUCCESS, HttpResponseStatus.OK.toString(), null);
|
||||
auditSvc.logEvent(event);
|
||||
|
||||
List<ILoggingEvent> logsList = listAppender.list;
|
||||
assertEquals(Level.INFO, logsList.get(0).getLevel());
|
||||
assertEquals(logsList.get(0).getMDCPropertyMap().get("eventId"), EventId.CREATE_AUTHORIZATION_CODE.toString());
|
||||
assertEquals(logsList.get(0).getMDCPropertyMap().get("eventStatus"), EventStatus.SUCCESS.toString());
|
||||
assertEquals(logsList.get(0).getMDCPropertyMap().get("responseStatusCode"), HttpResponseStatus.OK.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogEvent_failure() throws Exception {
|
||||
|
||||
String errMsg = "Internal Server Error";
|
||||
Event event = createAuditEvent(
|
||||
EventId.CREATE_AUTHORIZATION_CODE,
|
||||
EventStatus.FAILURE,
|
||||
HttpResponseStatus.INTERNAL_SERVER_ERROR.toString(),
|
||||
errMsg);
|
||||
auditSvc.logEvent(event);
|
||||
|
||||
List<ILoggingEvent> logsList = listAppender.list;
|
||||
assertEquals(Level.ERROR, logsList.get(0).getLevel());
|
||||
assertEquals(logsList.get(0).getMDCPropertyMap().get("eventId"), EventId.CREATE_AUTHORIZATION_CODE.toString());
|
||||
assertEquals(logsList.get(0).getMDCPropertyMap().get("eventStatus"), EventStatus.FAILURE.toString());
|
||||
assertEquals(
|
||||
logsList.get(0).getMDCPropertyMap().get("responseStatusCode"),
|
||||
HttpResponseStatus.INTERNAL_SERVER_ERROR.toString());
|
||||
assertEquals(logsList.get(0).getMDCPropertyMap().get("eventErrorMessage"), errMsg);
|
||||
}
|
||||
|
||||
private Event createAuditEvent(EventId id, EventStatus eventStatus, String responseStatus, String errorMsg) {
|
||||
|
||||
Event event = Event.builder()
|
||||
.eventId(id)
|
||||
.eventStatus(eventStatus)
|
||||
.responseStatusCode(responseStatus)
|
||||
.eventErrorMessage(errorMsg)
|
||||
.taxPeriod("2022")
|
||||
.userType("SYS")
|
||||
.stateId("FS")
|
||||
.remoteAddress("127.0.0.1")
|
||||
.build();
|
||||
return event;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package gov.irs.directfile.stateapi.authorization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nimbusds.jose.JOSEException;
|
||||
import com.nimbusds.jose.JWSVerifier;
|
||||
import com.nimbusds.jose.crypto.MACVerifier;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import gov.irs.directfile.models.encryption.DataEncryptDecrypt;
|
||||
import gov.irs.directfile.stateapi.model.AuthCodeRequest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@SpringBootTest
|
||||
@ActiveProfiles({"test", "token-integration-test"})
|
||||
public class AuthorizationTokenServiceIntegrationTest {
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Autowired
|
||||
AuthorizationTokenService authorizationTokenService;
|
||||
|
||||
@Autowired
|
||||
DataEncryptDecrypt dataEncryptDecrypt;
|
||||
|
||||
@Test
|
||||
public void givenTokenWithEncryptedTaxReturnClaims_whenClaimsDecrypted_thenClaimsMatchOriginalRequest() {
|
||||
// given
|
||||
AuthCodeRequest authCodeRequest =
|
||||
new AuthCodeRequest(UUID.randomUUID(), "123-00-4567", 2023, "MA", "123456789AB");
|
||||
AuthorizationTokenClaims claimsMap = mapper.convertValue(authCodeRequest, AuthorizationTokenClaims.class);
|
||||
StepVerifier.create(authorizationTokenService.generateAndEncrypt(claimsMap))
|
||||
.assertNext((token) -> {
|
||||
try {
|
||||
// when
|
||||
byte[] ciphertext = Base64.getUrlDecoder().decode(token);
|
||||
byte[] decrypted = dataEncryptDecrypt.decrypt(ciphertext);
|
||||
SignedJWSParts signedJWSParts = mapper.readValue(decrypted, SignedJWSParts.class);
|
||||
String signedJWS =
|
||||
String.join(".", signedJWSParts.s1(), signedJWSParts.s2(), signedJWSParts.s3());
|
||||
|
||||
SignedJWT signedJWT = SignedJWT.parse(signedJWS);
|
||||
JWTClaimsSet jwtClaimsSet = signedJWT.getJWTClaimsSet();
|
||||
AuthorizationTokenClaims decryptedClaims = mapper.convertValue(
|
||||
jwtClaimsSet.toJSONObject().get("tax-return-export-metadata"),
|
||||
AuthorizationTokenClaims.class);
|
||||
|
||||
// then
|
||||
assertEquals(authCodeRequest.getTaxReturnUuid().toString(), decryptedClaims.taxReturnUuid());
|
||||
assertEquals(authCodeRequest.getTaxYear(), decryptedClaims.taxYear());
|
||||
assertEquals(authCodeRequest.getStateCode(), decryptedClaims.stateCode());
|
||||
assertEquals(authCodeRequest.getSubmissionId(), decryptedClaims.submissionId());
|
||||
} catch (ParseException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.expectComplete()
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenASignedToken_whenVerifyingSignature_thenTokenSignatureIsValid() {
|
||||
// given
|
||||
AuthCodeRequest authCodeRequest =
|
||||
new AuthCodeRequest(UUID.randomUUID(), "123-00-4567", 2023, "MA", "123456789AB");
|
||||
AuthorizationTokenClaims claimsMap = mapper.convertValue(authCodeRequest, AuthorizationTokenClaims.class);
|
||||
StepVerifier.create(authorizationTokenService.generateAndEncrypt(claimsMap))
|
||||
.assertNext((token) -> {
|
||||
try {
|
||||
// when
|
||||
JWSVerifier signatureVerifier = new MACVerifier("GTc+SlI7C7ECPHAhAvIWqn2yAvzAGMVj");
|
||||
byte[] ciphertext = Base64.getUrlDecoder().decode(token);
|
||||
byte[] decrypted = dataEncryptDecrypt.decrypt(ciphertext);
|
||||
SignedJWSParts signedJWSParts = mapper.readValue(decrypted, SignedJWSParts.class);
|
||||
String signedJWS =
|
||||
String.join(".", signedJWSParts.s1(), signedJWSParts.s2(), signedJWSParts.s3());
|
||||
|
||||
SignedJWT signedJWT = SignedJWT.parse(signedJWS);
|
||||
|
||||
// then
|
||||
assertTrue(signedJWT.verify(signatureVerifier));
|
||||
} catch (ParseException | JOSEException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.expectComplete()
|
||||
.verify();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,209 @@
|
|||
package gov.irs.directfile.stateapi.authorization;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.ParseException;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
import com.amazonaws.encryptionsdk.exception.AwsCryptoException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.nimbusds.jose.*;
|
||||
import com.nimbusds.jose.crypto.MACSigner;
|
||||
import com.nimbusds.jose.crypto.MACVerifier;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
import lombok.SneakyThrows;
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import gov.irs.directfile.models.encryption.DataEncryptDecrypt;
|
||||
import gov.irs.directfile.stateapi.model.AuthCodeRequest;
|
||||
|
||||
import static gov.irs.directfile.stateapi.authorization.AuthorizationTokenService.EXPORT_CLAIM_KEY;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class AuthorizationTokenServiceTest {
|
||||
private static final String TEST_SIGNING_KEY = "GTc+SlI7C7ECPHAhAvIWqn2yAvzAGMVj";
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
public AuthorizationTokenService initializeService(DataEncryptDecrypt dataEncryptDecrypt) {
|
||||
return new AuthorizationTokenService(dataEncryptDecrypt, TEST_SIGNING_KEY, 60);
|
||||
}
|
||||
|
||||
private AuthorizationTokenClaims setupClaims() {
|
||||
AuthCodeRequest authCodeRequest =
|
||||
new AuthCodeRequest(UUID.randomUUID(), "123-00-4567", 2023, "MA", "123456789AB");
|
||||
return mapper.convertValue(authCodeRequest, AuthorizationTokenClaims.class);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private byte[] setupSignedJwt(AuthorizationTokenClaims claims) {
|
||||
JWSSigner signer = new MACSigner(TEST_SIGNING_KEY);
|
||||
Instant issuedAt = Instant.now();
|
||||
JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder()
|
||||
.claim(EXPORT_CLAIM_KEY, claims)
|
||||
.issueTime(Date.from(issuedAt))
|
||||
.expirationTime(Date.from(issuedAt.plusSeconds(60)))
|
||||
.build();
|
||||
JWSObject jwsObject = new JWSObject(
|
||||
new JWSHeader.Builder(JWSAlgorithm.HS256).build(), new Payload(jwtClaimsSet.toJSONObject()));
|
||||
jwsObject.sign(signer);
|
||||
String signedJwt = jwsObject.serialize();
|
||||
return signedJwt.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenTaxReturnSubmissionClaims_whenGeneratingAuthorizationToken_thenEncryptorIsCalled() {
|
||||
// given
|
||||
DataEncryptDecrypt dataEncryptDecrypt = mock(DataEncryptDecrypt.class);
|
||||
AuthorizationTokenService authorizationTokenService = initializeService(dataEncryptDecrypt);
|
||||
AuthorizationTokenClaims authorizationTokenClaims = setupClaims();
|
||||
|
||||
// when
|
||||
// NOTE: We aren't stubbing the encryptor's return value, so a NullPointerException will be thrown.
|
||||
// That's ok because we're not testing what the encryptor returns,
|
||||
// we're just testing that the encryptor is invoked
|
||||
assertThrows(NullPointerException.class, () -> authorizationTokenService
|
||||
.generateAndEncrypt(authorizationTokenClaims)
|
||||
.block());
|
||||
|
||||
// then
|
||||
verify(dataEncryptDecrypt).encrypt(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
givenTaxReturnSubmissionClaims_whenGeneratingAuthorizationToken_thenEncryptorIsCalledWithProperContext() {
|
||||
// given
|
||||
DataEncryptDecrypt dataEncryptDecrypt = mock(DataEncryptDecrypt.class);
|
||||
AuthorizationTokenService authorizationTokenService = initializeService(dataEncryptDecrypt);
|
||||
AuthorizationTokenClaims authorizationTokenClaims = setupClaims();
|
||||
|
||||
// when
|
||||
// NOTE: We aren't stubbing the encryptor's return value, so a NullPointerException will be thrown.
|
||||
// That's ok because we're not testing what the encryptor returns,
|
||||
// we're just testing that an encryption context is passed into the encryptor
|
||||
assertThrows(NullPointerException.class, () -> authorizationTokenService
|
||||
.generateAndEncrypt(authorizationTokenClaims)
|
||||
.block());
|
||||
ArgumentCaptor<Map<String, String>> encryptionContextCaptor = ArgumentCaptor.captor();
|
||||
|
||||
// then
|
||||
verify(dataEncryptDecrypt).encrypt(any(), encryptionContextCaptor.capture());
|
||||
assertEquals(Map.of("system", "DIRECT-FILE", "type", "STATE-API"), encryptionContextCaptor.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenGeneratingAuthorizationToken_whenAnExceptionIsThrown_thenItPropagates() {
|
||||
// given
|
||||
DataEncryptDecrypt dataEncryptDecrypt = mock(DataEncryptDecrypt.class);
|
||||
AuthorizationTokenService authorizationTokenService = initializeService(dataEncryptDecrypt);
|
||||
|
||||
// when
|
||||
when(dataEncryptDecrypt.encrypt(any(), any())).thenThrow(AwsCryptoException.class);
|
||||
AuthorizationTokenClaims claimsMap = setupClaims();
|
||||
|
||||
// then
|
||||
StepVerifier.create(authorizationTokenService.generateAndEncrypt(claimsMap))
|
||||
.expectError(AwsCryptoException.class)
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenTaxReturnSubmissionClaims_whenGeneratingAuthorizationToken_thenTokenIsSignedWithProperKey() {
|
||||
// given
|
||||
DataEncryptDecrypt dataEncryptDecrypt = mock(DataEncryptDecrypt.class);
|
||||
AuthorizationTokenService authorizationTokenService = initializeService(dataEncryptDecrypt);
|
||||
AuthorizationTokenClaims tokenClaims = setupClaims();
|
||||
byte[] signedJwtBytes = setupSignedJwt(tokenClaims);
|
||||
when(dataEncryptDecrypt.encrypt(any(), any())).thenReturn(signedJwtBytes);
|
||||
|
||||
// when
|
||||
authorizationTokenService.generateAndEncrypt(tokenClaims).block();
|
||||
|
||||
// then
|
||||
ArgumentCaptor<byte[]> signedTokenCaptor = ArgumentCaptor.captor();
|
||||
verify(dataEncryptDecrypt).encrypt(signedTokenCaptor.capture(), anyMap());
|
||||
byte[] signedTokenArgument = signedTokenCaptor.getValue();
|
||||
try {
|
||||
JWSVerifier signatureVerifier = new MACVerifier(TEST_SIGNING_KEY);
|
||||
SignedJWSParts signedJWSParts = mapper.readValue(signedTokenArgument, SignedJWSParts.class);
|
||||
String signedJWS = String.join(".", signedJWSParts.s1(), signedJWSParts.s2(), signedJWSParts.s3());
|
||||
SignedJWT signedJWT = SignedJWT.parse(signedJWS);
|
||||
assertTrue(signedJWT.verify(signatureVerifier));
|
||||
} catch (JOSEException | ParseException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
givenTaxReturnSubmissionClaims_whenGeneratingAuthorizationToken_thenTokenContainsEncryptedExportIdentifiers() {
|
||||
DataEncryptDecrypt dataEncryptDecrypt = mock(DataEncryptDecrypt.class);
|
||||
AuthorizationTokenService authorizationTokenService = initializeService(dataEncryptDecrypt);
|
||||
|
||||
// given
|
||||
AuthorizationTokenClaims tokenClaims = setupClaims();
|
||||
byte[] signedJwtBytes = setupSignedJwt(tokenClaims);
|
||||
when(dataEncryptDecrypt.encrypt(any(), any())).thenReturn(signedJwtBytes);
|
||||
|
||||
// when
|
||||
authorizationTokenService.generateAndEncrypt(tokenClaims).block();
|
||||
|
||||
// then
|
||||
ArgumentCaptor<byte[]> signedTokenCaptor = ArgumentCaptor.captor();
|
||||
verify(dataEncryptDecrypt).encrypt(signedTokenCaptor.capture(), anyMap());
|
||||
|
||||
byte[] signedTokenArgument = signedTokenCaptor.getValue();
|
||||
try {
|
||||
SignedJWSParts signedJWSParts = mapper.readValue(signedTokenArgument, SignedJWSParts.class);
|
||||
String signedJWS = String.join(".", signedJWSParts.s1(), signedJWSParts.s2(), signedJWSParts.s3());
|
||||
SignedJWT signedJWT = SignedJWT.parse(signedJWS);
|
||||
JWTClaimsSet jwtClaimsSet = signedJWT.getJWTClaimsSet();
|
||||
AuthorizationTokenClaims taxReturnExportClaim = mapper.convertValue(
|
||||
jwtClaimsSet.toJSONObject().get("tax-return-export-metadata"), AuthorizationTokenClaims.class);
|
||||
assertEquals(tokenClaims, taxReturnExportClaim);
|
||||
} catch (ParseException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenTaxReturnSubmissionClaims_whenGeneratingAuthorizationToken_thenExpirationIsSet() {
|
||||
DataEncryptDecrypt dataEncryptDecrypt = mock(DataEncryptDecrypt.class);
|
||||
AuthorizationTokenService authorizationTokenService = initializeService(dataEncryptDecrypt);
|
||||
|
||||
// given
|
||||
AuthorizationTokenClaims tokenClaims = setupClaims();
|
||||
byte[] signedJwtBytes = setupSignedJwt(tokenClaims);
|
||||
when(dataEncryptDecrypt.encrypt(any(), any())).thenReturn(signedJwtBytes);
|
||||
|
||||
// when
|
||||
authorizationTokenService.generateAndEncrypt(tokenClaims).block();
|
||||
|
||||
// then
|
||||
ArgumentCaptor<byte[]> signedTokenCaptor = ArgumentCaptor.captor();
|
||||
verify(dataEncryptDecrypt).encrypt(signedTokenCaptor.capture(), anyMap());
|
||||
|
||||
byte[] signedTokenArgument = signedTokenCaptor.getValue();
|
||||
try {
|
||||
SignedJWSParts signedJWSParts = mapper.readValue(signedTokenArgument, SignedJWSParts.class);
|
||||
String signedJWS = String.join(".", signedJWSParts.s1(), signedJWSParts.s2(), signedJWSParts.s3());
|
||||
SignedJWT signedJWT = SignedJWT.parse(signedJWS);
|
||||
JWTClaimsSet jwtClaimsSet = signedJWT.getJWTClaimsSet();
|
||||
assertTrue(jwtClaimsSet.getExpirationTime().after(Date.from(Instant.now())));
|
||||
assertTrue(jwtClaimsSet
|
||||
.getExpirationTime()
|
||||
.before(Date.from(Instant.now().plusSeconds(60))));
|
||||
} catch (ParseException | IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package gov.irs.directfile.stateapi.configuration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
|
||||
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
|
||||
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class AwsCredentialsConfigurationTest {
|
||||
|
||||
@Test
|
||||
void staticCredentialProviderCreatedWhenApplicablePropertySetToFalse() {
|
||||
ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner()
|
||||
.withPropertyValues(
|
||||
"aws.s3.accessKey=test",
|
||||
"aws.s3.secretKey=test",
|
||||
"aws.s3.default-credentials-provider-chain-enabled=false")
|
||||
.withUserConfiguration(AwsCredentialsConfiguration.class);
|
||||
applicationContextRunner.run((context) -> {
|
||||
assertThat(context.getBean(AwsCredentialsProvider.class)).isNotNull();
|
||||
assertThat(context.getBean(AwsCredentialsProvider.class)).isInstanceOf(StaticCredentialsProvider.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void staticCredentialProviderCreatedWhenApplicablePropertyIsNotSet() {
|
||||
ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner()
|
||||
.withPropertyValues("aws.s3.accessKey=test", "aws.s3.secretKey=test")
|
||||
.withUserConfiguration(AwsCredentialsConfiguration.class);
|
||||
applicationContextRunner.run((context) -> {
|
||||
assertThat(context.getBean(AwsCredentialsProvider.class)).isNotNull();
|
||||
assertThat(context.getBean(AwsCredentialsProvider.class)).isInstanceOf(StaticCredentialsProvider.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultCredentialProviderCreatedWhenApplicablePropertySetToTrue() {
|
||||
ApplicationContextRunner applicationContextRunner = new ApplicationContextRunner()
|
||||
.withPropertyValues(
|
||||
"aws.s3.accessKey=test",
|
||||
"aws.s3.secretKey=test",
|
||||
"aws.s3.default-credentials-provider-chain-enabled=true")
|
||||
.withUserConfiguration(AwsCredentialsConfiguration.class);
|
||||
applicationContextRunner.run((context) -> {
|
||||
assertThat(context.getBean(AwsCredentialsProvider.class)).isNotNull();
|
||||
assertThat(context.getBean(AwsCredentialsProvider.class)).isInstanceOf(DefaultCredentialsProvider.class);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package gov.irs.directfile.stateapi.configuration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import software.amazon.awssdk.services.kms.KmsClient;
|
||||
import software.amazon.encryption.s3.materials.CryptographicMaterialsManager;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@SpringBootTest(classes = {EncryptionClientConfiguration.class, AwsCredentialsConfiguration.class})
|
||||
@ActiveProfiles("test")
|
||||
class EncryptionClientConfigurationTest {
|
||||
|
||||
@Autowired
|
||||
EncryptionClientConfiguration encryptionClientConfiguration;
|
||||
|
||||
@Test
|
||||
void regionalKmsClientIsNotNull() {
|
||||
KmsClient kmsClient = encryptionClientConfiguration.regionalKmsClient();
|
||||
assertNotNull(kmsClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void kmsCryptoIsNotNull() {
|
||||
String testKmsKeyArn = "test-kms-arn";
|
||||
CryptographicMaterialsManager cryptographicMaterialsManager =
|
||||
encryptionClientConfiguration.kmsCrypto(testKmsKeyArn);
|
||||
assertNotNull(cryptographicMaterialsManager);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue