From 4fff8d88f27405d3e6f04bacaa9b76474df8c659 Mon Sep 17 00:00:00 2001 From: Emi Matchu Date: Mon, 29 Jan 2024 04:21:19 -0800 Subject: [PATCH] Add support_staff flag to user record; they can use Support tools A little architecture trick here! DTI 2020 authorizes support staff requests by means of a secret token, instead of user account stuff. And our support tools still all call DTI 2020 APIs. So here, we bridge the gap: we copy DTI 2020's support secret to this app's environment variables (I needed to update `deploy/files/production.env` and run `bin/deploy:setup` for this!), then users with the new `support_secret` flag have it added to their HTML documents in the meta tags. Then, the JS reads the meta tag. I also fixed an issue in the `deploy/setup.yml` playbook, where I had temporarily commented some stuff out to skip steps one time, and forgot to uncomment them after oops lol! --- app/helpers/application_helper.rb | 17 +- .../WardrobePage/support/useSupport.js | 21 +- .../wardrobe-2020/impress-2020-config.js | 16 +- app/views/layouts/application.html.haml | 2 +- app/views/outfits/edit.html.haml | 2 +- config/initializers/impress_2020.rb | 7 +- config/routes.rb | 2 +- ...240129114639_add_support_staff_to_users.rb | 5 + db/schema.rb | 3 +- deploy/setup.yml | 420 +++++++++--------- 10 files changed, 264 insertions(+), 231 deletions(-) create mode 100644 db/migrate/20240129114639_add_support_staff_to_users.rb diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 8497fa28..e8aaa775 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -87,9 +87,20 @@ module ApplicationHelper !@hide_home_link end - def impress_2020_meta_tag - tag 'meta', name: "impress-2020-origin", - content: Rails.configuration.impress_2020_origin + def support_staff? + user_signed_in? && current_user.support_staff? + end + + def impress_2020_meta_tags + impress_2020 = Rails.configuration.x.impress_2020 + capture do + concat tag("meta", name: "impress-2020-origin", + content: impress_2020.origin) + if support_staff? && impress_2020.support_secret.present? + concat tag("meta", name: "impress-2020-support-secret", + content: impress_2020.support_secret) + end + end end JAVASCRIPT_LIBRARIES = { diff --git a/app/javascript/wardrobe-2020/WardrobePage/support/useSupport.js b/app/javascript/wardrobe-2020/WardrobePage/support/useSupport.js index 078bb39d..6e3bce11 100644 --- a/app/javascript/wardrobe-2020/WardrobePage/support/useSupport.js +++ b/app/javascript/wardrobe-2020/WardrobePage/support/useSupport.js @@ -1,28 +1,31 @@ import * as React from "react"; +import { getSupportSecret } from "../../impress-2020-config"; + /** * useSupport returns the Support secret that the server requires for Support * actions... if the user has it set. For most users, this returns nothing! * + * This is specifically for communications for Impress 2020, which authorizes + * support requests using a shared support secret instead of user accounts. + * (This isn't a great model, we should abandon it in favor of true authorized + * requests as we deprecate Impress 2020!) + * * Specifically, we return an object of: * - isSupportUser: true iff the `supportSecret` is set * - supportSecret: the secret saved to this device, or null if not set * - * To become a Support user, you visit /?supportSecret=..., which saves the - * secret to your device. + * To become a Support user, get the `support_staff` flag set on your user + * account. Then, `getSupportSecret` will read the support secret from the HTML + * document. (If the flag is off, the HTML document does not contain the + * secret.) * * Note that this hook doesn't check that the secret is *correct*, so it's * possible that it will return an invalid secret. That's okay, because * the server checks the provided secret for each Support request. */ function useSupport() { - const supportSecret = React.useMemo( - () => - typeof localStorage !== "undefined" - ? localStorage.getItem("supportSecret") - : null, - [], - ); + const supportSecret = getSupportSecret(); const isSupportUser = supportSecret != null; diff --git a/app/javascript/wardrobe-2020/impress-2020-config.js b/app/javascript/wardrobe-2020/impress-2020-config.js index 1cae15f9..6ee75d98 100644 --- a/app/javascript/wardrobe-2020/impress-2020-config.js +++ b/app/javascript/wardrobe-2020/impress-2020-config.js @@ -1,10 +1,20 @@ -const IMPRESS_2020_ORIGIN = readImpress2020Origin(); +const ORIGIN = readOrigin(); +const SUPPORT_SECRET = readSupportSecret(); export function buildImpress2020Url(path) { - return new URL(path, IMPRESS_2020_ORIGIN).toString(); + return new URL(path, ORIGIN).toString(); } -function readImpress2020Origin() { +export function getSupportSecret() { + return SUPPORT_SECRET; +} + +function readOrigin() { const node = document.querySelector("meta[name=impress-2020-origin]"); return node?.content || "https://impress-2020.openneo.net" } + +function readSupportSecret() { + const node = document.querySelector("meta[name=impress-2020-support-secret]"); + return node?.content || null; +} diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index b461b4cb..4cb6ad65 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -17,7 +17,7 @@ = yield :meta = open_graph_tags = csrf_meta_tag - = impress_2020_meta_tag + = impress_2020_meta_tags = signed_in_meta_tag - if user_signed_in? = current_user_id_meta_tag diff --git a/app/views/outfits/edit.html.haml b/app/views/outfits/edit.html.haml index f7b8608c..06f1b85d 100644 --- a/app/views/outfits/edit.html.haml +++ b/app/views/outfits/edit.html.haml @@ -20,7 +20,7 @@ = render 'static/analytics' = open_graph_tags = csrf_meta_tags - = impress_2020_meta_tag + = impress_2020_meta_tags %meta{name: 'dti-current-user-id', content: user_signed_in? ? current_user.id : "null"} %body #wardrobe-2020-root diff --git a/config/initializers/impress_2020.rb b/config/initializers/impress_2020.rb index 0f93a47a..cbe2b352 100644 --- a/config/initializers/impress_2020.rb +++ b/config/initializers/impress_2020.rb @@ -1,2 +1,5 @@ -Rails.configuration.impress_2020_origin = - ENV["IMPRESS_2020_ORIGIN"] || "https://impress-2020.openneo.net" +Rails.configuration.x.impress_2020.origin = + ENV.fetch("IMPRESS_2020_ORIGIN", "https://impress-2020.openneo.net") + +Rails.configuration.x.impress_2020.support_secret = + ENV.fetch("IMPRESS_2020_SUPPORT_SECRET", nil) diff --git a/config/routes.rb b/config/routes.rb index d7bf4e29..095fdaa4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -89,7 +89,7 @@ OpenneoImpressItems::Application.routes.draw do # Static pages! get '/terms', as: :terms, - to: redirect(Rails.configuration.impress_2020_origin + "/terms") + to: redirect(Rails.configuration.x.impress_2020.origin + "/terms") # Other useful lil things! get '/sitemap.xml' => 'sitemap#index', :as => :sitemap, :format => :xml diff --git a/db/migrate/20240129114639_add_support_staff_to_users.rb b/db/migrate/20240129114639_add_support_staff_to_users.rb new file mode 100644 index 00000000..f5531dd1 --- /dev/null +++ b/db/migrate/20240129114639_add_support_staff_to_users.rb @@ -0,0 +1,5 @@ +class AddSupportStaffToUsers < ActiveRecord::Migration[7.1] + def change + add_column :users, :support_staff, :boolean, null: false, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 6ffe0d55..aa8374b8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_01_24_102340) do +ActiveRecord::Schema[7.1].define(version: 2024_01_29_114639) do create_table "alt_styles", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t| t.integer "species_id", null: false t.integer "color_id", null: false @@ -291,6 +291,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_24_102340) do t.integer "wanted_closet_hangers_visibility", default: 1, null: false t.integer "contact_neopets_connection_id" t.timestamp "last_trade_activity_at" + t.boolean "support_staff", default: false, null: false end create_table "zone_translations", id: :integer, charset: "latin1", collation: "latin1_swedish_ci", force: :cascade do |t| diff --git a/deploy/setup.yml b/deploy/setup.yml index f99188b8..31bbe18a 100644 --- a/deploy/setup.yml +++ b/deploy/setup.yml @@ -7,253 +7,253 @@ email_address: "emi@matchu.dev" # TODO: Extract this to personal config? impress_hostname: impress.openneo.net tasks: - # - name: Create SSH folder for logged-in user - # become: no - # file: - # name: .ssh - # mode: "700" - # state: directory + - name: Create SSH folder for logged-in user + become: no + file: + name: .ssh + mode: "700" + state: directory - # - name: Copy authorized SSH keys to logged-in user - # become: no - # copy: - # dest: ~/.ssh/authorized_keys - # src: files/authorized-ssh-keys.txt - # mode: "600" + - name: Copy authorized SSH keys to logged-in user + become: no + copy: + dest: ~/.ssh/authorized_keys + src: files/authorized-ssh-keys.txt + mode: "600" - # - name: Disable root SSH login - # lineinfile: - # dest: /etc/ssh/sshd_config - # regexp: ^#?PermitRootLogin - # line: PermitRootLogin no + - name: Disable root SSH login + lineinfile: + dest: /etc/ssh/sshd_config + regexp: ^#?PermitRootLogin + line: PermitRootLogin no - # - name: Disable password-based SSH authentication - # lineinfile: - # dest: /etc/ssh/sshd_config - # regexp: ^#?PasswordAuthentication - # line: PasswordAuthentication no + - name: Disable password-based SSH authentication + lineinfile: + dest: /etc/ssh/sshd_config + regexp: ^#?PasswordAuthentication + line: PasswordAuthentication no - # - name: Enable public-key SSH authentication - # lineinfile: - # dest: /etc/ssh/sshd_config - # regexp: ^#?PubkeyAuthentication - # line: PubkeyAuthentication yes + - name: Enable public-key SSH authentication + lineinfile: + dest: /etc/ssh/sshd_config + regexp: ^#?PubkeyAuthentication + line: PubkeyAuthentication yes - # - name: Update the apt cache - # apt: - # update_cache: yes + - name: Update the apt cache + apt: + update_cache: yes - # - name: Install fail2ban firewall with default settings - # apt: - # name: fail2ban + - name: Install fail2ban firewall with default settings + apt: + name: fail2ban - # - name: Configure ufw firewall to allow SSH connections on port 22 - # community.general.ufw: - # rule: allow - # port: "22" + - name: Configure ufw firewall to allow SSH connections on port 22 + community.general.ufw: + rule: allow + port: "22" - # - name: Configure ufw firewall to allow HTTP connections on port 80 - # community.general.ufw: - # rule: allow - # port: "80" + - name: Configure ufw firewall to allow HTTP connections on port 80 + community.general.ufw: + rule: allow + port: "80" - # - name: Configure ufw firewall to allow HTTPS connections on port 443 - # community.general.ufw: - # rule: allow - # port: "443" + - name: Configure ufw firewall to allow HTTPS connections on port 443 + community.general.ufw: + rule: allow + port: "443" - # - name: Enable ufw firewall with all other ports closed by default - # community.general.ufw: - # state: enabled - # policy: deny + - name: Enable ufw firewall with all other ports closed by default + community.general.ufw: + state: enabled + policy: deny - # - name: Install unattended-upgrades - # apt: - # name: unattended-upgrades + - name: Install unattended-upgrades + apt: + name: unattended-upgrades - # - name: Enable unattended-upgrades to auto-upgrade our system - # copy: - # content: | - # APT::Periodic::Update-Package-Lists "1"; - # APT::Periodic::Unattended-Upgrade "1"; - # dest: /etc/apt/apt.conf.d/20auto-upgrades + - name: Enable unattended-upgrades to auto-upgrade our system + copy: + content: | + APT::Periodic::Update-Package-Lists "1"; + APT::Periodic::Unattended-Upgrade "1"; + dest: /etc/apt/apt.conf.d/20auto-upgrades - # - name: Configure unattended-upgrades to auto-reboot our server when necessary - # lineinfile: - # regex: ^(//\s*)?Unattended-Upgrade::Automatic-Reboot ".*";$ - # line: Unattended-Upgrade::Automatic-Reboot "true"; - # dest: /etc/apt/apt.conf.d/50unattended-upgrades + - name: Configure unattended-upgrades to auto-reboot our server when necessary + lineinfile: + regex: ^(//\s*)?Unattended-Upgrade::Automatic-Reboot ".*";$ + line: Unattended-Upgrade::Automatic-Reboot "true"; + dest: /etc/apt/apt.conf.d/50unattended-upgrades - # - name: Configure unattended-upgrades to delay necessary reboots to 3am - # lineinfile: - # regex: ^(//\s*)?Unattended-Upgrade::Automatic-Reboot-Time ".*";$ - # line: Unattended-Upgrade::Automatic-Reboot-Time "03:00"; - # dest: /etc/apt/apt.conf.d/50unattended-upgrades + - name: Configure unattended-upgrades to delay necessary reboots to 3am + lineinfile: + regex: ^(//\s*)?Unattended-Upgrade::Automatic-Reboot-Time ".*";$ + line: Unattended-Upgrade::Automatic-Reboot-Time "03:00"; + dest: /etc/apt/apt.conf.d/50unattended-upgrades - # - name: Configure the system timezone to be US Pacific time - # community.general.timezone: - # name: America/Los_Angeles + - name: Configure the system timezone to be US Pacific time + community.general.timezone: + name: America/Los_Angeles - # - name: Create "impress" user - # user: - # name: impress - # comment: Impress App - # home: /srv/impress - # create_home: false - # shell: /bin/bash + - name: Create "impress" user + user: + name: impress + comment: Impress App + home: /srv/impress + create_home: false + shell: /bin/bash - # - name: Create "impress-deployers" group - # group: - # name: impress-deployers + - name: Create "impress-deployers" group + group: + name: impress-deployers - # - name: Add the current user to the "impress-deployers" group - # user: - # name: "{{ lookup('env', 'USER') }}" - # groups: - # - impress-deployers - # append: yes + - name: Add the current user to the "impress-deployers" group + user: + name: "{{ lookup('env', 'USER') }}" + groups: + - impress-deployers + append: yes - # # We use this so the deploy playbook doesn't have to prompt for a root - # # password: this user just is trusted to act as "impress" in the future. - # - name: Enable the "impress-deployers" group to freely act as the "impress" user - # community.general.sudoers: - # name: impress-deployers-as-impress - # group: impress-deployers - # runas: impress - # commands: ALL - # nopassword: yes + # We use this so the deploy playbook doesn't have to prompt for a root + # password: this user just is trusted to act as "impress" in the future. + - name: Enable the "impress-deployers" group to freely act as the "impress" user + community.general.sudoers: + name: impress-deployers-as-impress + group: impress-deployers + runas: impress + commands: ALL + nopassword: yes - # # Similarly, this enables us to manage the impress service in the deploy playbook - # # and in live debugging without a password. - # # NOTE: In the sudoers file, you need to specify the full path to the - # # command, to avoid tricks where you use PATH to get around the intent! - # - name: Enable the "impress-deployers" group to freely start and stop the impress service - # community.general.sudoers: - # name: impress-deployers-systemctl - # group: impress-deployers - # commands: - # - /bin/systemctl status impress - # - /bin/systemctl start impress - # - /bin/systemctl stop impress - # - /bin/systemctl restart impress - # nopassword: yes + # Similarly, this enables us to manage the impress service in the deploy playbook + # and in live debugging without a password. + # NOTE: In the sudoers file, you need to specify the full path to the + # command, to avoid tricks where you use PATH to get around the intent! + - name: Enable the "impress-deployers" group to freely start and stop the impress service + community.general.sudoers: + name: impress-deployers-systemctl + group: impress-deployers + commands: + - /bin/systemctl status impress + - /bin/systemctl start impress + - /bin/systemctl stop impress + - /bin/systemctl restart impress + nopassword: yes - # - name: Install ACL, to enable us to run commands as the "impress" user - # apt: - # name: acl + - name: Install ACL, to enable us to run commands as the "impress" user + apt: + name: acl - # - name: Install ruby-build - # git: - # repo: https://github.com/rbenv/ruby-build.git - # dest: /opt/ruby-build - # version: 4d4678bc1ed89aa6900c0ea0da23495445dbcf50 + - name: Install ruby-build + git: + repo: https://github.com/rbenv/ruby-build.git + dest: /opt/ruby-build + version: 4d4678bc1ed89aa6900c0ea0da23495445dbcf50 - # - name: Check if Ruby 3.1.4 is already installed - # stat: - # path: /opt/ruby-3.1.4 - # register: ruby_dir + - name: Check if Ruby 3.1.4 is already installed + stat: + path: /opt/ruby-3.1.4 + register: ruby_dir - # - name: Install Ruby 3.1.4 - # command: "/opt/ruby-build/bin/ruby-build 3.1.4 /opt/ruby-3.1.4" - # when: not ruby_dir.stat.exists + - name: Install Ruby 3.1.4 + command: "/opt/ruby-build/bin/ruby-build 3.1.4 /opt/ruby-3.1.4" + when: not ruby_dir.stat.exists - # - name: Add Ruby 3.1.4 to the global PATH, for developer convenience - # lineinfile: - # dest: /etc/profile - # line: 'PATH="/opt/ruby-3.1.4/bin:$PATH" # Added by impress deploy setup script' + - name: Add Ruby 3.1.4 to the global PATH, for developer convenience + lineinfile: + dest: /etc/profile + line: 'PATH="/opt/ruby-3.1.4/bin:$PATH" # Added by impress deploy setup script' - # - name: Install system dependencies for impress's Ruby gems - # apt: - # name: - # - libmysqlclient-dev - # - libyaml-dev + - name: Install system dependencies for impress's Ruby gems + apt: + name: + - libmysqlclient-dev + - libyaml-dev - # - name: Create the app folder - # file: - # path: /srv/impress - # owner: impress - # group: impress - # mode: "755" - # state: directory + - name: Create the app folder + file: + path: /srv/impress + owner: impress + group: impress + mode: "755" + state: directory - # - name: Add a convenient .bash_profile for when we log in as "impress" - # copy: - # owner: impress - # group: impress - # dest: /srv/impress/.bash_profile - # content: | - # set -a # Export all of the below - # RAILS_ENV=production - # EXECJS_RUNTIME=Disabled - # source /srv/impress/shared/production.env - # set +a + - name: Add a convenient .bash_profile for when we log in as "impress" + copy: + owner: impress + group: impress + dest: /srv/impress/.bash_profile + content: | + set -a # Export all of the below + RAILS_ENV=production + EXECJS_RUNTIME=Disabled + source /srv/impress/shared/production.env + set +a - # - name: Create the app's "versions" folder - # become_user: impress - # file: - # path: /srv/impress/versions - # state: directory + - name: Create the app's "versions" folder + become_user: impress + file: + path: /srv/impress/versions + state: directory - # - name: Create the app's "shared" folder - # become_user: impress - # file: - # path: /srv/impress/shared - # state: directory + - name: Create the app's "shared" folder + become_user: impress + file: + path: /srv/impress/shared + state: directory - # - name: Check for a current app version - # stat: - # path: /srv/impress/current - # register: current_app_version + - name: Check for a current app version + stat: + path: /srv/impress/current + register: current_app_version - # - name: Check whether we already have a placeholder app - # stat: - # path: /srv/impress/versions/initial-placeholder - # register: existing_placeholder_app - # when: not current_app_version.stat.exists + - name: Check whether we already have a placeholder app + stat: + path: /srv/impress/versions/initial-placeholder + register: existing_placeholder_app + when: not current_app_version.stat.exists - # - name: Create a placeholder app, to run until we deploy a real version - # become_user: impress - # copy: - # src: files/initial-placeholder - # dest: /srv/impress/versions - # when: | - # not current_app_version.stat.exists and - # not existing_placeholder_app.stat.exists + - name: Create a placeholder app, to run until we deploy a real version + become_user: impress + copy: + src: files/initial-placeholder + dest: /srv/impress/versions + when: | + not current_app_version.stat.exists and + not existing_placeholder_app.stat.exists - # - name: Configure the placeholder app to run in deployment mode - # become_user: impress - # command: - # chdir: /srv/impress/versions/initial-placeholder - # cmd: /opt/ruby-3.1.4/bin/bundle config set --local deployment true - # when: not current_app_version.stat.exists + - name: Configure the placeholder app to run in deployment mode + become_user: impress + command: + chdir: /srv/impress/versions/initial-placeholder + cmd: /opt/ruby-3.1.4/bin/bundle config set --local deployment true + when: not current_app_version.stat.exists - # - name: Install the placeholder app's dependencies - # become_user: impress - # command: - # chdir: /srv/impress/versions/initial-placeholder - # cmd: /opt/ruby-3.1.4/bin/bundle install - # when: not current_app_version.stat.exists + - name: Install the placeholder app's dependencies + become_user: impress + command: + chdir: /srv/impress/versions/initial-placeholder + cmd: /opt/ruby-3.1.4/bin/bundle install + when: not current_app_version.stat.exists - # - name: Set the placeholder app as the current version - # become_user: impress - # file: - # src: /srv/impress/versions/initial-placeholder - # dest: /srv/impress/current - # state: link - # when: not current_app_version.stat.exists + - name: Set the placeholder app as the current version + become_user: impress + file: + src: /srv/impress/versions/initial-placeholder + dest: /srv/impress/current + state: link + when: not current_app_version.stat.exists - # # NOTE: This file is uploaded with stricter permissions, to help protect - # # the secrets inside. Most of the app is world-readable for convenience - # # for debugging and letting nginx serve static files, but keep this safer! - # - name: Upload the production.env file - # become_user: impress - # copy: - # dest: /srv/impress/shared/production.env - # src: files/production.env - # mode: "600" - # notify: - # - Reload systemctl - # - Restart impress + # NOTE: This file is uploaded with stricter permissions, to help protect + # the secrets inside. Most of the app is world-readable for convenience + # for debugging and letting nginx serve static files, but keep this safer! + - name: Upload the production.env file + become_user: impress + copy: + dest: /srv/impress/shared/production.env + src: files/production.env + mode: "600" + notify: + - Reload systemctl + - Restart impress - name: Create service file for impress copy: