diff --git a/app/models/concerns/user_webauthn_methods.rb b/app/models/concerns/user_webauthn_methods.rb index 7dee0c4ba1e..448d71cf4a1 100644 --- a/app/models/concerns/user_webauthn_methods.rb +++ b/app/models/concerns/user_webauthn_methods.rb @@ -3,6 +3,7 @@ module UserWebauthnMethods included do has_many :webauthn_credentials, dependent: :destroy + has_one :webauthn_verification, dependent: :destroy after_initialize do self.webauthn_id ||= WebAuthn.generate_user_id diff --git a/app/models/webauthn_verification.rb b/app/models/webauthn_verification.rb new file mode 100644 index 00000000000..ecc7e4b3ebc --- /dev/null +++ b/app/models/webauthn_verification.rb @@ -0,0 +1,7 @@ +class WebauthnVerification < ApplicationRecord + belongs_to :user + + validates :user_id, uniqueness: true + validates :path_token, presence: true, uniqueness: true + validates :path_token_expires_at, presence: true +end diff --git a/db/migrate/20221214191823_create_webauthn_verifications.rb b/db/migrate/20221214191823_create_webauthn_verifications.rb new file mode 100644 index 00000000000..f06b22b2ee5 --- /dev/null +++ b/db/migrate/20221214191823_create_webauthn_verifications.rb @@ -0,0 +1,13 @@ +class CreateWebauthnVerifications < ActiveRecord::Migration[7.0] + def change + create_table :webauthn_verifications do |t| + t.string :path_token, limit: 128 + t.datetime :path_token_expires_at + t.string :otp + t.datetime :otp_expires_at + t.references :user, null: false, index: { unique: true }, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index fc2038cbcd9..8330b701254 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,8 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2022_03_29_203956) do - +ActiveRecord::Schema[7.0].define(version: 2022_12_14_191823) do # These are extensions that must be enabled in order to support this database enable_extension "hstore" enable_extension "plpgsql" @@ -294,6 +293,18 @@ t.index ["user_id"], name: "index_webauthn_credentials_on_user_id" end + create_table "webauthn_verifications", force: :cascade do |t| + t.string "path_token", limit: 128 + t.datetime "path_token_expires_at" + t.string "otp" + t.datetime "otp_expires_at" + t.bigint "user_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_webauthn_verifications_on_user_id", unique: true + end + add_foreign_key "api_keys", "users" add_foreign_key "webauthn_credentials", "users" + add_foreign_key "webauthn_verifications", "users" end diff --git a/doc/erd.dot b/doc/erd.dot index 0770c9a6f5d..eaab956dacd 100644 --- a/doc/erd.dot +++ b/doc/erd.dot @@ -279,6 +279,18 @@ m_WebauthnCredential [label = <sign_count integer (8) ∗
>]; +m_WebauthnVerification [label = < + +
WebauthnVerification
+| + + + + + +
otp string
otp_expires_at datetime (6,0)
path_token string (128) ∗ U
path_token_expires_at datetime (6,0) ∗
+>]; + m_User -> m_WebauthnVerification [arrowhead = "none", arrowtail = "none", weight = "3"]; m_User -> m_WebauthnCredential [arrowhead = "normal", arrowtail = "none", weight = "3"]; m_User -> m_WebHook [arrowhead = "normal", arrowtail = "none", weight = "3"]; m_Rubygem -> m_WebHook [arrowhead = "normal", arrowtail = "none", weight = "2"]; diff --git a/doc/erd.svg b/doc/erd.svg index 44e9a7e1f30..05f8ed55d92 100644 --- a/doc/erd.svg +++ b/doc/erd.svg @@ -1,551 +1,576 @@ - - + Gemcutter - -RubyGems.org domain model + +RubyGems.org domain model m_ApiKey - -ApiKey - -access_webhooks -boolean ∗ -add_owner -boolean ∗ -hashed_key -string ∗ -index_rubygems -boolean ∗ -last_accessed_at -datetime (6,0) -mfa -boolean ∗ -name -string ∗ -push_rubygem -boolean ∗ -remove_owner -boolean ∗ -show_dashboard -boolean ∗ -soft_deleted_at -datetime (6,0) -soft_deleted_rubygem_name -string -yank_rubygem -boolean ∗ + +ApiKey + +access_webhooks +boolean ∗ +add_owner +boolean ∗ +hashed_key +string ∗ +index_rubygems +boolean ∗ +last_accessed_at +datetime (6,0) +mfa +boolean ∗ +name +string ∗ +push_rubygem +boolean ∗ +remove_owner +boolean ∗ +show_dashboard +boolean ∗ +soft_deleted_at +datetime (6,0) +soft_deleted_rubygem_name +string +yank_rubygem +boolean ∗ m_ApiKeyRubygemScope - -ApiKeyRubygemScope + +ApiKeyRubygemScope - + m_ApiKey->m_ApiKeyRubygemScope - + m_Ownership - -Ownership - -confirmed_at -datetime (6,0) -owner_notifier -boolean ∗ -ownership_request_notifier -boolean ∗ -push_notifier -boolean ∗ -token -string -token_expires_at -datetime (6,0) + +Ownership + +confirmed_at +datetime (6,0) +owner_notifier +boolean ∗ +ownership_request_notifier +boolean ∗ +push_notifier +boolean ∗ +token +string +token_expires_at +datetime (6,0) + + + +m_ApiKey->m_Ownership + m_Delayed::Backend::ActiveRecord::Job - -Delayed::Backend::ActiveRecord::Job - -attempts -integer -failed_at -datetime (6,0) -handler -text -last_error -text -locked_at -datetime (6,0) -locked_by -string -priority -integer -queue -string -run_at -datetime (6,0) + +Delayed::Backend::ActiveRecord::Job + +attempts +integer +failed_at +datetime (6,0) +handler +text +last_error +text +locked_at +datetime (6,0) +locked_by +string +priority +integer +queue +string +run_at +datetime (6,0) m_Deletion - -Deletion - -number -string ∗ -platform -string -rubygem -string ∗ + +Deletion + +number +string ∗ +platform +string +rubygem +string ∗ m_Dependency - -Dependency - -requirements -string ∗ -scope -string -unresolved_name -string + +Dependency + +requirements +string ∗ +scope +string +unresolved_name +string m_GemDownload - -GemDownload - -count -integer (8) + +GemDownload + +count +integer (8) m_GemTypoException - -GemTypoException - -info -text -name -string ∗ U + +GemTypoException + +info +text +name +string ∗ U m_Linkset - -Linkset - -bugs -string -code -string -docs -string -home -string -mail -string -wiki -string + +Linkset + +bugs +string +code +string +docs +string +home +string +mail +string +wiki +string m_LogTicket - -LogTicket - -backend -integer -directory -string -key -string -processed_count -integer -status -string + +LogTicket + +backend +integer +directory +string +key +string +processed_count +integer +status +string - + m_Ownership->m_ApiKeyRubygemScope - - + + m_OwnershipCall - -OwnershipCall - -note -text ∗ -status -boolean ∗ + +OwnershipCall + +note +text ∗ +status +boolean ∗ m_OwnershipRequest - -OwnershipRequest - -note -text ∗ -status -integer (2) ∗ + +OwnershipRequest + +note +text ∗ +status +integer (2) ∗ - + m_OwnershipCall->m_OwnershipRequest - - + + m_Rubygem - -Rubygem - -indexed -boolean ∗ -name -string ∗ U -slug -string + +Rubygem + +indexed +boolean ∗ +name +string ∗ U +slug +string - + m_Rubygem->m_Dependency - - + + - + m_Rubygem->m_GemDownload - + + - + m_Rubygem->m_Linkset - + - + m_Rubygem->m_Ownership - - + + - + m_Rubygem->m_OwnershipCall - - + + - + m_Rubygem->m_OwnershipRequest - - - + + m_Subscription - -Subscription + +Subscription - + m_Rubygem->m_Subscription - - + + m_Version - -Version - -authors -text -built_at -datetime (6,0) -canonical_number -string -cert_chain -text -description -text -full_name -string ∗ U -indexed -boolean -info_checksum -string -latest -boolean -licenses -string -metadata -hstore ∗ -number -string -platform -string -position -integer -prerelease -boolean -required_ruby_version -string -required_rubygems_version -string (255) -requirements -text -sha256 -string -size -integer -summary -text -yanked_at -datetime (6,0) -yanked_info_checksum -string + +Version + +authors +text +built_at +datetime (6,0) +canonical_number +string +cert_chain +text +description +text +full_name +string ∗ U +indexed +boolean +info_checksum +string +latest +boolean +licenses +string +metadata +hstore ∗ +number +string +platform +string +position +integer +prerelease +boolean +required_ruby_version +string +required_rubygems_version +string (255) +requirements +text +sha256 +string +size +integer +summary +text +yanked_at +datetime (6,0) +yanked_info_checksum +string - + m_Rubygem->m_Version - - + + m_WebHook - -WebHook - -failure_count -integer -url -string ∗ + +WebHook + +failure_count +integer +url +string ∗ - + m_Rubygem->m_WebHook - - + + m_SendgridEvent - -SendgridEvent - -email -string -event_type -string -occurred_at -datetime (6,0) -payload -jsonb ∗ -sendgrid_id -string ∗ -status -string ∗ + +SendgridEvent + +email +string +event_type +string +occurred_at +datetime (6,0) +payload +jsonb ∗ +sendgrid_id +string ∗ +status +string ∗ m_User - -User - -api_key -string -blocked_email -string -confirmation_token -string (128) -email -string ∗ U -email_confirmed -boolean ∗ -email_reset -boolean -encrypted_password -string (128) -handle -string U -hide_email -boolean -mail_fails -integer -mfa_level -integer -mfa_recovery_codes -string -mfa_seed -string -remember_token -string (128) -remember_token_expires_at -datetime (6,0) -salt -string (128) -token -string (128) -token_expires_at -datetime (6,0) -twitter_username -string -unconfirmed_email -string -webauthn_id -string + +User + +api_key +string +blocked_email +string +confirmation_token +string (128) +email +string ∗ U +email_confirmed +boolean ∗ +email_reset +boolean +encrypted_password +string (128) +handle +string U +hide_email +boolean +mail_fails +integer +mfa_level +integer +mfa_recovery_codes +string +mfa_seed +string +remember_token +string (128) +remember_token_expires_at +datetime (6,0) +salt +string (128) +token +string (128) +token_expires_at +datetime (6,0) +twitter_username +string +unconfirmed_email +string +webauthn_id +string - + m_User->m_ApiKey - - + + - + m_User->m_Deletion - - + + - + m_User->m_Ownership - - + + - + m_User->m_OwnershipCall - - + + - + m_User->m_OwnershipRequest - - + + - + m_User->m_Subscription - - + + - + m_User->m_Version - - + + - + m_User->m_WebHook - - + + m_WebauthnCredential - -WebauthnCredential - -external_id -string ∗ U -nickname -string ∗ -public_key -string ∗ -sign_count -integer (8) ∗ + +WebauthnCredential + +external_id +string ∗ U +nickname +string ∗ U +public_key +string ∗ +sign_count +integer (8) ∗ - + m_User->m_WebauthnCredential - - + + + + + +m_WebauthnVerification + +WebauthnVerification + +otp +string +otp_expires_at +datetime (6,0) +path_token +string (128) ∗ U +path_token_expires_at +datetime (6,0) ∗ + + + +m_User->m_WebauthnVerification + m_User::WithPrivateFields - -User::WithPrivateFields - -api_key -string -blocked_email -string -confirmation_token -string (128) -email -string ∗ U -email_confirmed -boolean ∗ -email_reset -boolean -encrypted_password -string (128) -handle -string U -hide_email -boolean -mail_fails -integer -mfa_level -integer -mfa_recovery_codes -string -mfa_seed -string -remember_token -string (128) -remember_token_expires_at -datetime (6,0) -salt -string (128) -token -string (128) -token_expires_at -datetime (6,0) -twitter_username -string -unconfirmed_email -string -webauthn_id -string + +User::WithPrivateFields + +api_key +string +blocked_email +string +confirmation_token +string (128) +email +string ∗ U +email_confirmed +boolean ∗ +email_reset +boolean +encrypted_password +string (128) +handle +string U +hide_email +boolean +mail_fails +integer +mfa_level +integer +mfa_recovery_codes +string +mfa_seed +string +remember_token +string (128) +remember_token_expires_at +datetime (6,0) +salt +string (128) +token +string (128) +token_expires_at +datetime (6,0) +twitter_username +string +unconfirmed_email +string +webauthn_id +string - + m_Version->m_Dependency - - + + - + m_Version->m_GemDownload - + diff --git a/test/factories.rb b/test/factories.rb index d5b91432cc3..93d87d6020c 100644 --- a/test/factories.rb +++ b/test/factories.rb @@ -215,6 +215,14 @@ end end + factory :webauthn_verification do + user + path_token { SecureRandom.base58(20) } + path_token_expires_at { Time.now.utc + 1.minute } + otp { SecureRandom.base58(20) } + otp_expires_at { Time.now.utc + 1.minute } + end + factory :api_key_rubygem_scope do ownership api_key { create(:api_key, key: SecureRandom.hex(24)) } diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 9b3041e7fcf..cf68085762e 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -9,6 +9,7 @@ class UserTest < ActiveSupport::TestCase should have_many(:subscriptions).dependent(:destroy) should have_many(:web_hooks).dependent(:destroy) should have_many(:webauthn_credentials).dependent(:destroy) + should have_one(:webauthn_verification).dependent(:destroy) context "validations" do context "handle" do diff --git a/test/unit/webauthn_verification_test.rb b/test/unit/webauthn_verification_test.rb new file mode 100644 index 00000000000..45abbeb9f93 --- /dev/null +++ b/test/unit/webauthn_verification_test.rb @@ -0,0 +1,12 @@ +require "test_helper" + +class WebauthnVerificationTest < ActiveSupport::TestCase + subject { build(:webauthn_verification) } + + should belong_to :user + + should validate_uniqueness_of(:user_id) + should validate_presence_of(:path_token) + should validate_uniqueness_of(:path_token) + should validate_presence_of(:path_token_expires_at) +end