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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tf-apply.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ inputs.aws_account_id }}:role/javabin-ci-deploy-${{ github.event.repository.name }}
role-to-assume: arn:aws:iam::${{ inputs.aws_account_id }}:role/javabin-ci-app-${{ github.event.repository.name }}
aws-region: ${{ inputs.aws_region }}

- name: Check risk level
Expand Down
39 changes: 28 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,15 +218,32 @@ Registry merge ──► team-provisioner (STUB — not yet implemented)

## SSM Parameters

| Path | Used By |
|------|---------|
| `/javabin/slack/platform-resource-alerts-webhook` | slack-alert (security events), compliance-reporter, platform-ci (HIGH risk, drift) |
| `/javabin/slack/platform-cost-alerts-webhook` | slack-alert (cost), cost-report, daily-cost-check |
| `/javabin/slack/platform-override-alerts-webhook` | tf-apply (block notification), approve-override (approval confirmation) |
| `/javabin/platform/google-admin-sa` | team-provisioner (Google Admin SDK) |
| `/javabin/platform/github-app-key` | team-provisioner (GitHub App private key) |
| `/javabin/platform-overrides/{repo}/{sha}` | Risk gate override tokens (single-use) |
| `/javabin/platform-apps/{name}/*` | Per-app secrets (Cognito clients, etc.) |
All parameters are in `eu-central-1`. Use `--profile javabin --region eu-central-1` via CLI.

| Path | Type | Used By |
|------|------|---------|
| `/javabin/slack/platform-resource-alerts-webhook` | SecureString | slack-alert, compliance-reporter, platform-ci |
| `/javabin/slack/platform-cost-alerts-webhook` | String | slack-alert (cost), cost-report, daily-cost-check |
| `/javabin/slack/platform-override-alerts-webhook` | SecureString | tf-apply (block notification), approve-override |
| `/javabin/platform/google-admin-sa` | SecureString | team-provisioner (GCP SA JSON key, domain-wide delegation) |
| `/javabin/platform/google-admin-email` | String | team-provisioner (admin email for Google Admin SDK impersonation) |
| `/javabin/platform/github-app-id` | SecureString | team-provisioner (GitHub App ID) |
| `/javabin/platform/github-app-key` | SecureString | team-provisioner (GitHub App private key) |
| `/javabin/platform/github-app-client-secret` | SecureString | team-provisioner (GitHub App client secret) |
| `/javabin/platform-overrides/{repo}/{sha}` | SecureString | Risk gate override tokens (single-use) |
| `/javabin/platform-apps/{name}/*` | varies | Per-app secrets (Cognito clients, etc. — future) |

## GCP Connection

**GCP Org:** java.no
**GCP Project:** `javabin-platform`
**Purpose:** Service account with domain-wide delegation for Google Admin SDK

A GCP service account in the `javabin-platform` project has domain-wide delegation configured
in Google Workspace Admin. The team-provisioner Lambda uses it to manage Google Groups
(create groups, sync membership) by impersonating the admin email stored in SSM.
The SA JSON key is at `/javabin/platform/google-admin-sa`, the impersonation target at
`/javabin/platform/google-admin-email`.

## Work Packages

Expand All @@ -237,7 +254,7 @@ Registry merge ──► team-provisioner (STUB — not yet implemented)
| 0a | AWS Discovery | **Done** |
| 0b | Bootstrap State Backend | **Done** — S3 backend live |
| 0c | Organizations + Permission Boundary | **Done** — org enabled, boundary deployed, SCP deferred |
| 1 | Identity (Google + Identity Center + Cognito) | **Not started** — Google Admin access confirmed, `identity/` dir is empty, no Cognito pools deployed |
| 1 | Identity (Google + Identity Center + Cognito) | **Partially done** — GCP SA with domain-wide delegation configured, GitHub App credentials in SSM. Identity Center and Cognito pools not deployed (`identity/` dir empty) |
| 2a | Networking | **Deployed** — VPC, subnets, NAT |
| 2b | Ingress | **Deployed** — ALB + ACM cert |
| 2c | IAM / OIDC | **Deployed** — 4 CI roles + per-app roles |
Expand All @@ -256,7 +273,7 @@ Registry merge ──► team-provisioner (STUB — not yet implemented)
### Known Issues
- **Registry → platform dispatch broken**: `provision-app.yml` fails because `GH_TOKEN` env var not set in workflow
- **platform-test-app CI failing**: ECR repo `platform-test-app` doesn't exist (never provisioned), tf-plan gets 404 on platform repo checkout
- **Team provisioner is a stub**: Lambda exists and is deployed but does nothing (logs and returns 200)
- **Team provisioner is a stub**: Lambda deployed but only logs. SSM credentials are ready (Google SA + GitHub App) — needs actual sync implementation
- **Identity module empty**: `terraform/platform/identity/` is an empty directory, not wired in `main.tf`
- **Cognito pools not deployed**: `cognito-app-client` module exists but has no pools to connect to

