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
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@ jobs:

annotate_check:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: --health-cmd "pg_isready -U postgres" --health-interval 10s --health-timeout 5s --health-retries 5

env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/blueprint_development

steps:
- name: Checkout code
uses: actions/checkout@v7
Expand All @@ -68,6 +82,9 @@ jobs:
ruby-version: .ruby-version
bundler-cache: true

- name: Load database schema
run: bin/rails db:create db:schema:load

- name: Check if annotations are up to date
run: |
bundle exec annotaterb models --frozen --show-migration
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ GEM
safely_block (>= 1)
bootsnap (1.25.0)
msgpack (~> 1.5)
brakeman (8.0.6)
brakeman (8.1.0)
racc
builder (3.3.0)
bullet (8.2.0)
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/guild_signups_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,8 @@ def create
private

def signup_params
permitted = params.require(:guild_signup).permit(:role, :name, :email, :country, :ideas, :attendee_activities)
permitted[:role] = "attendee" unless %w[organizer attendee].include?(permitted[:role])
permitted = params.require(:guild_signup).permit(:name, :email, :country, :ideas, :attendee_activities)
permitted[:role] = params.dig(:guild_signup, :role) == "organizer" ? "organizer" : "attendee"
permitted
end

Expand Down
59 changes: 59 additions & 0 deletions app/controllers/privacy_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
class PrivacyController < ActionController::API
rescue_from(StandardError) { |error| render_error :internal_error, :internal_server_error, error_class: error.class.name }
rescue_from(PrivacyToken::Invalid) { render_error :invalid_token, :unauthorized }
rescue_from(PrivacyToken::ActionMismatch) { render_error :action_mismatch, :bad_request }
rescue_from(PrivacyToken::Replayed) { render_error :replayed, :conflict }
rescue_from(PrivacyToken::Unavailable) { render_error :jwks_unavailable, :service_unavailable }
rescue_from(PrivacyDeletion::ManualReviewRequired) { render_error :manual_review_required, :conflict }
rescue_from(PrivacyDeletion::ExternalFailure) { render_error :storage_unavailable, :service_unavailable }

around_action :without_sentry_events
before_action :verify_token!

def delete
raise PrivacyToken::Invalid unless [ true, false ].include?(@payload["dry_run"])

render_result PrivacyDeletion.new(@payload["subject"], dry_run: @payload["dry_run"]).call
end

def export
render_result PrivacyDeletion.new(@payload["subject"], dry_run: true).call
end

private

# Sentry sends request headers and SQL breadcrumbs, which would include the token and the subject's emails.
def without_sentry_events
Sentry.with_scope do |scope|
scope&.add_event_processor { nil }
yield
end
end

def verify_token!
token = request.authorization.to_s[/\ABearer (\S+)\z/, 1]
@payload = PrivacyToken.verify!(token, action: action_name)
end

def render_result(result)
log_outcome(result.found? ? "found" : "not_found")
render json: {
request_id: @payload["request_id"],
found: result.found?,
dry_run: @payload["dry_run"],
deleted: result.deleted,
retained: result.retained
}
end

def render_error(code, status, **details)
log_outcome(code, **details)
render json: { error: code }, status: status
end

def log_outcome(outcome, **details)
Rails.logger.tagged("PrivacyRequest") do
Rails.logger.info({ event: "privacy_request", request_id: @payload&.dig("request_id"), action: action_name, outcome: outcome, **details }.to_json)
end
end
end
2 changes: 1 addition & 1 deletion app/models/ai_review.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# retry_count :integer default(0), not null
# review_phase :string not null
# started_at :datetime
# status :string default(NULL), not null
# status :string default("pending"), not null
# steps :jsonb not null
# total_tokens :integer
# created_at :datetime not null
Expand Down
36 changes: 20 additions & 16 deletions app/models/marksmith/renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,25 @@ def initialize(options = {})
super(options)
end

def self.find_blob_from_url(src)
# Standard ActiveStorage URLs
if (match = src.match(%r{/rails/active_storage/blobs/(?:redirect/|proxy/)?([^/]+)/}))
return ActiveStorage::Blob.find_signed(match[1])
end

# Marksmith/user-attachments URLs (Base64-encoded JSON with blob_id)
if (match = src.match(%r{/user-attachments/blobs/(?:redirect/|proxy/)?([^/]+)/}))
token = match[1].split("--").first
decoded = JSON.parse(Base64.decode64(token))
blob_id = decoded.dig("_rails", "data") || decoded["data"]
return ActiveStorage::Blob.find_by(id: blob_id)
end

nil
rescue ActiveSupport::MessageVerifier::InvalidSignature, JSON::ParserError, ArgumentError
nil
end

def image(src, title, alt_text)
optimized_src = optimize_image_src(src)

Expand Down Expand Up @@ -147,22 +166,7 @@ def local_active_storage_blob?(src)
end

def find_blob_from_url(src)
# Standard ActiveStorage URLs
if (match = src.match(%r{/rails/active_storage/blobs/(?:redirect/|proxy/)?([^/]+)/}))
return ActiveStorage::Blob.find_signed(match[1])
end

# Marksmith/user-attachments URLs (Base64-encoded JSON with blob_id)
if (match = src.match(%r{/user-attachments/blobs/(?:redirect/|proxy/)?([^/]+)/}))
token = match[1].split("--").first
decoded = JSON.parse(Base64.decode64(token))
blob_id = decoded.dig("_rails", "data") || decoded["data"]
return ActiveStorage::Blob.find_by(id: blob_id)
end

nil
rescue ActiveSupport::MessageVerifier::InvalidSignature, JSON::ParserError, ArgumentError
nil
self.class.find_blob_from_url(src)
end

def host_for_urls
Expand Down
24 changes: 24 additions & 0 deletions app/models/privacy_token_use.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# == Schema Information
#
# Table name: privacy_token_uses
#
# id :bigint not null, primary key
# jti :string not null
# used_at :datetime not null
#
# Indexes
#
# index_privacy_token_uses_on_jti (jti) UNIQUE
# index_privacy_token_uses_on_used_at (used_at)
#
class PrivacyTokenUse < ApplicationRecord
RETENTION = 1.day

def self.first_use?(jti)
insert({ jti: jti }, unique_by: :jti, returning: :id).rows.any?
end

def self.prune!
where(used_at: ...RETENTION.ago).delete_all
end
end
2 changes: 1 addition & 1 deletion app/models/shop_order.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# quantity :integer
# rejected_at :datetime
# rejection_reason :string
# state :integer default("pending"), not null
# state :integer default(0), not null
# tracking_number :string
# created_at :datetime not null
# updated_at :datetime not null
Expand Down
Loading
Loading