[deploy] Feature: Setup Admin Area to Update Vault Secrets (#8935)

This commit is contained in:
Molly Struve 2020-06-29 15:08:09 -05:00 committed by GitHub
parent bacff174a6
commit 4e0fee644c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 165 additions and 6 deletions

View file

@ -28,6 +28,7 @@ class Internal::ApplicationController < ApplicationController
{ name: "tags", controller: "tags" },
{ name: "tools", controller: "tools" },
{ name: "users", controller: "users" },
{ name: "vault secrets", controller: "secrets" },
{ name: "webhooks", controller: "webhook_endpoints" },
{ name: "welcome", controller: "welcome" },
].sort_by { |menu_item| menu_item[:name] }.freeze

View file

@ -0,0 +1,33 @@
class Internal::SecretsController < Internal::ApplicationController
layout "internal"
before_action :validate_settable_secret, only: [:update]
after_action only: [:update] do
Audit::Logger.log(:internal, current_user, params.dup)
end
def index
@vault_enabled = AppSecrets.vault_enabled?
@secrets = AppSecrets::SETTABLE_SECRETS.map do |key|
secret_value = AppSecrets[key]
secret_value = secret_value.present? ? "#{secret_value.first(8)}******" : "Not In Vault"
[key, secret_value]
end
end
def update
AppSecrets[params[:key_name]] = params[:key_value]
flash[:success] = "Secret #{params[:key_name]} was successfully updated in Vault."
redirect_to internal_secrets_path
end
private
def validate_settable_secret
update_param = params.permit(*AppSecrets::SETTABLE_SECRETS)
params[:key_name], params[:key_value] = update_param.to_h.to_a.first
bad_request unless update_param.present? && params[:key_name].is_a?(String) && params[:key_value].is_a?(String)
end
end

View file

@ -1,6 +1,12 @@
class AppSecrets
SETTABLE_SECRETS = %w[
SLACK_CHANNEL
SLACK_DEPLOY_CHANNEL
SLACK_WEBHOOK_URL
].freeze
def self.[](key)
result = Vault.kv(namespace).read(key)&.data&.fetch(:value) if ENV["VAULT_TOKEN"].present?
result = Vault.kv(namespace).read(key)&.data&.fetch(:value) if vault_enabled?
result ||= ApplicationConfig[key]
result
@ -12,6 +18,10 @@ class AppSecrets
Vault.kv(namespace).write(key, value: value)
end
def self.vault_enabled?
ENV["VAULT_TOKEN"].present?
end
def self.namespace
ENV["VAULT_SECRET_NAMESPACE"]
end

7
app/models/secret.rb Normal file
View file

@ -0,0 +1,7 @@
class Secret < ApplicationRecord
# This class exists to take advantage of Rolify for limiting authorization
# on internal vault secrets.
# NOTE: It is not backed by a database table and should not be expected to
# function like a traditional Rails model
resourcify
end

View file

@ -0,0 +1,29 @@
<% if !@vault_enabled %>
<div class="alert alert-warning" role="alert">
<p>
Vault is not currently setup for your application. This means your secrets are being stored as ENV variables and cannot be updated here. To update them you will need to update the appropriate files in your environment.
<p>
</div>
<% end %>
<table class="table">
<thead>
<tr>
<th scope="col">Secret Name</th>
<th scope="col">Secret Value</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<% @secrets.each do |key, partial_value| %>
<tr>
<%= form_with(url: "/internal/secrets", method: "PUT", local: true) do %>
<td><%= label_tag key, key %></td>
<td><%= text_field_tag key, partial_value %></td>
<td>
<%= submit_tag("Update", data: { confirm: "My username is @#{current_user.username} and I want to update this Vault Secret." }, disabled: !@vault_enabled) %></td>
<% end %>
</tr>
<% end %>
</tbody>
</table>

View file