Expand Down
130 changes: 119 additions & 11 deletions docs/app-yaml-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,32 +203,138 @@ alarms:
memory_threshold: 80 # default: 80 (percent)
```

### environments (planned — not yet supported)
### environments

Multi-environment configuration. If omitted, a single production environment is created.

> **Note:** This field is planned for a future release. The `app-stack` module and `generate-terraform.sh` do not yet read the `environments` key. For now, all apps deploy as a single production environment.
Multi-environment configuration. If omitted, a single production environment is created (backwards compatible). When present, each key defines a separate environment with its own ECS service, ALB target group, DNS record, IAM role, and Terraform state.

```yaml
environments:
prod:
compute:
cpu: 512
memory: 1024
routing:
host: submit.javazone.no
priority: 100
dev:
routing:
host: dev.submit.javazone.no
priority: 101
auto_deploy: true
```

Each environment inherits all top-level defaults and can override: `compute`, `routing`, `domain`, `resources`, `environment`, `budget_alert_nok`, `alarms`.

#### Environment defaults

The `dev` environment gets smaller defaults automatically:

| Field | dev default | other envs default |
|-------|-------------|-------------------|
| `compute.cpu` | 256 | 512 |
| `compute.memory` | 512 | 1024 |

All other fields (port, desired_count, health_check, etc.) use the same defaults regardless of environment.

#### Full example

```yaml
name: submittheforce
team: pkom
compute:
cpu: 512
memory: 1024
port: 8080
resources:
databases:
- name: submissions
hash_key: id
env: SUBMISSIONS_TABLE
environment:
LOG_LEVEL: info
environments:
prod:
routing:
host: submit.javazone.no
priority: 100
compute:
cpu: 256
memory: 512
desired_count: 2
environment:
LOG_LEVEL: warn
dev:
routing:
host: dev.submit.javazone.no
priority: 101
budget_alert_nok: 200
auto_deploy: true
```

This creates two environments:

| | prod | dev |
|---|---|---|
| CPU / Memory | 512 / 1024 (from top-level) | 256 / 512 (dev defaults) |
| Desired count | 2 (override) | 1 (default) |
| Domain | submit.javazone.no | dev.submit.javazone.no |
| DynamoDB table | `javabin-submittheforce-prod-submissions` | `javabin-submittheforce-dev-submissions` |
| ECS service | `submittheforce-prod` | `submittheforce-dev` |
| LOG_LEVEL | warn (override) | info (from top-level) |
| State key | `apps/submittheforce-prod/terraform.tfstate` | `apps/submittheforce-dev/terraform.tfstate` |

#### Override behavior

- **compute, routing, alarms**: Per-field override. Unspecified fields fall back to top-level, then to defaults.
- **resources**: Per-type replacement. Each resource type (buckets, databases, secrets, queues) is independently replaced — if the environment defines `resources.databases`, it gets only those databases (not merged with top-level). Resource types not defined in the environment inherit from top-level.
- **environment** (env vars): Merged. Top-level env vars are applied first, then environment-specific ones override on conflict.

#### auto_deploy

```yaml
environments:
dev:
auto_deploy: true
```

Each environment inherits top-level defaults and can override: `compute`, `routing`, `resources`, `environment`, `budget_alert_nok`, `alarms`.
When `true`, merges to main automatically deploy to this environment without manual approval. Default: `false`. This is read by CI workflows, not by Terraform.

#### ECR repository

All environments share a single ECR repository named after the base app name. The same container image is deployed to all environments. The prod environment (or the first environment if no prod exists) creates the ECR repository; other environments reference it via data source.

**Important:** When first setting up multi-env, apply the environment that creates ECR (prod) before other environments.

#### Naming

Environment name must be lowercase alphanumeric, max 4 characters (e.g. `dev`, `prod`, `test`, `stag`). Multi-env apps should keep their name under ~19 characters to allow room for the environment suffix in ALB target group names (32-char AWS limit). Resources are named with an environment suffix:

| Resource | Single-env | Multi-env (dev) |
|----------|-----------|-----------------|
| ECS service | `moresleep` | `moresleep-dev` |
| IAM task role | `javabin-moresleep` | `javabin-moresleep-dev` |
| ALB target group | `javabin-moresleep` | `javabin-moresleep-dev` |
| DynamoDB table | `javabin-moresleep-sessions` | `javabin-moresleep-dev-sessions` |
| S3 bucket | `moresleep-data-{account}` | `moresleep-dev-data-{account}` |
| Log group | `/ecs/javabin/moresleep` | `/ecs/javabin/moresleep-dev` |
| ECR repo | `moresleep` | `moresleep` (shared) |

#### Generated directory structure

Single-env:
```
terraform/
backend.tf
providers.tf
main.tf
```

Multi-env:
```
terraform/
prod/
backend.tf # state key: apps/{name}-prod/terraform.tfstate
providers.tf
main.tf # environment_name = "prod", create_ecr = true
dev/
backend.tf # state key: apps/{name}-dev/terraform.tfstate
providers.tf
main.tf # environment_name = "dev", create_ecr = false
```

## How It Works

Expand All @@ -253,3 +359,5 @@ Generated files have a `# GENERATED FROM app.yaml` marker. The script only overw
| CloudWatch logs | `/ecs/javabin/{name}` |
| DNS record | `{routing.host}` |
| SSM (Cognito) | `/javabin/platform-apps/{name}/cognito-*` |

