From eced6e0f9f2f31548c16a61355cf7e30db7278a7 Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Thu, 12 Oct 2023 15:54:54 +0200 Subject: [PATCH] Add Vault Identity Tokens to authenticate to the monolith (#247) * Switched Token Generation to Vault's identity tokens * Run rubocop * remove test rote * remove typo * refactored to fit new standard * fix rubocop --- .rubocop.yml | 1 + Frontend/src/api/schema.d.ts | 68 ++++++++++++++++++++++++------------ app/helpers/payment_api.rb | 14 +++++--- app/helpers/user_api.rb | 9 +++-- app/helpers/wca_api.rb | 18 ++++++---- infra/handler/variables.tf | 4 +-- 6 files changed, 76 insertions(+), 38 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 5a6cc668..a6cd6ff8 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -7,6 +7,7 @@ AllCops: - 'Frontend/**/*' - 'bin/**/*' - 'vendor/**/*' + - 'infra/**/*' Bundler/OrderedGems: Enabled: false diff --git a/Frontend/src/api/schema.d.ts b/Frontend/src/api/schema.d.ts index c41aa531..c77d0aef 100644 --- a/Frontend/src/api/schema.d.ts +++ b/Frontend/src/api/schema.d.ts @@ -6,7 +6,7 @@ export interface paths { "/api/v1/registrations/{competition_id}": { - /** List registrations for a given competition_id */ + /** Public: list registrations for a given competition_id */ get: { parameters: { path: { @@ -14,25 +14,25 @@ export interface paths { }; }; responses: { - /** @description Valid competition_id but no registrations for it */ + /** @description PASSING comp service down but registrations exist */ 200: { content: { "application/json": components["schemas"]["registration"][]; }; }; - /** @description Competition ID doesnt exist */ + /** @description PASSING Competition ID doesnt exist */ 404: { content: { "application/json": components["schemas"]["error_response"]; }; }; - /** @description Competition service unavailable - 500 error */ + /** @description PASSING Competition service unavailable - 500 error */ 500: { content: { "application/json": components["schemas"]["error_response"]; }; }; - /** @description Competition service unavailable - 502 error */ + /** @description PASSING Competition service unavailable - 502 error */ 502: { content: { "application/json": components["schemas"]["error_response"]; @@ -41,31 +41,52 @@ export interface paths { }; }; }; - "/api/v1/register": { - /** Add an attendee registration */ - post: { + "/api/v1/registrations/{competition_id}/admin": { + /** Public: list registrations for a given competition_id */ + get: { parameters: { - header?: { - Authorization?: string; + path: { + competition_id: string; }; }; + responses: { + /** @description PASSING organizer has access to comp 2 */ + 200: { + content: { + "application/json": components["schemas"]["registrationAdmin"][]; + }; + }; + /** @description PASSING organizer cannot access registrations for comps they arent organizing - multi comp auth */ + 401: { + content: never; + }; + }; + }; + }; + "/api/v1/register": { + /** Add an attendee registration */ + post: { requestBody: { content: { - "application/json": components["schemas"]["submitRegistrationBody"]; + "application/json": components["schemas"]["registration"]; }; }; responses: { - /** @description only required fields included */ + /** @description PASSING only required fields included */ 202: { - content: { - "application/json": components["schemas"]["success_response"]; - }; + content: never; + }; + /** @description PASSING empty payload provided */ + 400: { + content: never; + }; + /** @description PASSING user impersonation (no admin permission, JWWT token user_id does not match registration user_id) */ + 401: { + content: never; }; - /** @description user impersonation attempt */ + /** @description PASSING comp not open */ 403: { - content: { - "application/json": components["schemas"]["error_response"]; - }; + content: never; }; }; }; @@ -90,9 +111,10 @@ export interface components { registrationAdmin: { user_id: string; event_ids: EventId[]; - comment?: string; - admin_comment?: string; - guests?: number; + comment?: string | null; + admin_comment?: string | null; + guests?: number | null; + email?: string; }; submitRegistrationBody: { user_id: string; @@ -118,6 +140,8 @@ export interface components { pathItems: never; } +export type $defs = Record; + export type external = Record; export type operations = Record; diff --git a/app/helpers/payment_api.rb b/app/helpers/payment_api.rb index 7febf0b5..7a0cc2be 100644 --- a/app/helpers/payment_api.rb +++ b/app/helpers/payment_api.rb @@ -2,17 +2,21 @@ require_relative 'error_codes' require_relative 'wca_api' -require_relative 'mocks' class PaymentApi < WcaApi def self.get_ticket(attendee_id, amount, currency_code) - token = self.get_wca_token("payments.worldcubeassociation.org") - response = HTTParty.post("https://test-registration.worldcubeassociation.org/api/v10/payment/init", + response = HTTParty.post(payment_init_path, body: { "attendee_id" => attendee_id, "amount" => amount, "currency_code" => currency_code }.to_json, - headers: { 'Authorization' => "Bearer: #{token}", + headers: { WCA_API_HEADER => self.get_wca_token, "Content-Type" => "application/json" }) unless response.ok? raise "Error from the payments service" end - [response["client_secret"], response["connected_account_id"]] + response["id"] end + + private + + def payment_init_path + "#{WCA_HOST}/api/internal/v1/payment/init" + end end diff --git a/app/helpers/user_api.rb b/app/helpers/user_api.rb index 50133da8..6ec21640 100644 --- a/app/helpers/user_api.rb +++ b/app/helpers/user_api.rb @@ -8,8 +8,7 @@ class UserApi < WcaApi def self.get_permissions(user_id) if Rails.env.production? - token = self.get_wca_token("users.worldcubeassociation.org") - HTTParty.get("https://test-registration.worldcubeassociation.org/api/v10/internal/users/#{user_id}/permissions", headers: { 'Authorization' => "Bearer: #{token}" }) + HTTParty.get(permissions_path(user_id), headers: { WCA_API_HEADER => self.get_wca_token }) else Mocks.permissions_mock(user_id) end @@ -30,4 +29,10 @@ def self.can_administer?(user_id, competition_id) end permissions["can_administer_competitions"]["scope"] == "*" || permissions["can_administer_competitions"]["scope"].include?(competition_id) end + + private + + def permissions_path(user_id) + "#{WCA_HOST}/api/internal/v1/users/#{user_id}/permissions" + end end diff --git a/app/helpers/wca_api.rb b/app/helpers/wca_api.rb index 73650e31..c34f7b83 100644 --- a/app/helpers/wca_api.rb +++ b/app/helpers/wca_api.rb @@ -1,12 +1,16 @@ # frozen_string_literal: true class WcaApi - # TODO: switch this to Vault identity tokens https://developer.hashicorp.com/vault/docs/secrets/identity/identity-token - def self.get_wca_token(audience) - iat = Time.now.to_i - jti_raw = [JwtOptions.secret, iat].join(':').to_s - jti = Digest::MD5.hexdigest(jti_raw) - payload = { data: { service_id: "registration.worldcubeassociation.org" }, aud: audience, exp: Time.now.to_i + JwtOptions.expiry, sub: "registration.worldcubeassociation.org", iat: iat, jti: jti } - JWT.encode payload, JwtOptions.secret, JwtOptions.algorithm + WCA_API_HEADER = 'X-WCA-Service-Token' + # Uses Vault ID Tokens: see https://developer.hashicorp.com/vault/docs/secrets/identity/identity-token + def self.get_wca_token + Vault.with_retries(Vault::HTTPConnectionError) do + data = Vault.logical.read("identity/oidc/token/#{@vault_application}") + if data.present? + data.data[:data][:token] + else # TODO: should we hard error out here? + puts "Tried to get identity token, but got error" + end + end end end diff --git a/infra/handler/variables.tf b/infra/handler/variables.tf index 1058feac..1a5ff28d 100644 --- a/infra/handler/variables.tf +++ b/infra/handler/variables.tf @@ -42,8 +42,8 @@ variable "host" { variable "wca_host" { type = string - description = "The host for generating absolute URLs in the application" - default = "worldcubeassociation.org" + description = "The host for generating URLs to the monolith" + default = "https://worldcubeassociation.org" } variable "shared_resources" {