@ -106,6 +106,8 @@ Rails.application.routes.draw do
resources :badges, only: :index
post "badges/award_badges", to: "badges#award_badges"
resources :path_redirects, only: %i[new create index edit update destroy]
resources :secrets, only: %i[index]
put "secrets", to: "secrets#update"
end
namespace :stories, defaults: { format: "json" } do

View file

@ -10,13 +10,12 @@ RSpec.describe AppSecrets, type: :lib do
allow(described_class).to receive(:namespace).and_return(namespace)
allow(Vault).to receive(:kv) { vault_stub }
allow(ApplicationConfig).to receive(:[])
allow(ENV).to receive(:[])
end
describe "[]" do
context "with VAULT_TOKEN present" do
context "with vault_enabled" do
before do
allow(ENV).to receive(:[]).with("VAULT_TOKEN").and_return("present")
allow(described_class).to receive(:vault_enabled?).and_return(true)
end
it "fetches keys from Vault" do
@ -40,8 +39,8 @@ RSpec.describe AppSecrets, type: :lib do
end
end
context "without VAULT_TOKEN present" do
before { allow(ApplicationConfig).to receive(:[]).with("VAULT_TOKEN").and_return("") }
context "without vault_enabled" do
before { allow(described_class).to receive(:vault_enabled?).and_return(false) }
it "fetches keys from ApplicationConfig" do
allow(Vault).to receive(:kv) { instance_double("Vault::Kv", read: nil) }
@ -62,4 +61,18 @@ RSpec.describe AppSecrets, type: :lib do
expect(write_stub).to have_received(:write).with(key, value: "secret-value")
end
end
describe "#vault_enabled?" do
before { allow(ENV).to receive(:[]) }
it "returns true if VAULT_TOKEN present" do
allow(ENV).to receive(:[]).with("VAULT_TOKEN").and_return("present")
expect(described_class.vault_enabled?).to be(true)
end
it "returns false if VAULT_TOKEN is missing" do
allow(ENV).to receive(:[]).with("VAULT_TOKEN").and_return("")
expect(described_class.vault_enabled?).to be(false)
end
end
end

View file

@ -0,0 +1,64 @@
require "rails_helper"
RSpec.describe "/internal/secrets", type: :request do
before do
allow(AppSecrets).to receive(:vault_enabled?).and_return(true)
allow(AppSecrets).to receive(:[]=)
end
context "when the user is not an admin" do
it "blocks the request" do
user = create(:user)
sign_in user
expect do
get internal_secrets_path
end.to raise_error(Pundit::NotAuthorizedError)
end
end
context "when the user is an admin" do
let(:admin) { create(:user, :admin) }
before { sign_in admin }
describe "GET /internal/secrets" do
it "renders with status 200" do
get internal_secrets_path
expect(response.status).to eq 200
end
it "displays an alert when Vault is not enabled" do
allow(AppSecrets).to receive(:vault_enabled?).and_return(false)
get internal_secrets_path
expect(response.body).to include("Vault is not currently setup for your application")
end
end
describe "PUT /internal/secrets" do
let(:valid_secret) { AppSecrets::SETTABLE_SECRETS.first }
let(:valid_params) { { valid_secret => "SECRET_VALUE" } }
it "successfully sets a secret and shows flash message" do
allow(AppSecrets).to receive(:[]=)
put internal_secrets_path, params: valid_params
expect(response.status).to eq 302
expect(AppSecrets).to have_received(:[]=).with(valid_secret, "SECRET_VALUE")
expect(flash[:success]).to include("Secret #{valid_secret} was successfully updated in Vault")
end
it "returns a bad_request with invalid params" do
put internal_secrets_path, params: {}
expect(response.status).to eq 400
end
it "creates an audit log" do
Audit::Subscribe.listen :internal
expect do
put internal_secrets_path, params: valid_params
end.to change(AuditLog, :count).by(1)
Audit::Subscribe.forget :internal
end
end
end
end