diff --git a/README.md b/README.md index 309a16b..ce238f1 100644 --- a/README.md +++ b/README.md @@ -10,25 +10,163 @@ avoid questions that can be answered by reading the source code, but otherwise i kindly `bin/lint` your code before you submit it! -### areas of focus +## local dev setup -the ops view components (look in `app/components`) are a hot mess... +### prerequisites -so is the onboarding controller, she should really be ripped out and replaced. +you'll need: +- ruby 3.4.4+ (i use [mise](https://mise.jdx.dev) to manage this) +- node.js + yarn +- postgres (see below) +- imagemagick & libvips (image processing) +- libxmlsec1 (SAML signing) -## dev setup +on macOS: -- make sure you have working installations of ruby ≥ 3.4.4 & nodejs -- clone repo -- create .env.development, populate `DATABASE_URL` w/ a local postgres instance and `LOCKBOX_MASTER_KEY` with the value of `openssl rand -hex 32` -- if you want to use docker, you can run `docker compose -f docker-compose-dbonly.yml up` to spin up a database and plug `postgresql://postgres@localhost:5432/identity_vault_development` in as your `DATABASE_URL` - - if you don't have docker and are on macOS, [orbstack](https://orbstack.dev) may be helpful -- run `bundle install` -- run `bin/rails db:prepare` -- console in (`bin/rails console`) - - `Backend::User.create!(slack_id: "U", username: "", active: true, super_admin: true)` -- run `bin/dev` (and `bin/vite dev` if you want hot reload on css & js) -- visit `http://localhost:3000/backend/login`, paste that Slack ID in, and "fake it til' you make it" +```bash +brew install imagemagick libvips libxmlsec1 yarn +``` + +### database + +easiest way is docker. if you don't have it and you're on macOS, [orbstack](https://orbstack.dev) works well enough. + +```bash +docker compose -f docker-compose-dbonly.yml up -d +``` + +this gives you a postgres instance at `postgresql://postgres@localhost:5432/identity_vault_development`. + +if you've got your own postgres running somewhere, that works too – just point at it. + +### environment + +create a `.env.development` file: + +```bash +DATABASE_URL=postgresql://postgres@localhost:5432/identity_vault_development +``` + +that's it for local dev – lockbox will use a deterministic dev key automatically. see [environment variables](#environment-variables) below for the full list. + +### install & setup + +```bash +bundle install +yarn install +bin/rails db:prepare +bin/rails db:seed +``` + +the seeds create a dev account with 2FA already set up. it'll print out the TOTP secret – add that to your authenticator app. + +### running the thing + +```bash +bin/dev +``` + +if you want hot reload on css & js, also run `bin/vite dev` in another terminal. + +### logging in to the backend + +1. go to http://localhost:3000/login +2. enter `identity@hackclub.com` +3. grab the verification code from http://localhost:3000/letter_opener +4. enter your TOTP code (from the authenticator app you set up during seeding) +5. head to http://localhost:3000/backend + +the backend requires 2FA – that's why the seeds set up a TOTP for you. + +## environment variables + +### required + +| var | description | +|-----|-------------| +| `DATABASE_URL` | postgres connection string | + +### required in production + +| var | description | +|-----|-------------| +| `SECRET_KEY_BASE` | rails secret key – generate with `openssl rand -hex 64` | +| `LOCKBOX_MASTER_KEY` | encryption key for lockbox fields – generate with `openssl rand -hex 32` | + +### active record encryption + +used for `encrypts` fields (like aadhaar data). generate these with `bin/rails db:encryption:init` or use random strings. + +| var | description | +|-----|-------------| +| `ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY` | primary encryption key | +| `ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY` | deterministic encryption key | +| `ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT` | key derivation salt | + +### slack integration + +| var | description | +|-----|-------------| +| `SLACK_BOT_TOKEN` | bot token (xoxb-*) | +| `SLACK_TEAM_ID` | workspace ID (T*) | +| `SLACK_SCIM_TOKEN` | SCIM API token for user provisioning | +| `SLACK_CLIENT_ID` | OAuth client ID | +| `SLACK_CLIENT_SECRET` | OAuth client secret | +| `SLACK_SIGNING_SECRET` | webhook request verification | +| `SLACK_ADULT_WEBHOOK_URL` | webhook for guardian notifications | + +### SAML + +| var | description | +|-----|-------------| +| `SAML_IDP_CERT_PATH` | path to SAML IdP certificate | +| `SAML_IDP_KEY_PATH` | path to SAML IdP private key | + +generate a self-signed cert for local dev: + +```bash +openssl req -x509 -newkey rsa:2048 -keyout saml_key.pem -out saml_cert.pem -days 365 -nodes -subj "/CN=localhost" +``` + +### OIDC + +| var | description | +|-----|-------------| +| `OIDC_SIGNING_KEY` | RSA private key for JWT signing | + +generate an RSA key: + +```bash +openssl genrsa -out oidc_key.pem 2048 +``` + +then set `OIDC_SIGNING_KEY` to the contents of `oidc_key.pem` (the whole thing including the BEGIN/END lines). + +### email (production/staging/uat) + +| var | description | +|-----|-------------| +| `SES_SMTP_HOST` | SES SMTP endpoint | +| `SES_SMTP_USERNAME` | SES SMTP username | +| `SES_SMTP_PASSWORD` | SES SMTP password | + +### document storage (production) + +| var | description | +|-----|-------------| +| `CLOUDFLARE_R2_ENDPOINT` | R2 endpoint URL | +| `CLOUDFLARE_R2_ACCESS_KEY_ID` | R2 access key | +| `CLOUDFLARE_R2_SECRET_ACCESS_KEY` | R2 secret key | + +### other + +| var | description | +|-----|-------------| +| `SENTRY_DSN` | error tracking | +| `GOOGLE_PLACES_API_KEY` | address autocomplete | +| `ANALYTICS_DATABASE_URL` | separate analytics DB (optional) | +| `DISABLE_ANALYTICS` | set to "true" to disable Ahoy | +| `SOURCE_COMMIT` | git commit for version display | ## security diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f7e476b..c29bc60 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -64,7 +64,7 @@ module ApplicationHelper end def google_places_api_script_tag - api_key = Rails.application.credentials.dig(:google, :places_api_key) + api_key = ENV["GOOGLE_PLACES_API_KEY"] return unless api_key.present? tag.script(src: "https://maps.googleapis.com/maps/api/js?key=#{api_key}&loading=async&libraries=places&callback=onGoogleMapsLoaded", async: true, defer: true) diff --git a/app/jobs/slack/notify_guardians_job.rb b/app/jobs/slack/notify_guardians_job.rb index 7f6cdd5..c63b3c6 100644 --- a/app/jobs/slack/notify_guardians_job.rb +++ b/app/jobs/slack/notify_guardians_job.rb @@ -29,7 +29,7 @@ class Slack::NotifyGuardiansJob < ApplicationJob verf = identity.latest_verification context_line = "*ref:* <#{backend_identity_url(identity)}|#{identity.public_id}> / <#{backend_verification_url(verf)}|#{verf.public_id}>" - HTTP.post(Rails.application.credentials.slack.adult_webhook, body: { + HTTP.post(ENV["SLACK_ADULT_WEBHOOK_URL"], body: { "blocks": [ { "type": "section", diff --git a/config/environments/uat.rb b/config/environments/uat.rb index b56f9a7..3c72640 100644 --- a/config/environments/uat.rb +++ b/config/environments/uat.rb @@ -62,10 +62,10 @@ Rails.application.configure do # Amazon SES SMTP settings config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { - address: Rails.application.credentials.dig(:ses, :smtp_host) || "email-smtp.us-east-1.amazonaws.com", + address: ENV["SES_SMTP_HOST"], port: 587, - user_name: Rails.application.credentials.dig(:ses, :smtp_username), - password: Rails.application.credentials.dig(:ses, :smtp_password), + user_name: ENV["SES_SMTP_USERNAME"], + password: ENV["SES_SMTP_PASSWORD"], authentication: "plain", enable_starttls: true } diff --git a/config/initializers/active_record_encryption.rb b/config/initializers/active_record_encryption.rb new file mode 100644 index 0000000..f6b9c3c --- /dev/null +++ b/config/initializers/active_record_encryption.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Configure Active Record Encryption to use environment variables +# instead of Rails credentials + +if ENV["ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY"].present? + Rails.application.config.active_record.encryption.primary_key = ENV["ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY"] + Rails.application.config.active_record.encryption.deterministic_key = ENV["ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY"] + Rails.application.config.active_record.encryption.key_derivation_salt = ENV["ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT"] +end diff --git a/config/initializers/lockbox.rb b/config/initializers/lockbox.rb new file mode 100644 index 0000000..94f7d3a --- /dev/null +++ b/config/initializers/lockbox.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +if ENV["LOCKBOX_MASTER_KEY"].present? + Lockbox.master_key = ENV["LOCKBOX_MASTER_KEY"] +elsif Rails.env.development? || Rails.env.test? + # generate a deterministic key for dev/test so encrypted data persists across restarts + # this is NOT secure for production – always set LOCKBOX_MASTER_KEY in prod + Lockbox.master_key = Digest::SHA256.hexdigest("hca-dev-key") +else + raise "LOCKBOX_MASTER_KEY must be set in production" +end diff --git a/config/storage.yml b/config/storage.yml index cfa2236..e3c6333 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -22,9 +22,9 @@ encrypted_local_disk: prod_id_documents: service: EncryptedS3 - endpoint: <%= Rails.application.credentials.dig(:cloudflare, :endpoint) %> - access_key_id: <%= Rails.application.credentials.dig(:cloudflare, :access_key_id) %> - secret_access_key: <%= Rails.application.credentials.dig(:cloudflare, :secret_access_key) %> + endpoint: <%= ENV["CLOUDFLARE_R2_ENDPOINT"] %> + access_key_id: <%= ENV["CLOUDFLARE_R2_ACCESS_KEY_ID"] %> + secret_access_key: <%= ENV["CLOUDFLARE_R2_SECRET_ACCESS_KEY"] %> bucket: hackclub-identity-docs-prod region: auto private_url_policy: stream diff --git a/db/seeds.rb b/db/seeds.rb index 4fbd6ed..24344c8 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,9 +1,73 @@ -# This file should ensure the existence of records required to run the application in every environment (production, -# development, test). The code here should be idempotent so that it can be executed at any point in every environment. -# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). -# -# Example: -# -# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| -# MovieGenre.find_or_create_by!(name: genre_name) -# end +# frozen_string_literal: true + +# Seeds for local development +# Run with: bin/rails db:seed + +return unless Rails.env.development? + +puts "🌱 seeding development data..." + +# Create a test identity +identity = Identity.find_or_initialize_by(primary_email: "identity@hackclub.com") + +if identity.new_record? + identity.assign_attributes( + first_name: "Dev", + last_name: "User", + birthday: Date.new(2000, 1, 1), + country: :US + ) + identity.save! + puts " created identity: #{identity.primary_email}" +else + puts " identity already exists: #{identity.primary_email}" +end + +# Create or find a verified TOTP for backend access +totp = identity.totps.verified.first || identity.totps.find_by(aasm_state: "unverified") + +if totp.nil? + totp = identity.totps.create!(aasm_state: "verified") + puts " created TOTP for 2FA" +elsif totp.aasm_state != "verified" + totp.update!(aasm_state: "verified") + puts " verified existing TOTP" +else + puts " TOTP already exists and verified" +end + +# Create backend user with super admin +backend_user = identity.backend_user || identity.build_backend_user + +if backend_user.new_record? + backend_user.assign_attributes( + username: "dev", + active: true, + super_admin: true + ) + backend_user.save! + puts " created backend user with super_admin" +else + puts " backend user already exists" +end + +puts "" +puts "=" * 60 +puts "dev account ready!" +puts "=" * 60 +puts "" +puts " email: identity@hackclub.com" +puts " totp secret: #{totp.secret}" +puts "" +puts "add this secret to your authenticator app, or use this URI:" +puts "" +puts " #{totp.provisioning_uri}" +puts "" +puts "login flow:" +puts " 1. go to http://localhost:3000/login" +puts " 2. enter: identity@hackclub.com" +puts " 3. grab the code from http://localhost:3000/letter_opener" +puts " 4. enter the TOTP code from your authenticator" +puts " 5. go to http://localhost:3000/backend" +puts "" +puts "=" * 60 \ No newline at end of file