Skip to content

Commit de89bed

Browse files
committed
Merge branch 'main' into frontend/cosmo-scaffold
2 parents c676973 + da7b5b7 commit de89bed

134 files changed

Lines changed: 6208 additions & 26989 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/common-cdk/common_constructs/stack.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ def __init__(self, *args, environment_context: dict, environment_name: str, **kw
9797

9898
self.environment_context = environment_context
9999
self.environment_name = environment_name
100+
101+
# Guard: all pipeline environments (test, beta, prod) MUST have a domain_name configured
102+
if environment_name in ('test', 'beta', 'prod') and not environment_context.get('domain_name'):
103+
raise ValueError(
104+
f"Pipeline environments (test, beta, prod) require 'domain_name' to be configured. "
105+
f"Environment '{environment_name}' is missing this required configuration."
106+
)
107+
100108
# We only set the API_BASE_URL common env var if the API_DOMAIN_NAME is set
101109
# The API_BASE_URL is used by the feature flag client to call the flag check endpoint
102110
if self.api_domain_name:
@@ -130,14 +138,20 @@ def search_api_domain_name(self) -> str | None:
130138

131139
@property
132140
def ui_domain_name(self) -> str | None:
141+
# Allow explicit override via environment context for cases where the UI is hosted
142+
# on a different domain than the backend's hosted zone (e.g. cosmetology backend uses
143+
# cosmetology.compactconnect.org but the UI is at app.compactconnect.org)
144+
override = self.environment_context.get('ui_domain_name_override')
145+
if override is not None:
146+
return override
133147
if self.hosted_zone is not None:
134148
return f'app.{self.hosted_zone.zone_name}'
135149
return None
136150

137151
@property
138152
def allowed_origins(self) -> list[str]:
139153
allowed_origins = []
140-
if self.hosted_zone is not None:
154+
if self.ui_domain_name is not None:
141155
allowed_origins.append(f'https://{self.ui_domain_name}')
142156

143157
if self.environment_context.get('allow_local_ui', False):

backend/compact-connect/common_constructs/cc_api.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
MethodLoggingLevel,
1818
ResponseType,
1919
RestApi,
20+
SecurityPolicy,
2021
StageOptions,
2122
)
2223
from aws_cdk.aws_certificatemanager import Certificate, CertificateValidation
@@ -89,7 +90,14 @@ def __init__(
8990
validation=CertificateValidation.from_dns(hosted_zone=stack.hosted_zone),
9091
subject_alternative_names=[stack.hosted_zone.zone_name],
9192
)
92-
domain_kwargs = {'domain_name': DomainNameOptions(certificate=certificate, domain_name=domain_name)}
93+
domain_kwargs = {
94+
'domain_name': DomainNameOptions(
95+
certificate=certificate,
96+
domain_name=domain_name,
97+
# this resource defaults to TLS_1_2, but we will explicitly set this anyway
98+
security_policy=SecurityPolicy.TLS_1_2,
99+
)
100+
}
93101

94102
access_log_group = LogGroup(scope, 'ApiAccessLogGroup', retention=RetentionDays.ONE_MONTH)
95103
NagSuppressions.add_resource_suppressions(
@@ -103,10 +111,14 @@ def __init__(
103111
],
104112
)
105113

114+
# Disable the default execute-api endpoint for all pipeline environments so traffic must use the custom domain.
115+
disable_execute_api_endpoint = environment_name in ('test', 'beta', 'prod')
116+
106117
super().__init__(
107118
scope,
108119
construct_id,
109120
cloud_watch_role=True,
121+
disable_execute_api_endpoint=disable_execute_api_endpoint,
110122
deploy_options=StageOptions(
111123
# NOTE: If we are ever updating our pipeline architecture which requires a change to the pipeline stack
112124
# name, the domain base path mapping for the API will fail to deploy unless we change the name of the

backend/compact-connect/lambdas/nodejs/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"version": "1.0.0",
44
"type": "commonjs",
55
"description": "NodeJS lambdas for Compact Connect",
6+
"resolutions": {
7+
"fast-xml-parser": "5.3.6"
8+
},
69
"scripts": {
710
"build": "tsc",
811
"watch": "tsc -w",

backend/compact-connect/lambdas/nodejs/yarn.lock

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3453,12 +3453,12 @@ fast-levenshtein@^2.0.6:
34533453
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
34543454
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
34553455

3456-
fast-xml-parser@5.3.4:
3457-
version "5.3.4"
3458-
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz#06f39aafffdbc97bef0321e626c7ddd06a043ecf"
3459-
integrity sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==
3456+
fast-xml-parser@5.3.4, fast-xml-parser@5.3.6:
3457+
version "5.3.6"
3458+
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.3.6.tgz#85a69117ca156b1b3c52e426495b6de266cb6a4b"
3459+
integrity sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==
34603460
dependencies:
3461-
strnum "^2.1.0"
3461+
strnum "^2.1.2"
34623462

34633463
fb-watchman@^2.0.0:
34643464
version "2.0.2"
@@ -5116,7 +5116,7 @@ strip-json-comments@^3.1.1:
51165116
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
51175117
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
51185118

5119-
strnum@^2.1.0:
5119+
strnum@^2.1.2:
51205120
version "2.1.2"
51215121
resolved "https://registry.yarnpkg.com/strnum/-/strnum-2.1.2.tgz#a5e00ba66ab25f9cafa3726b567ce7a49170937a"
51225122
integrity sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==

backend/compact-connect/tests/app/base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,15 @@ def _inspect_api_stack(self, api_stack: ApiStack):
552552
},
553553
)
554554

555+
# When a custom domain is configured, verify the API Gateway domain uses TLS 1.2
556+
if api_stack.hosted_zone is not None:
557+
api_template.has_resource_properties(
558+
'AWS::ApiGateway::DomainName',
559+
{
560+
'SecurityPolicy': 'TLS_1_2',
561+
},
562+
)
563+
555564
def _check_no_stack_annotations(self, stack: Stack):
556565
with self.subTest(f'Security Rules: {stack.stack_name}'):
557566
errors = Annotations.from_stack(stack).find_error('*', Match.string_like_regexp('.*'))

backend/compact-connect/tests/common_constructs/test_cognito_user_backup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,13 @@ class TestCognitoUserBackup(TestCase):
3131
def setUpClass(cls):
3232
"""Set up test infrastructure."""
3333
cls.app = App()
34-
# The persistent stack and layer are required for CognitoUserBackup, as an internal lambda depends on it
34+
# The persistent stack and layer are required for CognitoUserBackup, as an internal lambda depends on it.
35+
# Use a non-pipeline environment name so domain_name is not required (avoids HostedZone.from_lookup in tests).
3536
common_stack = AppStack(
3637
cls.app,
3738
'CommonStack',
3839
environment_context={},
39-
environment_name='test',
40+
environment_name='sandbox',
4041
standard_tags=StandardTags(project='compact-connect', service='compact-connect', environment='test'),
4142
)
4243
# Create common lambda layers

backend/compact-connect/tests/common_constructs/test_data_migration.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@ def test_data_migration_synthesizes(self):
1717
from common_constructs.python_common_layer_versions import PythonCommonLayerVersions
1818

1919
app = App()
20-
# The persistent stack and layer are required for DataMigration, as an internal lambda depends on it
20+
# The persistent stack and layer are required for DataMigration, as an internal lambda depends on it.
21+
# Use a non-pipeline environment name so domain_name is not required (avoids HostedZone.from_lookup in tests).
2122
common_stack = AppStack(
2223
app,
2324
'CommonStack',
2425
environment_context={},
25-
environment_name='test',
26+
environment_name='sandbox',
2627
standard_tags=StandardTags(project='compact-connect', service='compact-connect', environment='test'),
2728
)
2829
# Create common lambda layers

backend/cosmetology-app/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,23 @@ The `cdk.json` file tells the CDK Toolkit how to execute your app, including con
4444
deployment. You can add local configuration that will be merged into the `cdk.json['context']` values with a
4545
`cdk.context.json` file that you will not check in.
4646

47+
### `ui_domain_name_override`
48+
49+
**Important:** Because the cosmetology backend is hosted on a different domain than the shared frontend UI application (e.g. the
50+
backend hosted zone is `cosmetology.compactconnect.org` but the UI lives at `app.compactconnect.org`), each
51+
environment's context must include a `ui_domain_name_override` field that specifies the correct UI domain name. Without
52+
this override, the UI domain would be incorrectly derived from the backend's hosted zone (e.g.
53+
`app.cosmetology.compactconnect.org` instead of `app.compactconnect.org`). This value is used for CORS allowed origins,
54+
Cognito callback/logout URLs, and email template links.
55+
56+
Example:
57+
```json
58+
{
59+
"domain_name": "cosmetology.compactconnect.org",
60+
"ui_domain_name_override": "app.compactconnect.org"
61+
}
62+
```
63+
4764
This project is set up like a standard Python project. To use it, create and activate a python virtual environment
4865
using the tools of your choice (`pyenv` and `venv` are common).
4966

backend/cosmetology-app/app_clients/README.md

Lines changed: 1 addition & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,7 @@ The following scopes are available at the jurisdiction level:
5656
```
5757

5858
Currently, the most common scope needed by app clients is `{jurisdiction}/{compact}.write`, which allows uploading
59-
license data for a jurisdiction/compact combination. Scopes that expose PII (e.g., `.readSSN`, `.readPrivate`) should
60-
be granted sparingly and will require valid request signatures once a signing public key is configured for the
61-
jurisdiction.
59+
license data for a jurisdiction/compact combination.
6260

6361
### 3. Create App Client Using Interactive Python Script
6462

@@ -108,143 +106,6 @@ link that you'll generate separately.
108106
As part of the email message sent to the consuming team, be sure to include the onboarding instructions document from
109107
the `it_staff_onboarding_instructions/` directory.
110108

111-
## Managing API Signing Public Keys
112-
113-
### Overview
114-
115-
Signature-based authentication provides an additional layer of security for API access to sensitive licensure data. Each
116-
compact/state combination can have multiple SIGNATURE public keys configured to support key rotation and zero-downtime
117-
deployments.
118-
119-
### Authorization Requirements
120-
121-
**⚠️ CRITICAL SECURITY NOTICE:** Due to the sensitivity of the data protected by SIGNATURE authentication (including
122-
partial Social Security Numbers, personal addresses, and professional license details), configuration of new SIGNATURE
123-
public keys in production environments **MUST** include explicit authorization from the state board executive director.
124-
125-
126-
### Creating SIGNATURE Public Keys
127-
128-
Once a state configures a public key, they will be able to access the SIGNATURE-required API endpoints. API endpoints with
129-
_optional_ SIGNATURE support will also begin to enforce SIGNATURE signatures for that combination of compact and state. **This
130-
means that, once a compact/state has a public key configured, they will be denied access to SIGNATURE-Optional endpoints,
131-
such as the `POST license` endpoint, unless they have also implemented SIGNATURE signatures there as well.** Be sure that
132-
the representative is advised that they should begin signing those requests _before_ CompactConnect has a configured
133-
public key.
134-
135-
#### 1. Prerequisites
136-
137-
Before creating a new SIGNATURE public key, ensure you have:
138-
- **Production Authorization**: Explicit approval from the state board executive director for production environments
139-
- Validated the identity of the individual providing the public key to you
140-
- Jurisdiction and compact information confirmed
141-
- Contact information for the state IT representative
142-
- The public key file (`.pub` format) from the state IT representative (copy it to the same directory you are running the script from). The name of the file must match the key id.
143-
- AWS credentials configured with permissions to write to the compact configuration table
144-
- Python 3.10+ installed with boto3 dependency (`pip install boto3`)
145-
146-
#### 2. Key ID Naming Convention
147-
148-
The state IT department should provide an identifier; however, you can recommend a descriptive key ID that includes:
149-
- Environment indicator (if applicable)
150-
- Version or date suffix
151-
152-
Examples:
153-
- `prod-key-001`
154-
- `beta-key-2024-01`
155-
156-
#### 3. Create SIGNATURE Public Key Using Interactive Python Script
157-
158-
**Use the provided Python script in the bin directory for streamlined SIGNATURE key management:**
159-
160-
```bash
161-
python3 bin/manage_signature_keys.py create -t <compact_configuration_table_name>
162-
```
163-
164-
**Interactive Process:**
165-
The script will prompt you for:
166-
- Compact (cosm)
167-
- State postal abbreviation (e.g., "ky", "la")
168-
- Key ID (e.g., "client-org-prod-key-001")
169-
170-
**File Reading:**
171-
The script will:
172-
- Notify you that it will read the public key from `<key-id>.pub`
173-
- Validate the PEM format of the public key
174-
- Check for existing keys with the same ID
175-
- Write the key to the compact configuration database
176-
177-
**⚠️NOTICE:** Once the public key has been successfully stored, remove the `.pub` file from the directory to ensure it
178-
is never accidentally checked into the project.
179-
180-
#### 4. Database Schema
181-
182-
SIGNATURE keys are stored in the compact configuration table with the following schema:
183-
- **Primary Key (pk)**: `{compact}#SIGNATURE_KEYS#{state}`
184-
- **Sort Key (sk)**: `{compact}#JURISDICTION#{jurisdiction}#{key_id}`
185-
- **Additional Fields**:
186-
- `publicKey`: PEM-encoded public key content
187-
- `compact`: Compact abbreviation
188-
- `jurisdiction`: Jurisdiction abbreviation
189-
- `keyId`: Key identifier
190-
- `createdAt`: Creation timestamp
191-
192-
### Deleting SIGNATURE Public Keys
193-
194-
#### 1. Prerequisites
195-
196-
Before deleting a SIGNATURE public key, ensure you have:
197-
- Confirmation that the key is no longer in use by the state IT department
198-
- Confirmation of the key id to be deleted
199-
- Understanding of the impact on API access for the compact/state combination
200-
201-
#### 2. Delete SIGNATURE Public Key Using Interactive Python Script
202-
203-
```bash
204-
python3 bin/manage_signature_keys.py delete -t <table_name>
205-
```
206-
207-
**Interactive Process:**
208-
The script will:
209-
- Prompt for compact and state
210-
- List all existing keys for the compact/state combination
211-
- Allow you to select the specific key ID to delete
212-
- Require typing "DELETE" to confirm the deletion
213-
- Remove the key from the compact configuration database
214-
215-
### Key Rotation Best Practices
216-
217-
#### 1. Planning
218-
219-
- Coordinate with the State IT representative well in advance
220-
- Plan for zero-downtime deployment
221-
222-
#### 2. Implementation
223-
224-
- Create new keys before removing old ones
225-
- Allow both keys to be active during the transition period
226-
- Monitor API access and authentication success rates
227-
- Remove old keys only after confirming new keys are working correctly
228-
229-
#### 3. Documentation
230-
231-
- Document key rotation dates and reasons
232-
- Maintain audit trail of all key management activities
233-
234-
### Security Considerations
235-
236-
#### 1. Key Storage
237-
238-
- Public keys are stored in DynamoDB with appropriate access controls
239-
- Private keys should never be stored in CompactConnect systems
240-
- State IT departments are responsible for secure private key management
241-
242-
#### 2. Access Control
243-
244-
- Only authorized technical staff should have access to key management resources
245-
- All key management activities should be logged and audited
246-
- Production key creation requires executive director approval
247-
248109
## Rotating App Client Credentials
249110

250111
Unfortunately, AWS Cognito does not support rotating app client credentials for an existing app client. The only way

backend/cosmetology-app/bin/generate_mock_license_csv_upload_file.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444

4545
FIELDS = (
4646
'ssn',
47-
'npi',
4847
'licenseNumber',
4948
'licenseType',
5049
'licenseStatus',
@@ -146,10 +145,8 @@ def get_mock_license(
146145
license_data = {
147146
# |Zero padded 4 digit int|
148147
'ssn': f'{ssn_prefix}-{(i // 10_000) % 100:02}-{(i % 10_000):04}',
149-
# Some have NPI, some don't
150-
'npi': str(randint(1_000_000_000, 9_999_999_999)) if choice([True, False]) else None,
151-
# Some have License number, some don't
152-
'licenseNumber': generate_mock_license_number() if choice([True, False]) else None,
148+
# licenseNumber is required
149+
'licenseNumber': generate_mock_license_number(),
153150
'licenseType': choice(LICENSE_TYPES[compact]),
154151
'givenName': name_faker.first_name(),
155152
'middleName': name_faker.first_name(),

0 commit comments

Comments
 (0)