Add keybase integration (#661)

This commit is contained in:
Gus Caplan 2019-06-12 08:08:57 -05:00 committed by Peter Bhat Harkins
parent 75750213da
commit 4fc391e97d
16 changed files with 329 additions and 0 deletions

1
.gitignore vendored
View File

@ -38,6 +38,7 @@ public/apple-touch-icon*
# files added in production
lib/tasks/deploy.rake
config/initializers/production.rb
config/initializers/development.rb
config/database.yml
config/initializers/secret_token.rb
config/*.sphinx.conf

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M0,0c0,5.33,0,10.67,0,16c5.33,0,10.67,0,16,0c0-5.33,0-10.67,0-16C10.67,0,5.33,0,0,0z M13.14,13.45c-3.2,0-6.43,0-9.63,0
c0-0.1,0-0.2,0-0.3c0-0.11,0-0.22,0-0.33c0-0.11,0-0.1,0.1-0.11c0.23-0.02,0.46-0.03,0.69-0.07c0.46-0.08,0.67-0.31,0.74-0.78
c0.02-0.14,0.03-0.29,0.03-0.43c0-2.25,0-4.5,0-6.74c0-0.18-0.02-0.36-0.04-0.53C4.99,3.85,4.79,3.66,4.5,3.55
c-0.27-0.1-0.55-0.11-0.83-0.13c-0.05,0-0.1,0-0.15,0c0-0.03,0-0.06,0-0.08c0-0.19,0-0.38,0-0.57c0-0.07,0.02-0.09,0.09-0.09
c1.04,0,2.07,0,3.11,0c0.71,0,1.42,0,2.13,0c0.07,0,0.1,0.01,0.1,0.09c-0.01,0.19,0,0.37,0,0.56c0,0.03,0,0.05,0,0.08
c-0.14,0.01-0.28,0-0.41,0.02c-0.2,0.03-0.4,0.06-0.59,0.11C7.6,3.65,7.42,3.89,7.38,4.22c-0.02,0.15-0.03,0.3-0.03,0.45
c0,2.13,0,4.26,0,6.39c0,0.27,0.03,0.54,0.06,0.8c0.03,0.29,0.22,0.48,0.49,0.58c0.23,0.09,0.48,0.11,0.72,0.12
c0.63,0.02,1.25,0.01,1.86-0.15c0.88-0.22,1.47-0.77,1.76-1.64c0.09-0.28,0.13-0.57,0.2-0.86c0.01-0.02,0.03-0.06,0.05-0.06
c0.23,0,0.42,0,0.66,0C13.11,11.06,13.16,12.25,13.14,13.45z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<g>
<path fill="#AC130D" d="M16,16c-5.33,0-10.67,0-16,0C0,10.67,0,5.33,0,0c5.33,0,10.67,0,16,0C16,5.33,16,10.67,16,16z"/>
<path fill="#FFFFFF" d="M3.5,13.45c0-0.1,0-0.2,0-0.3c0-0.11,0-0.22,0-0.33c0-0.11,0-0.1,0.1-0.11c0.23-0.02,0.46-0.03,0.69-0.07
c0.46-0.08,0.67-0.31,0.74-0.78c0.02-0.14,0.03-0.29,0.03-0.43c0-2.25,0-4.5,0-6.74c0-0.18-0.02-0.36-0.04-0.53
C4.99,3.85,4.79,3.66,4.5,3.55c-0.27-0.1-0.55-0.11-0.83-0.13c-0.05,0-0.1,0-0.15,0c0-0.03,0-0.06,0-0.08c0-0.19,0-0.38,0-0.57
c0-0.07,0.02-0.09,0.09-0.09c1.04,0,2.07,0,3.11,0c0.71,0,1.42,0,2.13,0c0.07,0,0.1,0.01,0.1,0.09c-0.01,0.19,0,0.37,0,0.56
c0,0.03,0,0.05,0,0.08c-0.14,0.01-0.28,0-0.41,0.02c-0.2,0.03-0.4,0.06-0.59,0.11C7.6,3.65,7.42,3.89,7.38,4.22
c-0.02,0.15-0.03,0.3-0.03,0.45c0,2.13,0,4.26,0,6.39c0,0.27,0.03,0.54,0.06,0.8c0.03,0.29,0.22,0.48,0.49,0.58
c0.23,0.09,0.48,0.11,0.72,0.12c0.63,0.02,1.25,0.01,1.86-0.15c0.88-0.22,1.47-0.77,1.76-1.64c0.09-0.28,0.13-0.57,0.2-0.86
c0.01-0.02,0.03-0.06,0.05-0.06c0.23,0,0.42,0,0.66,0c-0.02,1.2,0.02,2.4,0,3.6C9.94,13.45,6.71,13.45,3.5,13.45z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,73 @@
class KeybaseProofsController < ApplicationController
before_action :require_logged_in_user, only: [:new, :create]
before_action :check_new_params, only: :new
before_action :check_user_matches, only: :new
before_action :force_to_json, only: [:kbconfig]
def new
@kb_username = params[:kb_username]
@kb_signature = params[:kb_signature]
@kb_ua = params[:kb_ua]
@kb_avatar = Keybase.avatar_url(@kb_username)
end
def create
kb_username = post_params[:kb_username]
kb_signature = post_params[:kb_signature]
kb_ua = post_params[:kb_ua]
if Keybase.proof_valid?(kb_username, kb_signature, @user.username)
@user.add_or_update_keybase_proof(kb_username, kb_signature)
@user.save!
return redirect_to Keybase.success_url(kb_username, kb_signature, kb_ua, @user.username)
else
flash[:error] = "Failed to connect your account to Keybase. Try again from Keybase."
return redirect_to settings_path
end
end
def kbconfig
return render json: {} unless Keybase.enabled?
@domain = Keybase.DOMAIN
@name = Rails.application.name
@brand_color = "#AC130D"
@description = "Computing-focused community centered around link aggregation and discussion"
@contacts = ["admin@#{Keybase.DOMAIN}"]
# rubocop:disable Style/FormatStringToken
@prefill_url = "#{new_keybase_proof_url}?kb_username=%{kb_username}&" \
"kb_signature=%{sig_hash}&kb_ua=%{kb_ua}&username=%{username}"
@profile_url = "#{u_url}/%{username}"
@check_url = "#{u_url}/%{username}.json"
# rubocop:enable Style/FormatStringToken
@logo_black = "https://lobste.rs/small-black-logo.svg"
@logo_full = "https://lobste.rs/full-color.logo.svg"
@user_re = User.username_regex_s[1...-1]
end
private
def force_to_json
request.format = :json
end
def check_user_matches
unless case_insensitive_match?(@user.username, params[:username])
flash[:error] = "not logged in as the correct user"
return redirect_to settings_path
end
end
def case_insensitive_match?(first_string, second_string)
# can replace this with first_string.casecmp?(second_string) when ruby >= 2.4.6
first_string.casecmp(second_string).zero?
end
def post_params
params.require(:keybase_proof).permit(:kb_username, :kb_signature, :kb_ua, :username)
end
def check_new_params
redirect_to settings_path unless [:kb_username, :kb_signature, :kb_ua, :username].all? do |k|
params[k].present?
end
end
end

View File

@ -0,0 +1,9 @@
module KeybaseProofsHelper
def keybase_user_link(kb_sig)
File.join Keybase.BASE_URL, kb_sig[:kb_username]
end
def keybase_proof_link(kb_sig)
File.join Keybase.BASE_URL, kb_sig[:kb_username], "sigchain\##{kb_sig[:sig_hash]}"
end
end

View File

@ -67,6 +67,7 @@ class User < ApplicationRecord
s.string :twitter_oauth_token
s.string :twitter_oauth_token_secret
s.string :twitter_username
s.any :keybase_signatures, array: true
s.string :homepage
end
@ -169,6 +170,10 @@ class User < ApplicationRecord
h[:twitter_username] = self.twitter_username
end
if self.keybase_signatures.present?
h[:keybase_signatures] = self.keybase_signatures
end
h
end
@ -433,6 +438,12 @@ class User < ApplicationRecord
Time.current - self.created_at <= NEW_USER_DAYS.days
end
def add_or_update_keybase_proof(kb_username, kb_signature)
self.keybase_signatures ||= []
self.keybase_signatures.reject! {|kbsig| kbsig['kb_username'] == kb_username }
self.keybase_signatures.push('kb_username' => kb_username, 'sig_hash' => kb_signature)
end
def is_heavy_self_promoter?
total_count = self.stories_submitted_count

View File

@ -0,0 +1,13 @@
<% if user.keybase_signatures? %>
Linked to
<% user.keybase_signatures.each do |kbs| %>
<strong><%= link_to "@#{kbs[:kb_username]}", keybase_user_link(kbs) %></strong>
<strong><%= link_to "✔", keybase_proof_link(kbs) %></strong>
&nbsp;
<% end %>
<% if for_self %>
(<%= link_to "Disconnect on Keybase", "https://keybase.io/download" %>)
<% end %>
<% elsif for_self %>
Install <%= link_to "Keybase", "https://keybase.io/download" %> to prove a cryptographic connection.
<% end %>

View File

@ -0,0 +1,24 @@
# see https://keybase.io/docs/proof_integration_guide#1-config
JSON.pretty_generate({
"version": 1,
"domain": @domain,
"display_name": @name,
"description": @description,
"brand_color": @brand_color,
"logo": {
"svg_black": @logo_black,
"svg_full": @logo_full
},
"username": {
"re": @user_re,
"min": 1,
"max": 25
},
"prefill_url": @prefill_url,
"profile_url": @profile_url,
"check_url": @check_url,
"check_path": ["keybase_signatures"],
"avatar_path": ["avatar_url"],
"contact": @contacts
})

View File

@ -0,0 +1,25 @@
<div class="box wide">
<div class="legend">
<span>
Connect Your Account (<%= @user.username %>) to Keybase
</span>
</div>
<div id="gravatar">
<%= avatar_img(@user, 100) %>
</div>
<p>
<%= image_tag @kb_avatar, width: 100 %>
</p>
<%= form_with(url: keybase_proofs_url, scope: :keybase_proof) do |f| %>
Is this you on Keybase? <b><i><%= @kb_username %></b></i>
<%= f.hidden_field "kb_username", value: @kb_username %>
<%= f.hidden_field "kb_signature", value: @kb_signature %>
<%= f.hidden_field "kb_ua", value: @kb_ua %>
<%= f.hidden_field "username", value: @user.username %>
<p>
<%= f.submit "Confirm" %>
</p>
<% end %>
</div>

View File

@ -144,6 +144,17 @@
</div>
<% end %>
<% if Keybase.enabled? %>
<div class="boxline">
<%= f.label :kb_username,
raw("<a href=\"https://keybase.io/\">Keybase</a>:"),
:class => "required" %>
<span>
<%= render :partial => "keybase_proofs/proofs", locals: {user: @edit_user, for_self: true} %>
</span>
</div>
<% end %>
<br>
<div class="legend">

View File

@ -149,9 +149,20 @@
<% else %>
<span class="na">A mystery...</span>
<% end %>
<br>
</div>
<% end %>
<% if Keybase.enabled? %>
<% for_self = (@showing_user == @user) %>
<% if @showing_user.keybase_signatures? || for_self %>
<label class="required">Keybase:</label>
<span class="d">
<%= render :partial => "keybase_proofs/proofs", locals: {user: @showing_user, for_self: for_self} %>
</span>
<% end %>
<% end %>
<% if @user && @user.is_moderator? && !@showing_user.is_moderator? %>
<h2>Moderator Information</h2>

View File

@ -35,5 +35,8 @@ if Rails.env.production?
BCrypt::Engine.cost = 12
Keybase.DOMAIN = Rails.application.domain
Keybase.BASE_URL = ENV.fetch('KEYBASE_BASE_URL') { 'https://keybase.io' }
ActionMailer::Base.delivery_method = :sendmail
end

View File

@ -147,6 +147,9 @@ Rails.application.routes.draw do
get "/settings/twitter_callback" => "settings#twitter_callback"
post "/settings/twitter_disconnect" => "settings#twitter_disconnect"
resources :keybase_proofs, only: [:new, :create]
get "/.well-known/keybase-proof-config" => "keybase_proofs#kbconfig", :as => "keybase_config"
get "/filters" => "filters#index"
post "/filters" => "filters#update"

52
extras/keybase.rb Normal file
View File

@ -0,0 +1,52 @@
class Keybase
cattr_accessor :DOMAIN
cattr_accessor :BASE_URL
# these need to be overridden in config/initializers/production.rb
@@DOMAIN = nil
@@BASE_URL = nil
def self.enabled?
@@DOMAIN.present? || ENV['KEYBASE_BASE_URL']
end
def self.avatar_url(kb_username)
s = Sponge.new
url = [
File.join(base_url, '/_/api/1.0/user/pic_url.json?'),
"username=#{kb_username}",
].join('')
res = s.fetch(url, :get).body
return JSON.parse(res).fetch('pic_url', default_keybase_avatar_url)
rescue ::DNSError, ::JSON::ParserError
default_keybase_avatar_url
end
def self.proof_valid?(kb_username, kb_signature, username)
s = Sponge.new
url = [
File.join(base_url, '/_/api/1.0/sig/proof_valid.json?'),
"domain=#{@@DOMAIN}&",
"kb_username=#{kb_username}&",
"sig_hash=#{kb_signature}&",
"username=#{username}",
].join('')
res = s.fetch(url, :get).body
js = JSON.parse(res)
return js && js["proof_valid"].present? && js["proof_valid"]
end
def self.success_url(kb_username, kb_signature, kb_ua, username)
File.join(base_url, "/_/proof_creation_success?domain=#{@@DOMAIN}&" \
"kb_username=#{kb_username}&username=#{username}&" \
"sig_hash=#{kb_signature}&kb_ua=#{kb_ua}")
end
def self.default_keybase_avatar_url
"https://keybase.io/images/icons/icon-keybase-logo-48@2x.png"
end
def self.base_url
@@BASE_URL || "https://keybase.io"
end
end

View File

@ -141,6 +141,8 @@ class Sponge
end
if BAD_NETS.select {|n| IPAddr.new(n).include?(ip) }.any?
# This blocks all requests to localhost, so you might need to comment
# it out if you're building an end-to-end integration locally.
raise BadIPsError.new("refusing to talk to IP #{ip}")
end

View File

@ -0,0 +1,67 @@
require "rails_helper"
describe KeybaseProofsController do
render_views
let(:user) { create(:user) }
let(:kb_username) { "cryptojim" }
let(:kb_sig) { "1"*66 }
let(:valid_kb_params) do
{ kb_username: kb_username, kb_signature: kb_sig,
kb_ua: "sega-genesis", username: user.username, }
end
let(:new_params) { valid_kb_params }
let(:create_params) { { keybase_proof: valid_kb_params } }
before do
stub_login_as user
end
context 'new' do
it 'renders the expected kb_username' do
get :new, params: new_params
expect(response.body).to include(kb_username)
end
end
context 'create' do
context 'when the user does not already have a proof' do
it 'saves the signature to the user settings' do
expect(Keybase).to receive(:proof_valid?)
.with(kb_username, kb_sig, user.username).and_return(true)
post :create, params: create_params
expect(user.reload.keybase_signatures).to eq [
{ 'kb_username' => kb_username, 'sig_hash' => kb_sig },
]
end
end
context 'when the user already has proofs' do
let(:other_kb_username) { 'somethingelse' }
let(:other_kb_sig) { '3'*66 }
let(:expected_keybase_signatures) do
[
{ 'kb_username' => kb_username, 'sig_hash' => kb_sig },
{ 'kb_username' => other_kb_username, 'sig_hash' => other_kb_sig },
]
end
before do
user.add_or_update_keybase_proof(kb_username, '2'*66)
user.add_or_update_keybase_proof(other_kb_username, other_kb_sig)
user.save!
end
it 'updates the signature for the matching user and retains any others' do
expect(Keybase).to receive(:proof_valid?)
.with(kb_username, kb_sig, user.username).and_return(true)
post :create, params: create_params
expect(user.reload.keybase_signatures).to match_array expected_keybase_signatures
end
end
end
end