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!
This commit is contained in:
Emi Matchu 2024-01-29 04:21:19 -08:00
parent 7cf3786023
commit 4fff8d88f2
10 changed files with 264 additions and 231 deletions

View file

@ -87,9 +87,20 @@ module ApplicationHelper
!@hide_home_link !@hide_home_link
end end
def impress_2020_meta_tag def support_staff?
tag 'meta', name: "impress-2020-origin", user_signed_in? && current_user.support_staff?
content: Rails.configuration.impress_2020_origin 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 end
JAVASCRIPT_LIBRARIES = { JAVASCRIPT_LIBRARIES = {

View file

@ -1,28 +1,31 @@
import * as React from "react"; import * as React from "react";
import { getSupportSecret } from "../../impress-2020-config";
/** /**
* useSupport returns the Support secret that the server requires for Support * useSupport returns the Support secret that the server requires for Support
* actions... if the user has it set. For most users, this returns nothing! * 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: * Specifically, we return an object of:
* - isSupportUser: true iff the `supportSecret` is set * - isSupportUser: true iff the `supportSecret` is set
* - supportSecret: the secret saved to this device, or null if not set * - supportSecret: the secret saved to this device, or null if not set
* *
* To become a Support user, you visit /?supportSecret=..., which saves the * To become a Support user, get the `support_staff` flag set on your user
* secret to your device. * 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 * 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 * possible that it will return an invalid secret. That's okay, because
* the server checks the provided secret for each Support request. * the server checks the provided secret for each Support request.
*/ */
function useSupport() { function useSupport() {
const supportSecret = React.useMemo( const supportSecret = getSupportSecret();
() =>
typeof localStorage !== "undefined"
? localStorage.getItem("supportSecret")
: null,
[],
);
const isSupportUser = supportSecret != null; const isSupportUser = supportSecret != null;

View file

@ -1,10 +1,20 @@
const IMPRESS_2020_ORIGIN = readImpress2020Origin(); const ORIGIN = readOrigin();
const SUPPORT_SECRET = readSupportSecret();
export function buildImpress2020Url(path) { 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]"); const node = document.querySelector("meta[name=impress-2020-origin]");
return node?.content || "https://impress-2020.openneo.net" return node?.content || "https://impress-2020.openneo.net"
} }
function readSupportSecret() {
const node = document.querySelector("meta[name=impress-2020-support-secret]");
return node?.content || null;
}

View file

@ -17,7 +17,7 @@
= yield :meta = yield :meta
= open_graph_tags = open_graph_tags
= csrf_meta_tag = csrf_meta_tag
= impress_2020_meta_tag = impress_2020_meta_tags
= signed_in_meta_tag = signed_in_meta_tag
- if user_signed_in? - if user_signed_in?
= current_user_id_meta_tag = current_user_id_meta_tag

View file

@ -20,7 +20,7 @@
= render 'static/analytics' = render 'static/analytics'
= open_graph_tags = open_graph_tags
= csrf_meta_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"} %meta{name: 'dti-current-user-id', content: user_signed_in? ? current_user.id : "null"}
%body %body
#wardrobe-2020-root #wardrobe-2020-root

View file

@ -1,2 +1,5 @@
Rails.configuration.impress_2020_origin = Rails.configuration.x.impress_2020.origin =
ENV["IMPRESS_2020_ORIGIN"] || "https://impress-2020.openneo.net" ENV.fetch("IMPRESS_2020_ORIGIN", "https://impress-2020.openneo.net")
Rails.configuration.x.impress_2020.support_secret =
ENV.fetch("IMPRESS_2020_SUPPORT_SECRET", nil)

View file

@ -89,7 +89,7 @@ OpenneoImpressItems::Application.routes.draw do
# Static pages! # Static pages!
get '/terms', as: :terms, 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! # Other useful lil things!
get '/sitemap.xml' => 'sitemap#index', :as => :sitemap, :format => :xml get '/sitemap.xml' => 'sitemap#index', :as => :sitemap, :format => :xml

View file

@ -0,0 +1,5 @@
class AddSupportStaffToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :support_staff, :boolean, null: false, default: false
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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| create_table "alt_styles", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
t.integer "species_id", null: false t.integer "species_id", null: false
t.integer "color_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 "wanted_closet_hangers_visibility", default: 1, null: false
t.integer "contact_neopets_connection_id" t.integer "contact_neopets_connection_id"
t.timestamp "last_trade_activity_at" t.timestamp "last_trade_activity_at"
t.boolean "support_staff", default: false, null: false
end end
create_table "zone_translations", id: :integer, charset: "latin1", collation: "latin1_swedish_ci", force: :cascade do |t| create_table "zone_translations", id: :integer, charset: "latin1", collation: "latin1_swedish_ci", force: :cascade do |t|

View file

@ -7,253 +7,253 @@
email_address: "emi@matchu.dev" # TODO: Extract this to personal config? email_address: "emi@matchu.dev" # TODO: Extract this to personal config?
impress_hostname: impress.openneo.net impress_hostname: impress.openneo.net
tasks: tasks:
# - name: Create SSH folder for logged-in user - name: Create SSH folder for logged-in user
# become: no become: no
# file: file:
# name: .ssh name: .ssh
# mode: "700" mode: "700"
# state: directory state: directory
# - name: Copy authorized SSH keys to logged-in user - name: Copy authorized SSH keys to logged-in user
# become: no become: no
# copy: copy:
# dest: ~/.ssh/authorized_keys dest: ~/.ssh/authorized_keys
# src: files/authorized-ssh-keys.txt src: files/authorized-ssh-keys.txt
# mode: "600" mode: "600"
# - name: Disable root SSH login - name: Disable root SSH login
# lineinfile: lineinfile:
# dest: /etc/ssh/sshd_config dest: /etc/ssh/sshd_config
# regexp: ^#?PermitRootLogin regexp: ^#?PermitRootLogin
# line: PermitRootLogin no line: PermitRootLogin no
# - name: Disable password-based SSH authentication - name: Disable password-based SSH authentication
# lineinfile: lineinfile:
# dest: /etc/ssh/sshd_config dest: /etc/ssh/sshd_config
# regexp: ^#?PasswordAuthentication regexp: ^#?PasswordAuthentication
# line: PasswordAuthentication no line: PasswordAuthentication no
# - name: Enable public-key SSH authentication - name: Enable public-key SSH authentication
# lineinfile: lineinfile:
# dest: /etc/ssh/sshd_config dest: /etc/ssh/sshd_config
# regexp: ^#?PubkeyAuthentication regexp: ^#?PubkeyAuthentication
# line: PubkeyAuthentication yes line: PubkeyAuthentication yes
# - name: Update the apt cache - name: Update the apt cache
# apt: apt:
# update_cache: yes update_cache: yes
# - name: Install fail2ban firewall with default settings - name: Install fail2ban firewall with default settings
# apt: apt:
# name: fail2ban name: fail2ban
# - name: Configure ufw firewall to allow SSH connections on port 22 - name: Configure ufw firewall to allow SSH connections on port 22
# community.general.ufw: community.general.ufw:
# rule: allow rule: allow
# port: "22" port: "22"
# - name: Configure ufw firewall to allow HTTP connections on port 80 - name: Configure ufw firewall to allow HTTP connections on port 80
# community.general.ufw: community.general.ufw:
# rule: allow rule: allow
# port: "80" port: "80"
# - name: Configure ufw firewall to allow HTTPS connections on port 443 - name: Configure ufw firewall to allow HTTPS connections on port 443
# community.general.ufw: community.general.ufw:
# rule: allow rule: allow
# port: "443" port: "443"
# - name: Enable ufw firewall with all other ports closed by default - name: Enable ufw firewall with all other ports closed by default
# community.general.ufw: community.general.ufw:
# state: enabled state: enabled
# policy: deny policy: deny
# - name: Install unattended-upgrades - name: Install unattended-upgrades
# apt: apt:
# name: unattended-upgrades name: unattended-upgrades
# - name: Enable unattended-upgrades to auto-upgrade our system - name: Enable unattended-upgrades to auto-upgrade our system
# copy: copy:
# content: | content: |
# APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Update-Package-Lists "1";
# APT::Periodic::Unattended-Upgrade "1"; APT::Periodic::Unattended-Upgrade "1";
# dest: /etc/apt/apt.conf.d/20auto-upgrades dest: /etc/apt/apt.conf.d/20auto-upgrades
# - name: Configure unattended-upgrades to auto-reboot our server when necessary - name: Configure unattended-upgrades to auto-reboot our server when necessary
# lineinfile: lineinfile:
# regex: ^(//\s*)?Unattended-Upgrade::Automatic-Reboot ".*";$ regex: ^(//\s*)?Unattended-Upgrade::Automatic-Reboot ".*";$
# line: Unattended-Upgrade::Automatic-Reboot "true"; line: Unattended-Upgrade::Automatic-Reboot "true";
# dest: /etc/apt/apt.conf.d/50unattended-upgrades dest: /etc/apt/apt.conf.d/50unattended-upgrades
# - name: Configure unattended-upgrades to delay necessary reboots to 3am - name: Configure unattended-upgrades to delay necessary reboots to 3am
# lineinfile: lineinfile:
# regex: ^(//\s*)?Unattended-Upgrade::Automatic-Reboot-Time ".*";$ regex: ^(//\s*)?Unattended-Upgrade::Automatic-Reboot-Time ".*";$
# line: Unattended-Upgrade::Automatic-Reboot-Time "03:00"; line: Unattended-Upgrade::Automatic-Reboot-Time "03:00";
# dest: /etc/apt/apt.conf.d/50unattended-upgrades dest: /etc/apt/apt.conf.d/50unattended-upgrades
# - name: Configure the system timezone to be US Pacific time - name: Configure the system timezone to be US Pacific time
# community.general.timezone: community.general.timezone:
# name: America/Los_Angeles name: America/Los_Angeles
# - name: Create "impress" user - name: Create "impress" user
# user: user:
# name: impress name: impress
# comment: Impress App comment: Impress App
# home: /srv/impress home: /srv/impress
# create_home: false create_home: false
# shell: /bin/bash shell: /bin/bash
# - name: Create "impress-deployers" group - name: Create "impress-deployers" group
# group: group:
# name: impress-deployers name: impress-deployers
# - name: Add the current user to the "impress-deployers" group - name: Add the current user to the "impress-deployers" group
# user: user:
# name: "{{ lookup('env', 'USER') }}" name: "{{ lookup('env', 'USER') }}"
# groups: groups:
# - impress-deployers - impress-deployers
# append: yes append: yes
# # We use this so the deploy playbook doesn't have to prompt for a root # 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. # 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 - name: Enable the "impress-deployers" group to freely act as the "impress" user
# community.general.sudoers: community.general.sudoers:
# name: impress-deployers-as-impress name: impress-deployers-as-impress
# group: impress-deployers group: impress-deployers
# runas: impress runas: impress
# commands: ALL commands: ALL
# nopassword: yes nopassword: yes
# # Similarly, this enables us to manage the impress service in the deploy playbook # Similarly, this enables us to manage the impress service in the deploy playbook
# # and in live debugging without a password. # and in live debugging without a password.
# # NOTE: In the sudoers file, you need to specify the full path to the # 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! # 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 - name: Enable the "impress-deployers" group to freely start and stop the impress service
# community.general.sudoers: community.general.sudoers:
# name: impress-deployers-systemctl name: impress-deployers-systemctl
# group: impress-deployers group: impress-deployers
# commands: commands:
# - /bin/systemctl status impress - /bin/systemctl status impress
# - /bin/systemctl start impress - /bin/systemctl start impress
# - /bin/systemctl stop impress - /bin/systemctl stop impress
# - /bin/systemctl restart impress - /bin/systemctl restart impress
# nopassword: yes nopassword: yes
# - name: Install ACL, to enable us to run commands as the "impress" user - name: Install ACL, to enable us to run commands as the "impress" user
# apt: apt:
# name: acl name: acl
# - name: Install ruby-build - name: Install ruby-build
# git: git:
# repo: https://github.com/rbenv/ruby-build.git repo: https://github.com/rbenv/ruby-build.git
# dest: /opt/ruby-build dest: /opt/ruby-build
# version: 4d4678bc1ed89aa6900c0ea0da23495445dbcf50 version: 4d4678bc1ed89aa6900c0ea0da23495445dbcf50
# - name: Check if Ruby 3.1.4 is already installed - name: Check if Ruby 3.1.4 is already installed
# stat: stat:
# path: /opt/ruby-3.1.4 path: /opt/ruby-3.1.4
# register: ruby_dir register: ruby_dir
# - name: Install Ruby 3.1.4 - name: Install Ruby 3.1.4
# command: "/opt/ruby-build/bin/ruby-build 3.1.4 /opt/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 when: not ruby_dir.stat.exists
# - name: Add Ruby 3.1.4 to the global PATH, for developer convenience - name: Add Ruby 3.1.4 to the global PATH, for developer convenience
# lineinfile: lineinfile:
# dest: /etc/profile dest: /etc/profile
# line: 'PATH="/opt/ruby-3.1.4/bin:$PATH" # Added by impress deploy setup script' line: 'PATH="/opt/ruby-3.1.4/bin:$PATH" # Added by impress deploy setup script'
# - name: Install system dependencies for impress's Ruby gems - name: Install system dependencies for impress's Ruby gems
# apt: apt:
# name: name:
# - libmysqlclient-dev - libmysqlclient-dev
# - libyaml-dev - libyaml-dev
# - name: Create the app folder - name: Create the app folder
# file: file:
# path: /srv/impress path: /srv/impress
# owner: impress owner: impress
# group: impress group: impress
# mode: "755" mode: "755"
# state: directory state: directory
# - name: Add a convenient .bash_profile for when we log in as "impress" - name: Add a convenient .bash_profile for when we log in as "impress"
# copy: copy:
# owner: impress owner: impress
# group: impress group: impress
# dest: /srv/impress/.bash_profile dest: /srv/impress/.bash_profile
# content: | content: |
# set -a # Export all of the below set -a # Export all of the below
# RAILS_ENV=production RAILS_ENV=production
# EXECJS_RUNTIME=Disabled EXECJS_RUNTIME=Disabled
# source /srv/impress/shared/production.env source /srv/impress/shared/production.env
# set +a set +a
# - name: Create the app's "versions" folder - name: Create the app's "versions" folder
# become_user: impress become_user: impress
# file: file:
# path: /srv/impress/versions path: /srv/impress/versions
# state: directory state: directory
# - name: Create the app's "shared" folder - name: Create the app's "shared" folder
# become_user: impress become_user: impress
# file: file:
# path: /srv/impress/shared path: /srv/impress/shared
# state: directory state: directory
# - name: Check for a current app version - name: Check for a current app version
# stat: stat:
# path: /srv/impress/current path: /srv/impress/current
# register: current_app_version register: current_app_version
# - name: Check whether we already have a placeholder app - name: Check whether we already have a placeholder app
# stat: stat:
# path: /srv/impress/versions/initial-placeholder path: /srv/impress/versions/initial-placeholder
# register: existing_placeholder_app register: existing_placeholder_app
# when: not current_app_version.stat.exists when: not current_app_version.stat.exists
# - name: Create a placeholder app, to run until we deploy a real version - name: Create a placeholder app, to run until we deploy a real version
# become_user: impress become_user: impress
# copy: copy:
# src: files/initial-placeholder src: files/initial-placeholder
# dest: /srv/impress/versions dest: /srv/impress/versions
# when: | when: |
# not current_app_version.stat.exists and not current_app_version.stat.exists and
# not existing_placeholder_app.stat.exists not existing_placeholder_app.stat.exists
# - name: Configure the placeholder app to run in deployment mode - name: Configure the placeholder app to run in deployment mode
# become_user: impress become_user: impress
# command: command:
# chdir: /srv/impress/versions/initial-placeholder chdir: /srv/impress/versions/initial-placeholder
# cmd: /opt/ruby-3.1.4/bin/bundle config set --local deployment true cmd: /opt/ruby-3.1.4/bin/bundle config set --local deployment true
# when: not current_app_version.stat.exists when: not current_app_version.stat.exists
# - name: Install the placeholder app's dependencies - name: Install the placeholder app's dependencies
# become_user: impress become_user: impress
# command: command:
# chdir: /srv/impress/versions/initial-placeholder chdir: /srv/impress/versions/initial-placeholder
# cmd: /opt/ruby-3.1.4/bin/bundle install cmd: /opt/ruby-3.1.4/bin/bundle install
# when: not current_app_version.stat.exists when: not current_app_version.stat.exists
# - name: Set the placeholder app as the current version - name: Set the placeholder app as the current version
# become_user: impress become_user: impress
# file: file:
# src: /srv/impress/versions/initial-placeholder src: /srv/impress/versions/initial-placeholder
# dest: /srv/impress/current dest: /srv/impress/current
# state: link state: link
# when: not current_app_version.stat.exists when: not current_app_version.stat.exists
# # NOTE: This file is uploaded with stricter permissions, to help protect # NOTE: This file is uploaded with stricter permissions, to help protect
# # the secrets inside. Most of the app is world-readable for convenience # the secrets inside. Most of the app is world-readable for convenience
# # for debugging and letting nginx serve static files, but keep this safer! # for debugging and letting nginx serve static files, but keep this safer!
# - name: Upload the production.env file - name: Upload the production.env file
# become_user: impress become_user: impress
# copy: copy:
# dest: /srv/impress/shared/production.env dest: /srv/impress/shared/production.env
# src: files/production.env src: files/production.env
# mode: "600" mode: "600"
# notify: notify:
# - Reload systemctl - Reload systemctl
# - Restart impress - Restart impress
- name: Create service file for impress - name: Create service file for impress
copy: copy: