From 26f21fd5a03b1c6407cd81c58481288d06958ad3 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 4 Feb 2018 05:42:13 +0100 Subject: [PATCH 1/7] CAS + SAML authentication feature (#6425) * Cas authentication feature * Config * Remove class_eval + Omniauth initializer * Codeclimate review * Codeclimate review 2 * Codeclimate review 3 * Remove uid/email reconciliation * SAML authentication * Clean up code * Improve login form * Fix code style issues * Add locales --- .env.production.sample | 44 +++++++++- Gemfile | 3 + Gemfile.lock | 16 ++++ .../auth/confirmations_controller.rb | 24 ++++++ .../auth/omniauth_callbacks_controller.rb | 33 ++++++++ app/javascript/styles/mastodon/forms.scss | 18 +++++ app/models/concerns/omniauthable.rb | 81 +++++++++++++++++++ app/models/identity.rb | 22 +++++ app/models/user.rb | 2 + .../confirmations/finish_signup.html.haml | 14 ++++ app/views/auth/sessions/new.html.haml | 9 +++ config/i18n-tasks.yml | 1 + config/initializers/omniauth.rb | 59 ++++++++++++++ config/locales/en.yml | 5 ++ config/locales/fr.yml | 2 + config/routes.rb | 2 + .../20180204034416_create_identities.rb | 11 +++ db/schema.rb | 12 ++- spec/fabricators/identity_fabricator.rb | 5 ++ spec/models/identity_spec.rb | 5 ++ 20 files changed, 365 insertions(+), 3 deletions(-) create mode 100644 app/controllers/auth/omniauth_callbacks_controller.rb create mode 100644 app/models/concerns/omniauthable.rb create mode 100644 app/models/identity.rb create mode 100644 app/views/auth/confirmations/finish_signup.html.haml create mode 100644 config/initializers/omniauth.rb create mode 100644 db/migrate/20180204034416_create_identities.rb create mode 100644 spec/fabricators/identity_fabricator.rb create mode 100644 spec/models/identity_spec.rb diff --git a/.env.production.sample b/.env.production.sample index 3f0edd72ff..777336de1d 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -13,7 +13,7 @@ DB_PORT=5432 # Federation # Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation. # LOCAL_DOMAIN should *NOT* contain the protocol part of the domain e.g https://example.com. -LOCAL_DOMAIN=example.com +LOCAL_DOMAIN=example.com # Changing LOCAL_HTTPS in production is no longer supported. (Mastodon will always serve https:// links) @@ -58,7 +58,7 @@ VAPID_PUBLIC_KEY= # E-mail configuration # Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers # If you want to use an SMTP server without authentication (e.g local Postfix relay) -# then set SMTP_AUTH_METHOD and SMTP_OPENSSL_VERIFY_MODE to 'none' and +# then set SMTP_AUTH_METHOD and SMTP_OPENSSL_VERIFY_MODE to 'none' and # *comment* SMTP_LOGIN and SMTP_PASSWORD (leaving them blank is not enough). SMTP_SERVER=smtp.mailgun.org SMTP_PORT=587 @@ -135,3 +135,43 @@ STREAMING_CLUSTER_NUM=1 # If you use Docker, you may want to assign UID/GID manually. # UID=1000 # GID=1000 + +# Optional CAS authentication (cf. omniauth-cas) : +# CAS_ENABLED=true +# CAS_URL=https://sso.myserver.com/ +# CAS_HOST=sso.myserver.com/ +# CAS_PORT=443 +# CAS_SSL=true +# CAS_VALIDATE_URL= +# CAS_CALLBACK_URL= +# CAS_LOGOUT_URL= +# CAS_LOGIN_URL= +# CAS_UID_FIELD='user' +# CAS_CA_PATH= +# CAS_DISABLE_SSL_VERIFICATION=false +# CAS_UID_KEY='user' +# CAS_NAME_KEY='name' +# CAS_EMAIL_KEY='email' +# CAS_NICKNAME_KEY='nickname' +# CAS_FIRST_NAME_KEY='firstname' +# CAS_LAST_NAME_KEY='lastname' +# CAS_LOCATION_KEY='location' +# CAS_IMAGE_KEY='image' +# CAS_PHONE_KEY='phone' + +# Optional SAML authentication (cf. omniauth-saml) +# SAML_ENABLED=true +# SAML_ACS_URL= +# SAML_ISSUER=http://localhost:3000/auth/auth/saml/metadata +# SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO +# SAML_IDP_CERT= +# SAML_IDP_CERT_FINGERPRINT= +# SAML_NAME_IDENTIFIER_FORMAT= +# SAML_CERT= +# SAML_PRIVATE_KEY= +# SAML_SECURITY_WANT_ASSERTION_SIGNED=true +# SAML_SECURITY_WANT_ASSERTION_ENCRYPTED=true +# SAML_ATTRIBUTES_STATEMENTS_UID="urn:oid:0.9.2342.19200300.100.1.1" +# SAML_ATTRIBUTES_STATEMENTS_EMAIL="urn:oid:1.3.6.1.4.1.5923.1.1.1.6" +# SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.5.4.42" +# SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1" diff --git a/Gemfile b/Gemfile index f3844aca6f..5b6ae707db 100644 --- a/Gemfile +++ b/Gemfile @@ -32,6 +32,9 @@ gem 'devise', '~> 4.4' gem 'devise-two-factor', '~> 3.0' gem 'devise_pam_authenticatable2', '~> 8.0' +gem 'omniauth-cas', '~> 1.1', install_if: -> { ENV['CAS_ENABLED'] == 'true' } +gem 'omniauth-saml', '~> 1.8', install_if: -> { ENV['SAML_ENABLED'] == 'true' } +gem 'omniauth', '~> 1.2' gem 'doorkeeper', '~> 4.2' gem 'fast_blank', '~> 1.0' diff --git a/Gemfile.lock b/Gemfile.lock index 7da9bfe394..c357bfbd1c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -201,6 +201,7 @@ GEM hamster (3.0.0) concurrent-ruby (~> 1.0) hashdiff (0.3.7) + hashie (3.5.7) highline (1.7.10) hiredis (0.6.1) hkdf (0.3.0) @@ -304,6 +305,16 @@ GEM sidekiq (>= 3.5.0) statsd-ruby (~> 1.2.0) oj (3.3.10) + omniauth (1.8.1) + hashie (>= 3.4.6, < 3.6.0) + rack (>= 1.6.2, < 3) + omniauth-cas (1.1.1) + addressable (~> 2.3) + nokogiri (~> 1.5) + omniauth (~> 1.2) + omniauth-saml (1.9.0) + omniauth (~> 1.3, >= 1.3.2) + ruby-saml (~> 1.4, >= 1.4.3) orm_adapter (0.5.0) ostatus2 (2.0.3) addressable (~> 2.5) @@ -455,6 +466,8 @@ GEM unicode-display_width (~> 1.0, >= 1.0.1) ruby-oembed (0.12.0) ruby-progressbar (1.9.0) + ruby-saml (1.6.1) + nokogiri (>= 1.5.10) rufus-scheduler (3.4.2) et-orbi (~> 1.0) safe_yaml (1.0.4) @@ -606,6 +619,9 @@ DEPENDENCIES nokogiri (~> 1.8) nsa (~> 0.2) oj (~> 3.3) + omniauth (~> 1.2) + omniauth-cas (~> 1.1) + omniauth-saml (~> 1.8) ostatus2 (~> 2.0) ox (~> 2.8) paperclip (~> 5.1) diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index 2fdb281f40..a240425cd8 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -2,4 +2,28 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController layout 'auth' + + before_action :set_user, only: [:finish_signup] + + # GET/PATCH /users/:id/finish_signup + def finish_signup + return unless request.patch? && params[:user] + if @user.update(user_params) + @user.skip_reconfirmation! + sign_in(@user, bypass: true) + redirect_to root_path, notice: I18n.t('devise.confirmations.send_instructions') + else + @show_errors = true + end + end + + private + + def set_user + @user = current_user + end + + def user_params + params.require(:user).permit(:email) + end end diff --git a/app/controllers/auth/omniauth_callbacks_controller.rb b/app/controllers/auth/omniauth_callbacks_controller.rb new file mode 100644 index 0000000000..bbf63bed30 --- /dev/null +++ b/app/controllers/auth/omniauth_callbacks_controller.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController + skip_before_action :verify_authenticity_token + + def self.provides_callback_for(provider) + provider_id = provider.to_s.chomp '_oauth2' + + define_method provider do + @user = User.find_for_oauth(request.env['omniauth.auth'], current_user) + + if @user.persisted? + sign_in_and_redirect @user, event: :authentication + set_flash_message(:notice, :success, kind: provider_id.capitalize) if is_navigational_format? + else + session["devise.#{provider}_data"] = request.env['omniauth.auth'] + redirect_to new_user_registration_url + end + end + end + + Devise.omniauth_configs.each_key do |provider| + provides_callback_for provider + end + + def after_sign_in_path_for(resource) + if resource.email_verified? + root_path + else + finish_signup_path + end + end +end diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 2bef53cff7..dec7d22843 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -568,3 +568,21 @@ code { margin-bottom: 4px; } } + +.alternative-login { + margin-top: 20px; + margin-bottom: 20px; + + h4 { + font-size: 16px; + color: $ui-base-lighter-color; + text-align: center; + margin-bottom: 20px; + border: 0; + padding: 0; + } + + .button { + display: block; + } +} diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb new file mode 100644 index 0000000000..a3d55108d2 --- /dev/null +++ b/app/models/concerns/omniauthable.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module Omniauthable + extend ActiveSupport::Concern + + TEMP_EMAIL_PREFIX = 'change@me' + TEMP_EMAIL_REGEX = /\Achange@me/ + + included do + def omniauth_providers + Devise.omniauth_configs.keys + end + + def email_verified? + email && email !~ TEMP_EMAIL_REGEX + end + end + + class_methods do + def find_for_oauth(auth, signed_in_resource = nil) + # EOLE-SSO Patch + auth.uid = (auth.uid[0][:uid] || auth.uid[0][:user]) if auth.uid.is_a? Hashie::Array + identity = Identity.find_for_oauth(auth) + + # If a signed_in_resource is provided it always overrides the existing user + # to prevent the identity being locked with accidentally created accounts. + # Note that this may leave zombie accounts (with no associated identity) which + # can be cleaned up at a later date. + user = signed_in_resource ? signed_in_resource : identity.user + user = create_for_oauth(auth) if user.nil? + + if identity.user.nil? + identity.user = user + identity.save! + end + + user + end + + def create_for_oauth(auth) + # Check if the user exists with provided email if the provider gives us a + # verified email. If no verified email was provided or the user already + # exists, we assign a temporary email and ask the user to verify it on + # the next step via Auth::ConfirmationsController.finish_signup + + user = User.new(user_params_from_auth(auth)) + user.account.avatar_remote_url = auth.info.image if auth.info.image =~ /\A#{URI.regexp(%w(http https))}\z/ + user.skip_confirmation! + user.save! + user + end + + private + + def user_params_from_auth(auth) + email_is_verified = auth.info.email && (auth.info.verified || auth.info.verified_email) + email = auth.info.email if email_is_verified && !User.exists?(email: auth.info.email) + + { + email: email ? email : "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com", + password: Devise.friendly_token[0, 20], + account_attributes: { + username: ensure_unique_username(auth.uid), + display_name: [auth.info.first_name, auth.info.last_name].join(' '), + }, + } + end + + def ensure_unique_username(starting_username) + username = starting_username + i = 0 + + while Account.exists?(username: username) + i += 1 + username = "#{starting_username}_#{i}" + end + + username + end + end +end diff --git a/app/models/identity.rb b/app/models/identity.rb new file mode 100644 index 0000000000..a5e0c09ec1 --- /dev/null +++ b/app/models/identity.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: identities +# +# id :integer not null, primary key +# user_id :integer +# provider :string default(""), not null +# uid :string default(""), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class Identity < ApplicationRecord + belongs_to :user, dependent: :destroy + validates :uid, presence: true, uniqueness: { scope: :provider } + validates :provider, presence: true + + def self.find_for_oauth(auth) + find_or_create_by(uid: auth.uid, provider: auth.provider) + end +end diff --git a/app/models/user.rb b/app/models/user.rb index fa4ebfc717..fba4784538 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -39,6 +39,7 @@ class User < ApplicationRecord include Settings::Extend + include Omniauthable ACTIVE_DURATION = 14.days @@ -52,6 +53,7 @@ class User < ApplicationRecord :confirmable devise :pam_authenticatable + devise :omniauthable belongs_to :account, inverse_of: :user belongs_to :invite, counter_cache: :uses, optional: true diff --git a/app/views/auth/confirmations/finish_signup.html.haml b/app/views/auth/confirmations/finish_signup.html.haml new file mode 100644 index 0000000000..4b5161d6b0 --- /dev/null +++ b/app/views/auth/confirmations/finish_signup.html.haml @@ -0,0 +1,14 @@ +- content_for :page_title do + = t('auth.confirm_email') + += simple_form_for(current_user, as: 'user', url: finish_signup_path, html: { role: 'form'}) do |f| + - if @show_errors && current_user.errors.any? + #error_explanation + - current_user.errors.full_messages.each do |msg| + = msg + %br + + = f.input :email + + .actions + = f.submit t('auth.confirm_email'), class: 'button' diff --git a/app/views/auth/sessions/new.html.haml b/app/views/auth/sessions/new.html.haml index 3edb0d2d4f..1c3a0b6b4a 100644 --- a/app/views/auth/sessions/new.html.haml +++ b/app/views/auth/sessions/new.html.haml @@ -14,4 +14,13 @@ .actions = f.button :button, t('auth.login'), type: :submit +- if devise_mapping.omniauthable? and resource_class.omniauth_providers.any? + .simple_form.alternative-login + %h4= t('auth.or_log_in_with') + + .actions + - resource_class.omniauth_providers.each do |provider| + = link_to omniauth_authorize_path(resource_name, provider), class: "button button-#{provider}" do + = t("auth.providers.#{provider}", default: provider.to_s.chomp("_oauth2").capitalize) + .form-footer= render 'auth/shared/links' diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 014055804e..bcd816d304 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -46,6 +46,7 @@ ignore_missing: - 'terms.body_html' - 'application_mailer.salutation' - 'errors.500' + - 'auth.providers.*' ignore_unused: - 'activemodel.errors.*' diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb new file mode 100644 index 0000000000..97f32c0a42 --- /dev/null +++ b/config/initializers/omniauth.rb @@ -0,0 +1,59 @@ +Rails.application.config.middleware.use OmniAuth::Builder do + # Vanilla omniauth stategies +end + +Devise.setup do |config| + # Devise omniauth strategies + + # CAS strategy + if ENV['CAS_ENABLED'] == 'true' + cas_options = {} + cas_options[:url] = ENV['CAS_URL'] if ENV['CAS_URL'] + cas_options[:host] = ENV['CAS_HOST'] if ENV['CAS_HOST'] + cas_options[:port] = ENV['CAS_PORT'] if ENV['CAS_PORT'] + cas_options[:ssl] = ENV['CAS_SSL'] == 'true' if ENV['CAS_SSL'] + cas_options[:validate_url] = ENV['CAS_VALIDATE_URL'] if ENV['CAS_VALIDATE_URL'] + cas_options[:callback_url] = ENV['CAS_CALLBACK_URL'] if ENV['CAS_CALLBACK_URL'] + cas_options[:logout_url] = ENV['CAS_LOGOUT_URL'] if ENV['CAS_LOGOUT_URL'] + cas_options[:login_url] = ENV['CAS_LOGIN_URL'] if ENV['CAS_LOGIN_URL'] + cas_options[:uid_field] = ENV['CAS_UID_FIELD'] || 'user' if ENV['CAS_UID_FIELD'] + cas_options[:ca_path] = ENV['CAS_CA_PATH'] if ENV['CAS_CA_PATH'] + cas_options[:disable_ssl_verification] = ENV['CAS_DISABLE_SSL_VERIFICATION'] == 'true' if ENV['CAS_DISABLE_SSL_VERIFICATION'] + cas_options[:uid_key] = ENV['CAS_UID_KEY'] || 'user' + cas_options[:name_key] = ENV['CAS_NAME_KEY'] || 'name' + cas_options[:email_key] = ENV['CAS_EMAIL_KEY'] || 'email' + cas_options[:nickname_key] = ENV['CAS_NICKNAME_KEY'] || 'nickname' + cas_options[:first_name_key] = ENV['CAS_FIRST_NAME_KEY'] || 'firstname' + cas_options[:last_name_key] = ENV['CAS_LAST_NAME_KEY'] || 'lastname' + cas_options[:location_key] = ENV['CAS_LOCATION_KEY'] || 'location' + cas_options[:image_key] = ENV['CAS_IMAGE_KEY'] || 'image' + cas_options[:phone_key] = ENV['CAS_PHONE_KEY'] || 'phone' + config.omniauth :cas, cas_options + end + + # SAML strategy + if ENV['SAML_ENABLED'] == 'true' + saml_options = {} + saml_options[:assertion_consumer_service_url] = ENV['SAML_ACS_URL'] if ENV['SAML_ACS_URL'] + saml_options[:issuer] = ENV['SAML_ISSUER'] if ENV['SAML_ISSUER'] + saml_options[:idp_sso_target_url] = ENV['SAML_IDP_SSO_TARGET_URL'] if ENV['SAML_IDP_SSO_TARGET_URL'] + saml_options[:idp_sso_target_url_runtime_params] = ENV['SAML_IDP_SSO_TARGET_PARAMS'] if ENV['SAML_IDP_SSO_TARGET_PARAMS'] # FIXME: Should be parsable Hash + saml_options[:idp_cert] = ENV['SAML_IDP_CERT'] if ENV['SAML_IDP_CERT'] + saml_options[:idp_cert_fingerprint] = ENV['SAML_IDP_CERT_FINGERPRINT'] if ENV['SAML_IDP_CERT_FINGERPRINT'] + saml_options[:idp_cert_fingerprint_validator] = ENV['SAML_IDP_CERT_FINGERPRINT_VALIDATOR'] if ENV['SAML_IDP_CERT_FINGERPRINT_VALIDATOR'] # FIXME: Should be Lambda { |fingerprint| } + saml_options[:name_identifier_format] = ENV['SAML_NAME_IDENTIFIER_FORMAT'] if ENV['SAML_NAME_IDENTIFIER_FORMAT'] + saml_options[:request_attributes] = {} + saml_options[:certificate] = ENV['SAML_CERT'] if ENV['SAML_CERT'] + saml_options[:private_key] = ENV['SAML_PRIVATE_KEY'] if ENV['SAML_PRIVATE_KEY'] + saml_options[:security] = {} + saml_options[:security][:want_assertions_signed] = ENV['SAML_SECURITY_WANT_ASSERTION_SIGNED'] == 'true' + saml_options[:security][:want_assertions_encrypted] = ENV['SAML_SECURITY_WANT_ASSERTION_ENCRYPTED'] == 'true' + saml_options[:attribute_statements] = {} + saml_options[:attribute_statements][:uid] = [ENV['SAML_ATTRIBUTES_STATEMENTS_UID']] if ENV['SAML_ATTRIBUTES_STATEMENTS_UID'] + saml_options[:attribute_statements][:email] = [ENV['SAML_ATTRIBUTES_STATEMENTS_EMAIL']] if ENV['SAML_ATTRIBUTES_STATEMENTS_EMAIL'] + saml_options[:attribute_statements][:full_name] = [ENV['SAML_ATTRIBUTES_STATEMENTS_FULL_NAME']] if ENV['SAML_ATTRIBUTES_STATEMENTS_FULL_NAME'] + saml_options[:uid_attribute] = ENV['SAML_UID_ATTRIBUTE'] if ENV['SAML_UID_ATTRIBUTE'] + config.omniauth :saml, saml_options + end + +end diff --git a/config/locales/en.yml b/config/locales/en.yml index cd6138ff23..6805a6e877 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -355,6 +355,7 @@ en: auth: agreement_html: By signing up you agree to follow the rules of the instance and our terms of service. change_password: Security + confirm_email: Confirm email delete_account: Delete account delete_account_html: If you wish to delete your account, you can proceed here. You will be asked for confirmation. didnt_get_confirmation: Didn't receive confirmation instructions? @@ -364,6 +365,10 @@ en: logout: Logout migrate_account: Move to a different account migrate_account_html: If you wish to redirect this account to a different one, you can configure it here. + or_log_in_with: Or log in with + providers: + cas: CAS + saml: SAML register: Sign up resend_confirmation: Resend confirmation instructions reset_password: Reset password diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 3ad535f285..f0fc07f7a1 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -355,6 +355,7 @@ fr: auth: agreement_html: En vous inscrivant, vous souscrivez aux règles de l’instance et à nos conditions d’utilisation. change_password: Sécurité + confirm_email: Confirmer mon adresse mail delete_account: Supprimer le compte delete_account_html: Si vous désirez supprimer votre compte, vous pouvez cliquer ici. Il vous sera demandé de confirmer cette action. didnt_get_confirmation: Vous n’avez pas reçu les consignes de confirmation ? @@ -364,6 +365,7 @@ fr: logout: Se déconnecter migrate_account: Déplacer vers un compte différent migrate_account_html: Si vous voulez rediriger ce compte vers un autre, vous pouvez le configurer ici. + or_log_in_with: Ou authentifiez-vous avec register: S’inscrire resend_confirmation: Envoyer à nouveau les consignes de confirmation reset_password: Réinitialiser le mot de passe diff --git a/config/routes.rb b/config/routes.rb index 80a2c6d13d..34f33fa958 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -24,9 +24,11 @@ Rails.application.routes.draw do devise_scope :user do get '/invite/:invite_code', to: 'auth/registrations#new', as: :public_invite + match '/auth/finish_signup' => 'auth/confirmations#finish_signup', via: [:get, :patch], as: :finish_signup end devise_for :users, path: 'auth', controllers: { + omniauth_callbacks: 'auth/omniauth_callbacks', sessions: 'auth/sessions', registrations: 'auth/registrations', passwords: 'auth/passwords', diff --git a/db/migrate/20180204034416_create_identities.rb b/db/migrate/20180204034416_create_identities.rb new file mode 100644 index 0000000000..f6f5da910b --- /dev/null +++ b/db/migrate/20180204034416_create_identities.rb @@ -0,0 +1,11 @@ +class CreateIdentities < ActiveRecord::Migration[5.0] + def change + create_table :identities do |t| + t.references :user, foreign_key: { on_delete: :cascade } + t.string :provider, null: false, default: '' + t.string :uid, null: false, default: '' + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index a411de20ff..02e84cbd19 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.define(version: 20180109143959) do +ActiveRecord::Schema.define(version: 20180204034416) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -173,6 +173,15 @@ ActiveRecord::Schema.define(version: 20180109143959) do t.index ["account_id", "target_account_id"], name: "index_follows_on_account_id_and_target_account_id", unique: true end + create_table "identities", id: :serial, force: :cascade do |t| + t.integer "user_id" + t.string "provider", default: "", null: false + t.string "uid", default: "", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_identities_on_user_id" + end + create_table "imports", force: :cascade do |t| t.integer "type", null: false t.boolean "approved", default: false, null: false @@ -526,6 +535,7 @@ ActiveRecord::Schema.define(version: 20180109143959) do add_foreign_key "follow_requests", "accounts", name: "fk_76d644b0e7", on_delete: :cascade add_foreign_key "follows", "accounts", column: "target_account_id", name: "fk_745ca29eac", on_delete: :cascade add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade + add_foreign_key "identities", "users", on_delete: :cascade add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade add_foreign_key "invites", "users", on_delete: :cascade add_foreign_key "list_accounts", "accounts", on_delete: :cascade diff --git a/spec/fabricators/identity_fabricator.rb b/spec/fabricators/identity_fabricator.rb new file mode 100644 index 0000000000..bc832df9f7 --- /dev/null +++ b/spec/fabricators/identity_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator(:identity) do + user nil + provider "MyString" + uid "MyString" +end diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb new file mode 100644 index 0000000000..53f3554102 --- /dev/null +++ b/spec/models/identity_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Identity, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From 4e4f1b0dcb386464d653fcce765ca775e566a03c Mon Sep 17 00:00:00 2001 From: "Renato \"Lond\" Cerqueira" Date: Sun, 4 Feb 2018 06:00:10 +0100 Subject: [PATCH 2/7] Add option to show only local toots in timeline preview (#6292) * Add option to show only local toots in timeline preview Right know, toots from all the known fediverse are shown in the main page of an instance. That however doesn't reflect the instance itself. With this option the admin may choose to display only local toots so that users checking the instance get a better idea of internal conversations. * Fix issues pointed by codeclimate and eslint * Add default message for community timeline * Update pl.yml --- app/controllers/about_controller.rb | 2 +- app/controllers/admin/settings_controller.rb | 2 + .../mastodon/containers/timeline_container.js | 12 ++- .../standalone/community_timeline/index.js | 74 +++++++++++++++++++ .../mastodon/locales/defaultMessages.json | 9 +++ app/models/form/admin_settings.rb | 2 + app/views/admin/settings/edit.html.haml | 3 + config/locales/en.yml | 3 + config/locales/pl.yml | 3 + config/locales/pt-BR.yml | 3 + config/settings.yml | 1 + 11 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 app/javascript/mastodon/features/standalone/community_timeline/index.js diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 47690e81eb..4ffdfb6856 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -31,7 +31,7 @@ class AboutController < ApplicationController def initial_state_params { - settings: {}, + settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token, } end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 487282dc35..a6214dc3fc 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -19,6 +19,7 @@ module Admin min_invite_role activity_api_enabled peers_api_enabled + show_known_fediverse_at_about_page ).freeze BOOLEAN_SETTINGS = %w( @@ -28,6 +29,7 @@ module Admin show_staff_badge activity_api_enabled peers_api_enabled + show_known_fediverse_at_about_page ).freeze UPLOAD_SETTINGS = %w( diff --git a/app/javascript/mastodon/containers/timeline_container.js b/app/javascript/mastodon/containers/timeline_container.js index e84c921eeb..8719bb5c9e 100644 --- a/app/javascript/mastodon/containers/timeline_container.js +++ b/app/javascript/mastodon/containers/timeline_container.js @@ -6,6 +6,7 @@ import { hydrateStore } from '../actions/store'; import { IntlProvider, addLocaleData } from 'react-intl'; import { getLocale } from '../locales'; import PublicTimeline from '../features/standalone/public_timeline'; +import CommunityTimeline from '../features/standalone/community_timeline'; import HashtagTimeline from '../features/standalone/hashtag_timeline'; import initialState from '../initial_state'; @@ -23,17 +24,24 @@ export default class TimelineContainer extends React.PureComponent { static propTypes = { locale: PropTypes.string.isRequired, hashtag: PropTypes.string, + showPublicTimeline: PropTypes.bool.isRequired, + }; + + static defaultProps = { + showPublicTimeline: initialState.settings.known_fediverse, }; render () { - const { locale, hashtag } = this.props; + const { locale, hashtag, showPublicTimeline } = this.props; let timeline; if (hashtag) { timeline = ; - } else { + } else if (showPublicTimeline) { timeline = ; + } else { + timeline = ; } return ( diff --git a/app/javascript/mastodon/features/standalone/community_timeline/index.js b/app/javascript/mastodon/features/standalone/community_timeline/index.js new file mode 100644 index 0000000000..51e50e1f50 --- /dev/null +++ b/app/javascript/mastodon/features/standalone/community_timeline/index.js @@ -0,0 +1,74 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import StatusListContainer from '../../ui/containers/status_list_container'; +import { + refreshCommunityTimeline, + expandCommunityTimeline, +} from '../../../actions/timelines'; +import Column from '../../../components/column'; +import ColumnHeader from '../../../components/column_header'; +import { defineMessages, injectIntl } from 'react-intl'; +import { connectCommunityStream } from '../../../actions/streaming'; + +const messages = defineMessages({ + title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' }, +}); + +@connect() +@injectIntl +export default class CommunityTimeline extends React.PureComponent { + + static propTypes = { + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + handleHeaderClick = () => { + this.column.scrollTop(); + } + + setRef = c => { + this.column = c; + } + + componentDidMount () { + const { dispatch } = this.props; + + dispatch(refreshCommunityTimeline()); + this.disconnect = dispatch(connectCommunityStream()); + } + + componentWillUnmount () { + if (this.disconnect) { + this.disconnect(); + this.disconnect = null; + } + } + + handleLoadMore = () => { + this.props.dispatch(expandCommunityTimeline()); + } + + render () { + const { intl } = this.props; + + return ( + + + + + + ); + } + +} diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 9a46927c17..2788a7a149 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -1230,6 +1230,15 @@ ], "path": "app/javascript/mastodon/features/public_timeline/index.json" }, + { + "descriptors": [ + { + "defaultMessage": "A look inside...", + "id": "standalone.public_title" + } + ], + "path": "app/javascript/mastodon/features/standalone/community_timeline/index.json" + }, { "descriptors": [ { diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index dd629279c0..32922e7f15 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -34,6 +34,8 @@ class Form::AdminSettings :activity_api_enabled=, :peers_api_enabled, :peers_api_enabled=, + :show_known_fediverse_at_about_page, + :show_known_fediverse_at_about_page=, to: Setting ) end diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index 4f9115ed2d..73fd5642ee 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -18,6 +18,9 @@ .fields-group = f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html') + .fields-group + = f.input :show_known_fediverse_at_about_page, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_known_fediverse_at_about_page.title'), hint: t('admin.settings.show_known_fediverse_at_about_page.desc_html') + .fields-group = f.input :show_staff_badge, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_staff_badge.title'), hint: t('admin.settings.show_staff_badge.desc_html') diff --git a/config/locales/en.yml b/config/locales/en.yml index 6805a6e877..5cd3b08cf0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -290,6 +290,9 @@ en: open: desc_html: Allow anyone to create an account title: Open registration + show_known_fediverse_at_about_page: + desc_html: When toggled, it will show toots from all the known fediverse on preview. Otherwise it will only show local toots. + title: Show known fediverse on timeline preview show_staff_badge: desc_html: Show a staff badge on a user page title: Show staff badge diff --git a/config/locales/pl.yml b/config/locales/pl.yml index a667108003..633850b28d 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -291,6 +291,9 @@ pl: open: desc_html: Pozwól każdemu na założenie konta title: Otwarta rejestracja + show_known_fediverse_at_about_page: + desc_html: Jeśli włączone, podgląd instancji będzie wyświetlał wpisy z całego Fediwersum. W innym przypadku, będą wyświetlane tylko lokalne wpisy. + title: Pokazuj wszystkie znane wpisy na podglądzie instancji show_staff_badge: desc_html: Pokazuj odznakę uprawnień na stronie profilu użytkownika title: Pokazuj odznakę administracji diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 82c96c92b1..31481ced46 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -290,6 +290,9 @@ pt-BR: open: desc_html: Permitir que qualquer um crie uma conta title: Cadastro aberto + show_known_fediverse_at_about_page: + desc_html: Quando ligado, vai mostrar toots de todo o fediverso conhecido na prévia da timeline. Senão, mostra somente toots locais. + title: Mostrar fediverso conhecido na prévia da timeline show_staff_badge: desc_html: Mostrar uma insígnia de Equipe na página de usuário title: Mostrar insígnia de equipe diff --git a/config/settings.yml b/config/settings.yml index 4a2519464b..32d0687cee 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -49,6 +49,7 @@ defaults: &defaults bootstrap_timeline_accounts: '' activity_api_enabled: true peers_api_enabled: true + show_known_fediverse_at_about_page: true development: <<: *defaults From 258dcb849f2146069a2b2914cea3a28619f55c90 Mon Sep 17 00:00:00 2001 From: Daniel King Date: Sun, 4 Feb 2018 05:03:01 +0000 Subject: [PATCH 3/7] Upgrade Vagrant box to Xenial (#6421) * upgrade vagrant box to xenial this allows the redis version to be upgraded to support the new redis features used in the activity tracker * add libpam0g package to vagrant box this is required for native extensions of gems to build after the addition of PAM support was added in #5303 --- Vagrantfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Vagrantfile b/Vagrantfile index bbe3b7f3b1..ddcdf35102 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -39,6 +39,7 @@ sudo apt-get install \ libidn11-dev \ libprotobuf-dev \ libreadline-dev \ + libpam0g-dev \ -y # Install rvm @@ -79,7 +80,7 @@ VAGRANTFILE_API_VERSION = "2" Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| - config.vm.box = "ubuntu/trusty64" + config.vm.box = "ubuntu/xenial64" config.vm.provider :virtualbox do |vb| vb.name = "mastodon" From c156a83e7d4458355e7ab60ee118ca8c09b80ece Mon Sep 17 00:00:00 2001 From: abcang Date: Sun, 4 Feb 2018 20:31:46 +0900 Subject: [PATCH 4/7] Make sure status is not nil (#6428) --- app/mailers/notification_mailer.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 9fed4a6369..b458442962 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -9,7 +9,7 @@ class NotificationMailer < ApplicationMailer @me = recipient @status = notification.target_status - return if @me.user.disabled? + return if @me.user.disabled? || @status.nil? locale_for_account(@me) do thread_by_conversation(@status.conversation) @@ -33,7 +33,7 @@ class NotificationMailer < ApplicationMailer @account = notification.from_account @status = notification.target_status - return if @me.user.disabled? + return if @me.user.disabled? || @status.nil? locale_for_account(@me) do thread_by_conversation(@status.conversation) @@ -46,7 +46,7 @@ class NotificationMailer < ApplicationMailer @account = notification.from_account @status = notification.target_status - return if @me.user.disabled? + return if @me.user.disabled? || @status.nil? locale_for_account(@me) do thread_by_conversation(@status.conversation) From 3f35d4322266ee6f1bfab73a1161af2b0848573a Mon Sep 17 00:00:00 2001 From: abcang Date: Sun, 4 Feb 2018 20:32:10 +0900 Subject: [PATCH 5/7] Exclude nil from relationships array (#6427) --- app/controllers/api/v1/accounts/relationships_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb index 91a942d753..6cc3da4985 100644 --- a/app/controllers/api/v1/accounts/relationships_controller.rb +++ b/app/controllers/api/v1/accounts/relationships_controller.rb @@ -10,7 +10,7 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController accounts = Account.where(id: account_ids).select('id') # .where doesn't guarantee that our results are in the same order # we requested them, so return the "right" order to the requestor. - @accounts = accounts.index_by(&:id).values_at(*account_ids) + @accounts = accounts.index_by(&:id).values_at(*account_ids).compact render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships end From 9b6223f5e26ed53f285a95921e9c660e831a7f6d Mon Sep 17 00:00:00 2001 From: abcang Date: Sun, 4 Feb 2018 20:32:41 +0900 Subject: [PATCH 6/7] Validation of count works even when text of status is nil (#6429) --- app/validators/status_length_validator.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/validators/status_length_validator.rb b/app/validators/status_length_validator.rb index 77be3f1f54..ed5563f64f 100644 --- a/app/validators/status_length_validator.rb +++ b/app/validators/status_length_validator.rb @@ -23,6 +23,8 @@ class StatusLengthValidator < ActiveModel::Validator end def countable_text(status) + return '' if status.text.nil? + status.text.dup.tap do |new_text| new_text.gsub!(FetchLinkCardService::URL_PATTERN, 'x' * 23) new_text.gsub!(Account::MENTION_RE, '@\2') From 38e0133e1b01c21a710111097102a6eb205b9b9b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 4 Feb 2018 15:05:53 +0100 Subject: [PATCH 7/7] Make PAM gem optional, allow configuration over environment (#6415) --- .env.production.sample | 9 +++++++++ Gemfile | 2 +- app/models/user.rb | 2 +- config/initializers/devise.rb | 27 +++++++++------------------ 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.env.production.sample b/.env.production.sample index 777336de1d..a4b689a31f 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -136,6 +136,15 @@ STREAMING_CLUSTER_NUM=1 # UID=1000 # GID=1000 +# PAM authentication (optional) +# PAM_ENABLED=true +# Suffix for email address generation (nil by default) +# PAM_DEFAULT_SUFFIX=pam +# Name of the pam service (pam "auth" section is evaluated) +# PAM_DEFAULT_SERVICE=rpam +# Name of the pam service used for checking if an user can register (pam "account" section is evaluated) +# PAM_CONTROLLED_SERVICE=rpam + # Optional CAS authentication (cf. omniauth-cas) : # CAS_ENABLED=true # CAS_URL=https://sso.myserver.com/ diff --git a/Gemfile b/Gemfile index 5b6ae707db..3b39f39462 100644 --- a/Gemfile +++ b/Gemfile @@ -31,7 +31,7 @@ gem 'cld3', '~> 3.2.0' gem 'devise', '~> 4.4' gem 'devise-two-factor', '~> 3.0' -gem 'devise_pam_authenticatable2', '~> 8.0' +gem 'devise_pam_authenticatable2', '~> 8.0', install_if: -> { ENV['PAM_ENABLED'] == 'true' } gem 'omniauth-cas', '~> 1.1', install_if: -> { ENV['CAS_ENABLED'] == 'true' } gem 'omniauth-saml', '~> 1.8', install_if: -> { ENV['SAML_ENABLED'] == 'true' } gem 'omniauth', '~> 1.2' diff --git a/app/models/user.rb b/app/models/user.rb index fba4784538..feaf8b26c7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -52,7 +52,7 @@ class User < ApplicationRecord devise :registerable, :recoverable, :rememberable, :trackable, :validatable, :confirmable - devise :pam_authenticatable + devise :pam_authenticatable if Devise.pam_authentication devise :omniauthable belongs_to :account, inverse_of: :user diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index f2f7f1ba33..ba7ad9e6c8 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -315,22 +315,13 @@ Devise.setup do |config| # so you need to do it manually. For the users scope, it would be: # config.omniauth_path_prefix = '/my_engine/users/auth' - # PAM: only look for email field - config.usernamefield = nil - config.emailfield = "email" - - # authentication with pam possible - # if not enabled, all pam settings are ignored - #config.pam_authentication = true - # check if email is actually a username - config.check_at_sign = true - # suffix for email address generation (warning: without pam must provide email in the pam environment) - config.pam_default_suffix = "pam" - # name of the pam service - # pam "auth" section is evaluated - config.pam_default_service = "rpam" - # name of the pam service used for checking if an user can register - # pam "account" section is evaluated - # nil for allowing registration of pam names (not recommended) - config.pam_controlled_service = "rpam" + if ENV['PAM_ENABLED'] == 'true' + config.pam_authentication = true + config.usernamefield = nil + config.emailfield = 'email' + config.check_at_sign = true + config.pam_default_suffix = ENV.fetch('PAM_DEFAULT_SUFFIX') { nil } + config.pam_default_service = ENV.fetch('PAM_DEFAULT_SERVICE') { 'rpam' } + config.pam_controlled_service = ENV.fetch('PAM_CONTROLLED_SERVICE') { 'rpam' } + end end