> In multi-env mode, `{name}` becomes `{name}-{env}` in all resource names except ECR. See [environments naming](#naming) for details.
152 changes: 152 additions & 0 deletions docs/cognito-google-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Cognito + Google IdP Setup

Manual steps required alongside Terraform-managed infrastructure for Cognito user pools with Google Sign-In.

## Two Separate Google Integrations

This platform uses Google in two distinct ways. They require separate credentials and serve different purposes.

| Integration | Purpose | Credential Type | Stored In |
|------------|---------|----------------|-----------|
| Domain-wide delegation SA | Team provisioner manages Google Groups via Admin SDK | GCP Service Account JSON key | SSM `/javabin/platform/google-admin-sa` |
| OAuth 2.0 Client | User-facing Google Sign-In in Cognito pools | OAuth Client ID + Secret | Cognito IdP config (Terraform) |

The domain-wide delegation SA already exists in the `javabin-platform` GCP project. The OAuth client below is a separate credential that must be created.

## 1. Google Cloud Console: OAuth Client

### Prerequisites
- Access to `javabin-platform` GCP project (under `java.no` org)
- Permissions: `roles/oauthconfig.editor` or project Owner

### 1.1 Configure OAuth Consent Screen

1. Go to **APIs & Services > OAuth consent screen** in the `javabin-platform` project
2. Select **External** user type (needed for the external Cognito pool)
3. Fill in:
- App name: `Javabin Platform`
- User support email: `platform@java.no` (or board contact)
- Developer contact: same
4. Scopes: add `openid`, `email`, `profile`
5. Test users: skip (not needed once published)
6. Publish the app (move from Testing to Production) — Google review may take a few days

### 1.2 Create OAuth 2.0 Client ID

1. Go to **APIs & Services > Credentials > Create Credentials > OAuth client ID**
2. Application type: **Web application**
3. Name: `javabin-cognito`
4. Authorized redirect URIs — add both Cognito pool callback URLs:
```
https://auth-internal.javazone.no/oauth2/idpresponse
https://auth-external.javazone.no/oauth2/idpresponse
```
These are the Cognito hosted UI domains. Adjust if using custom domains.
5. Click **Create** and note the **Client ID** and **Client Secret**

These values are passed to Terraform as variables when creating the Cognito identity provider resources. They are not stored in SSM — Cognito holds them internally.

## 2. Cognito User Pools (Terraform-managed)

The `terraform/platform/identity/` module (not yet implemented) will create two Cognito user pools. Below is what Terraform will manage and what to verify manually.

### 2.1 Internal Pool (`javabin-internal`)

For Javabin heroes and board members — Google Workspace accounts only.

**Terraform creates:**
- User pool with email as primary attribute
- Google IdP with `hd=java.no` restriction (hosted domain claim)
- Hosted UI domain: `auth-internal.javazone.no`
- Default app client for platform admin UI

**Google IdP config (in Terraform):**
```hcl
resource "aws_cognito_identity_provider" "google_internal" {
user_pool_id = aws_cognito_user_pool.internal.id
provider_name = "Google"
provider_type = "Google"

provider_details = {
client_id = var.google_oauth_client_id
client_secret = var.google_oauth_client_secret
authorize_scopes = "openid email profile"
attributes_url = "https://people.googleapis.com/v1/people/me?personFields="
attributes_url_add_attributes = true
authorize_url = "https://accounts.google.com/o/oauth2/v2/auth"
oidc_issuer = "https://accounts.google.com"
token_url = "https://oauth2.googleapis.com/token"
token_request_method = "POST"
}

attribute_mapping = {
email = "email"
name = "name"
username = "sub"
}
}
```

**Manual verification after `terraform apply`:**
1. Visit `https://auth-internal.javazone.no/login?client_id=<CLIENT_ID>&response_type=code&scope=openid+email+profile&redirect_uri=<CALLBACK>`
2. Confirm only `@java.no` accounts can sign in
3. Verify the `hd` claim filtering works (non-java.no accounts get rejected)

### 2.2 External Pool (`javabin-external`)

For public users — JavaZone attendees, community members.

**Terraform creates:**
- User pool with email as primary attribute
- Google IdP **without** `hd` restriction (any Google account)
- Self-registration enabled
- Hosted UI domain: `auth-external.javazone.no`

**Key differences from internal:**
- No hosted domain restriction on Google IdP
- Self-registration allowed
- Optional: GitHub social IdP (if configured)
- MFA optional (not enforced)

**Manual verification after `terraform apply`:**
1. Test sign-in with a non-java.no Google account
2. Test self-registration with email/password
3. Verify email verification flow works

## 3. App Client Registration

Apps declare their auth needs in `app.yaml`:

```yaml
auth: internal # or: external, both, none
```

The `cognito-app-client` module (`terraform/modules/cognito-app-client/`) creates:
- Cognito user pool client(s) in the specified pool(s)
- SSM parameters with client credentials:
- `/{project}/platform-apps/{name}/cognito-internal-client-id`
- `/{project}/platform-apps/{name}/cognito-internal-client-secret`
- `/{project}/platform-apps/{name}/cognito-external-client-id`
- `/{project}/platform-apps/{name}/cognito-external-client-secret`

Apps read these SSM parameters at runtime — no secrets in env vars or code.

## 4. Checklist

### Before Terraform deployment
- [ ] OAuth consent screen configured and published in GCP
- [ ] OAuth 2.0 Client ID created with correct redirect URIs
- [ ] Client ID and Secret available for Terraform variables
- [ ] Domain-wide delegation SA already in SSM (separate from OAuth client)

### After Terraform deployment
- [ ] Internal pool: `@java.no` Google Sign-In works
- [ ] Internal pool: non-`@java.no` accounts are rejected
- [ ] External pool: any Google account can sign in
- [ ] External pool: self-registration works
- [ ] Hosted UI domains resolve correctly (Route53 + ACM)
- [ ] Test app client credentials in SSM are readable

### DNS records needed (created by identity module)
- `auth-internal.javazone.no` — CNAME to Cognito internal pool domain
- `auth-external.javazone.no` — CNAME to Cognito external pool domain
Loading