--- /dev/null
+# frozen_string_literal: true
+
+module Admin
+ class IpBlocksController < BaseController
+ def index
+ authorize :ip_block, :index?
+
+ @ip_blocks = IpBlock.page(params[:page])
+ @form = Form::IpBlockBatch.new
+ end
+
+ def new
+ authorize :ip_block, :create?
+
+ @ip_block = IpBlock.new(ip: '', severity: :no_access, expires_in: 1.year)
+ end
+
+ def create
+ authorize :ip_block, :create?
+
+ @ip_block = IpBlock.new(resource_params)
+
+ if @ip_block.save
+ log_action :create, @ip_block
+ redirect_to admin_ip_blocks_path, notice: I18n.t('admin.ip_blocks.created_msg')
+ else
+ render :new
+ end
+ end
+
+ def batch
+ @form = Form::IpBlockBatch.new(form_ip_block_batch_params.merge(current_account: current_account, action: action_from_button))
+ @form.save
+ rescue ActionController::ParameterMissing
+ flash[:alert] = I18n.t('admin.ip_blocks.no_ip_block_selected')
+ rescue Mastodon::NotPermittedError
+ flash[:alert] = I18n.t('admin.custom_emojis.not_permitted')
+ ensure
+ redirect_to admin_ip_blocks_path
+ end
+
+ private
+
+ def resource_params
+ params.require(:ip_block).permit(:ip, :severity, :comment, :expires_in)
+ end
+
+ def action_from_button
+ 'delete' if params[:delete]
+ end
+
+ def form_ip_block_batch_params
+ params.require(:form_ip_block_batch).permit(ip_block_ids: [])
+ end
+ end
+end
end
def create
- token = AppSignUpService.new.call(doorkeeper_token.application, account_params)
+ token = AppSignUpService.new.call(doorkeeper_token.application, request.remote_ip, account_params)
response = Doorkeeper::OAuth::TokenResponse.new(token)
headers.merge!(response.headers)
def build_resource(hash = nil)
super(hash)
- resource.locale = I18n.locale
- resource.invite_code = params[:invite_code] if resource.invite_code.blank?
- resource.current_sign_in_ip = request.remote_ip
+ resource.locale = I18n.locale
+ resource.invite_code = params[:invite_code] if resource.invite_code.blank?
+ resource.sign_up_ip = request.remote_ip
resource.build_account if resource.account.nil?
end
link_to record.target_account.acct, admin_account_path(record.target_account_id)
when 'Announcement'
link_to truncate(record.text), edit_admin_announcement_path(record.id)
+ when 'IpBlock'
+ "#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})"
end
end
end
when 'Announcement'
truncate(attributes['text'].is_a?(Array) ? attributes['text'].last : attributes['text'])
+ when 'IpBlock'
+ "#{attributes['ip']}/#{attributes['ip'].prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{attributes['severity']}")})"
end
end
end
--- /dev/null
+# frozen_string_literal: true
+
+class FastIpMap
+ MAX_IPV4_PREFIX = 32
+ MAX_IPV6_PREFIX = 128
+
+ # @param [Enumerable<IPAddr>] addresses
+ def initialize(addresses)
+ @fast_lookup = {}
+ @ranges = []
+
+ # Hash look-up is faster but only works for exact matches, so we split
+ # exact addresses from non-exact ones
+ addresses.each do |address|
+ if (address.ipv4? && address.prefix == MAX_IPV4_PREFIX) || (address.ipv6? && address.prefix == MAX_IPV6_PREFIX)
+ @fast_lookup[address.to_s] = true
+ else
+ @ranges << address
+ end
+ end
+
+ # We're more likely to hit wider-reaching ranges when checking for
+ # inclusion, so make sure they're sorted first
+ @ranges.sort_by!(&:prefix)
+ end
+
+ # @param [IPAddr] address
+ # @return [Boolean]
+ def include?(address)
+ @fast_lookup[address.to_s] || @ranges.any? { |cidr| cidr.include?(address) }
+ end
+end
included do
scope :expired, -> { where.not(expires_at: nil).where('expires_at < ?', Time.now.utc) }
- attr_reader :expires_in
+ def expires_in
+ return @expires_in if defined?(@expires_in)
+
+ if expires_at.nil?
+ nil
+ else
+ (expires_at - created_at).to_i
+ end
+ end
def expires_in=(interval)
self.expires_at = interval.to_i.seconds.from_now if interval.present?
--- /dev/null
+# frozen_string_literal: true
+
+class Form::IpBlockBatch
+ include ActiveModel::Model
+ include Authorization
+ include AccountableConcern
+
+ attr_accessor :ip_block_ids, :action, :current_account
+
+ def save
+ case action
+ when 'delete'
+ delete!
+ end
+ end
+
+ private
+
+ def ip_blocks
+ @ip_blocks ||= IpBlock.where(id: ip_block_ids)
+ end
+
+ def delete!
+ ip_blocks.each { |ip_block| authorize(ip_block, :destroy?) }
+
+ ip_blocks.each do |ip_block|
+ ip_block.destroy
+ log_action :destroy, ip_block
+ end
+ end
+end
--- /dev/null
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: ip_blocks
+#
+# id :bigint(8) not null, primary key
+# created_at :datetime not null
+# updated_at :datetime not null
+# expires_at :datetime
+# ip :inet default(#<IPAddr: IPv4:0.0.0.0/255.255.255.255>), not null
+# severity :integer default(NULL), not null
+# comment :text default(""), not null
+#
+
+class IpBlock < ApplicationRecord
+ CACHE_KEY = 'blocked_ips'
+
+ include Expireable
+
+ enum severity: {
+ sign_up_requires_approval: 5000,
+ no_access: 9999,
+ }
+
+ validates :ip, :severity, presence: true
+
+ after_commit :reset_cache
+
+ class << self
+ def blocked?(remote_ip)
+ blocked_ips_map = Rails.cache.fetch(CACHE_KEY) { FastIpMap.new(IpBlock.where(severity: :no_access).pluck(:ip)) }
+ blocked_ips_map.include?(remote_ip)
+ end
+ end
+
+ private
+
+ def reset_cache
+ Rails.cache.delete(CACHE_KEY)
+ end
+end
# sign_in_token :string
# sign_in_token_sent_at :datetime
# webauthn_id :string
+# sign_up_ip :inet
#
class User < ApplicationRecord
scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) }
scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended_at: nil }) }
scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) }
- scope :matches_ip, ->(value) { left_joins(:session_activations).where('users.current_sign_in_ip <<= ?', value).or(left_joins(:session_activations).where('users.last_sign_in_ip <<= ?', value)).or(left_joins(:session_activations).where('session_activations.ip <<= ?', value)) }
+ scope :matches_ip, ->(value) { left_joins(:session_activations).where('users.current_sign_in_ip <<= ?', value).or(left_joins(:session_activations).where('users.sign_up_ip <<= ?', value)).or(left_joins(:session_activations).where('users.last_sign_in_ip <<= ?', value)).or(left_joins(:session_activations).where('session_activations.ip <<= ?', value)) }
scope :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) }
before_validation :sanitize_languages
arr << [current_sign_in_at, current_sign_in_ip] if current_sign_in_ip.present?
arr << [last_sign_in_at, last_sign_in_ip] if last_sign_in_ip.present?
+ arr << [created_at, sign_up_ip] if sign_up_ip.present?
arr.sort_by { |pair| pair.first || Time.now.utc }.uniq(&:last).reverse!
end
end
def set_approved
- self.approved = open_registrations? || valid_invitation? || external?
+ self.approved = begin
+ if sign_up_from_ip_requires_approval?
+ false
+ else
+ open_registrations? || valid_invitation? || external?
+ end
+ end
+ end
+
+ def sign_up_from_ip_requires_approval?
+ !sign_up_ip.nil? && IpBlock.where(severity: :sign_up_requires_approval).where('ip >>= ?', sign_up_ip.to_s).exists?
end
def open_registrations?
--- /dev/null
+# frozen_string_literal: true
+
+class IpBlockPolicy < ApplicationPolicy
+ def index?
+ admin?
+ end
+
+ def create?
+ admin?
+ end
+
+ def destroy?
+ admin?
+ end
+end
# frozen_string_literal: true
class AppSignUpService < BaseService
- def call(app, params)
+ def call(app, remote_ip, params)
return unless allowed_registrations?
user_params = params.slice(:email, :password, :agreement, :locale)
account_params = params.slice(:username)
invite_request_params = { text: params[:reason] }
- user = User.create!(user_params.merge(created_by_application: app, password_confirmation: user_params[:password], account_attributes: account_params, invite_request_attributes: invite_request_params))
+ user = User.create!(user_params.merge(created_by_application: app, sign_up_ip: remote_ip, password_confirmation: user_params[:password], account_attributes: account_params, invite_request_attributes: invite_request_params))
Doorkeeper::AccessToken.create!(application: app,
resource_owner_id: user.id,
--- /dev/null
+.batch-table__row
+ %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
+ = f.check_box :ip_block_ids, { multiple: true, include_hidden: false }, ip_block.id
+ .batch-table__row__content
+ .batch-table__row__content__text
+ %samp= "#{ip_block.ip}/#{ip_block.ip.prefix}"
+ - if ip_block.comment.present?
+ •
+ = ip_block.comment
+ %br/
+ = t("simple_form.labels.ip_block.severities.#{ip_block.severity}")
--- /dev/null
+- content_for :page_title do
+ = t('admin.ip_blocks.title')
+
+- content_for :header_tags do
+ = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous'
+
+- if can?(:create, :ip_block)
+ - content_for :heading_actions do
+ = link_to t('admin.ip_blocks.add_new'), new_admin_ip_block_path, class: 'button'
+
+= form_for(@form, url: batch_admin_ip_blocks_path) do |f|
+ = hidden_field_tag :page, params[:page] || 1
+
+ .batch-table
+ .batch-table__toolbar
+ %label.batch-table__toolbar__select.batch-checkbox-all
+ = check_box_tag :batch_checkbox_all, nil, false
+ .batch-table__toolbar__actions
+ - if can?(:destroy, :ip_block)
+ = f.button safe_join([fa_icon('times'), t('admin.ip_blocks.delete')]), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
+ .batch-table__body
+ - if @ip_blocks.empty?
+ = nothing_here 'nothing-here--under-tabs'
+ - else
+ = render partial: 'ip_block', collection: @ip_blocks, locals: { f: f }
+
+= paginate @ip_blocks
+
--- /dev/null
+- content_for :page_title do
+ = t('.title')
+
+= simple_form_for @ip_block, url: admin_ip_blocks_path do |f|
+ = render 'shared/error_messages', object: @ip_block
+
+ .fields-group
+ = f.input :ip, as: :string, wrapper: :with_block_label, input_html: { placeholder: '192.0.2.0/24' }
+
+ .fields-group
+ = f.input :expires_in, wrapper: :with_block_label, collection: [1.day, 2.weeks, 1.month, 6.months, 1.year, 3.years].map(&:to_i), label_method: lambda { |i| I18n.t("admin.ip_blocks.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt')
+
+ .fields-group
+ = f.input :severity, as: :radio_buttons, collection: IpBlock.severities.keys, include_blank: false, wrapper: :with_block_label, label_method: lambda { |severity| safe_join([I18n.t("simple_form.labels.ip_block.severities.#{severity}"), content_tag(:span, I18n.t("simple_form.hints.ip_block.severities.#{severity}"), class: 'hint')]) }
+
+ .fields-group
+ = f.input :comment, as: :string, wrapper: :with_block_label
+
+ .actions
+ = f.button :button, t('admin.ip_blocks.add_new'), type: :submit
%strong= account.user_email
= "(@#{account.username})"
%br/
- = account.user_current_sign_in_ip
+ %samp= account.user_current_sign_in_ip
•
= t 'admin.accounts.time_in_queue', time: time_ago_in_words(account.user&.created_at)
class Scheduler::IpCleanupScheduler
include Sidekiq::Worker
- RETENTION_PERIOD = 1.year
+ IP_RETENTION_PERIOD = 1.year.freeze
sidekiq_options lock: :until_executed, retry: 0
def perform
- time_ago = RETENTION_PERIOD.ago
- SessionActivation.where('updated_at < ?', time_ago).in_batches.destroy_all
- User.where('last_sign_in_at < ?', time_ago).where.not(last_sign_in_ip: nil).in_batches.update_all(last_sign_in_ip: nil)
+ clean_ip_columns!
+ clean_expired_ip_blocks!
+ end
+
+ private
+
+ def clean_ip_columns!
+ SessionActivation.where('updated_at < ?', IP_RETENTION_PERIOD.ago).in_batches.destroy_all
+ User.where('current_sign_in_at < ?', IP_RETENTION_PERIOD.ago).in_batches.update_all(last_sign_in_ip: nil, current_sign_in_ip: nil, sign_up_ip: nil)
+ end
+
+ def clean_expired_ip_blocks!
+ IpBlock.expired.in_batches.destroy_all
end
end
req.remote_ip == '127.0.0.1' || req.remote_ip == '::1'
end
+ Rack::Attack.blocklist('deny from blocklist') do |req|
+ IpBlock.blocked?(req.remote_ip)
+ end
+
throttle('throttle_authenticated_api', limit: 300, period: 5.minutes) do |req|
req.authenticated_user_id if req.api_request?
end
create_domain_allow: Create Domain Allow
create_domain_block: Create Domain Block
create_email_domain_block: Create E-mail Domain Block
+ create_ip_block: Create IP rule
demote_user: Demote User
destroy_announcement: Delete Announcement
destroy_custom_emoji: Delete Custom Emoji
destroy_domain_allow: Delete Domain Allow
destroy_domain_block: Delete Domain Block
destroy_email_domain_block: Delete e-mail domain block
+ destroy_ip_block: Delete IP rule
destroy_status: Delete Status
disable_2fa_user: Disable 2FA
disable_custom_emoji: Disable Custom Emoji
create_domain_allow: "%{name} allowed federation with domain %{target}"
create_domain_block: "%{name} blocked domain %{target}"
create_email_domain_block: "%{name} blocked e-mail domain %{target}"
+ create_ip_block: "%{name} created rule for IP %{target}"
demote_user: "%{name} demoted user %{target}"
destroy_announcement: "%{name} deleted announcement %{target}"
destroy_custom_emoji: "%{name} destroyed emoji %{target}"
destroy_domain_allow: "%{name} disallowed federation with domain %{target}"
destroy_domain_block: "%{name} unblocked domain %{target}"
destroy_email_domain_block: "%{name} unblocked e-mail domain %{target}"
+ destroy_ip_block: "%{name} deleted rule for IP %{target}"
destroy_status: "%{name} removed status by %{target}"
disable_2fa_user: "%{name} disabled two factor requirement for user %{target}"
disable_custom_emoji: "%{name} disabled emoji %{target}"
expired: Expired
title: Filter
title: Invites
+ ip_blocks:
+ add_new: Create rule
+ created_msg: Successfully added new IP rule
+ delete: Delete
+ expires_in:
+ '1209600': 2 weeks
+ '15778476': 6 months
+ '2629746': 1 month
+ '31556952': 1 year
+ '86400': 1 day
+ '94670856': 3 years
+ new:
+ title: Create new IP rule
+ no_ip_block_selected: No IP rules were changed as none were selected
+ title: IP rules
pending_accounts:
title: Pending accounts (%{count})
relationships:
data: CSV file exported from another Mastodon server
invite_request:
text: This will help us review your application
+ ip_block:
+ comment: Optional. Remember why you added this rule.
+ expires_in: IP addresses are a finite resource, they are sometimes shared and often change hands. For this reason, indefinite IP blocks are not recommended.
+ ip: Enter an IPv4 or IPv6 address. You can block entire ranges using the CIDR syntax. Be careful not to lock yourself out!
+ severities:
+ no_access: Block access to all resources
+ sign_up_requires_approval: New sign-ups will require your approval
+ severity: Choose what will happen with requests from this IP
sessions:
otp: 'Enter the two-factor code generated by your phone app or use one of your recovery codes:'
webauthn: If it's an USB key be sure to insert it and, if necessary, tap it.
comment: Comment
invite_request:
text: Why do you want to join?
+ ip_block:
+ comment: Comment
+ ip: IP
+ severities:
+ no_access: Block access
+ sign_up_requires_approval: Limit sign-ups
+ severity: Rule
notification_emails:
digest: Send digest e-mails
favourite: Someone favourited your status
s.item :tags, safe_join([fa_icon('hashtag fw'), t('admin.tags.title')]), admin_tags_path, highlights_on: %r{/admin/tags}
s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url(limited: whitelist_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.admin? }
s.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_url, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.admin? }
+ s.item :ip_blocks, safe_join([fa_icon('ban fw'), t('admin.ip_blocks.title')]), admin_ip_blocks_url, highlights_on: %r{/admin/ip_blocks}, if: -> { current_user.admin? }
end
n.item :admin, safe_join([fa_icon('cogs fw'), t('admin.title')]), admin_dashboard_url, if: proc { current_user.staff? } do |s|
end
end
+ resources :ip_blocks, only: [:index, :new, :create] do
+ collection do
+ post :batch
+ end
+ end
+
resources :account_moderation_notes, only: [:create, :destroy]
resources :tags, only: [:index, :show, :update] do
--- /dev/null
+class CreateIpBlocks < ActiveRecord::Migration[5.2]
+ def change
+ create_table :ip_blocks do |t|
+ t.inet :ip, null: false, default: '0.0.0.0'
+ t.integer :severity, null: false, default: 0
+ t.datetime :expires_at
+ t.text :comment, null: false, default: ''
+
+ t.timestamps
+ end
+ end
+end
--- /dev/null
+class AddSignUpIpToUsers < ActiveRecord::Migration[5.2]
+ def change
+ add_column :users, :sign_up_ip, :inet
+ end
+end
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2020_09_17_222734) do
+ActiveRecord::Schema.define(version: 2020_10_08_220312) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
t.index ["user_id"], name: "index_invites_on_user_id"
end
+ create_table "ip_blocks", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.datetime "expires_at"
+ t.inet "ip", default: "0.0.0.0", null: false
+ t.integer "severity", default: 0, null: false
+ t.text "comment", default: "", null: false
+ end
+
create_table "list_accounts", force: :cascade do |t|
t.bigint "list_id", null: false
t.bigint "account_id", null: false
t.string "sign_in_token"
t.datetime "sign_in_token_sent_at"
t.string "webauthn_id"
+ t.inet "sign_up_ip"
t.index ["account_id"], name: "index_users_on_account_id"
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id"
require_relative 'mastodon/cache_cli'
require_relative 'mastodon/upgrade_cli'
require_relative 'mastodon/email_domain_blocks_cli'
+require_relative 'mastodon/ip_blocks_cli'
require_relative 'mastodon/version'
module Mastodon
desc 'email_domain_blocks SUBCOMMAND ...ARGS', 'Manage e-mail domain blocks'
subcommand 'email_domain_blocks', Mastodon::EmailDomainBlocksCLI
+ desc 'ip_blocks SUBCOMMAND ...ARGS', 'Manage IP blocks'
+ subcommand 'ip_blocks', Mastodon::IpBlocksCLI
+
option :dry_run, type: :boolean
desc 'self-destruct', 'Erase the server from the federation'
long_desc <<~LONG_DESC
--- /dev/null
+# frozen_string_literal: true
+
+require 'rubygems/package'
+require_relative '../../config/boot'
+require_relative '../../config/environment'
+require_relative 'cli_helper'
+
+module Mastodon
+ class IpBlocksCLI < Thor
+ def self.exit_on_failure?
+ true
+ end
+
+ option :severity, required: true, enum: %w(no_access sign_up_requires_approval), desc: 'Severity of the block'
+ option :comment, aliases: [:c], desc: 'Optional comment'
+ option :duration, aliases: [:d], type: :numeric, desc: 'Duration of the block in seconds'
+ option :force, type: :boolean, aliases: [:f], desc: 'Overwrite existing blocks'
+ desc 'add IP...', 'Add one or more IP blocks'
+ long_desc <<-LONG_DESC
+ Add one or more IP blocks. You can use CIDR syntax to
+ block IP ranges. You must specify --severity of the block. All
+ options will be copied for each IP block you create in one command.
+
+ You can add a --comment. If an IP block already exists for one of
+ the provided IPs, it will be skipped unless you use the --force
+ option to overwrite it.
+ LONG_DESC
+ def add(*addresses)
+ if addresses.empty?
+ say('No IP(s) given', :red)
+ exit(1)
+ end
+
+ skipped = 0
+ processed = 0
+ failed = 0
+
+ addresses.each do |address|
+ ip_block = IpBlock.find_by(ip: address)
+
+ if ip_block.present? && !options[:force]
+ say("#{address} is already blocked", :yellow)
+ skipped += 1
+ next
+ end
+
+ ip_block ||= IpBlock.new(ip: address)
+
+ ip_block.severity = options[:severity]
+ ip_block.comment = options[:comment]
+ ip_block.expires_in = options[:duration]
+
+ if ip_block.save
+ processed += 1
+ else
+ say("#{address} could not be saved", :red)
+ failed += 1
+ end
+ end
+
+ say("Added #{processed}, skipped #{skipped}, failed #{failed}", color(processed, failed))
+ end
+
+ option :force, type: :boolean, aliases: [:f], desc: 'Remove blocks for ranges that cover given IP(s)'
+ desc 'remove IP...', 'Remove one or more IP blocks'
+ long_desc <<-LONG_DESC
+ Remove one or more IP blocks. Normally, only exact matches are removed. If
+ you want to ensure that all of the given IP addresses are unblocked, you
+ can use --force which will also remove any blocks for IP ranges that would
+ cover the given IP(s).
+ LONG_DESC
+ def remove(*addresses)
+ if addresses.empty?
+ say('No IP(s) given', :red)
+ exit(1)
+ end
+
+ processed = 0
+ skipped = 0
+
+ addresses.each do |address|
+ ip_blocks = begin
+ if options[:force]
+ IpBlock.where('ip >>= ?', address)
+ else
+ IpBlock.where('ip <<= ?', address)
+ end
+ end
+
+ if ip_blocks.empty?
+ say("#{address} is not yet blocked", :yellow)
+ skipped += 1
+ next
+ end
+
+ ip_blocks.in_batches.destroy_all
+ processed += 1
+ end
+
+ say("Removed #{processed}, skipped #{skipped}", color(processed, 0))
+ end
+
+ option :format, aliases: [:f], enum: %w(plain nginx), desc: 'Format of the output'
+ desc 'export', 'Export blocked IPs'
+ long_desc <<-LONG_DESC
+ Export blocked IPs. Different formats are supported for usage with other
+ tools. Only blocks with no_access severity are returned.
+ LONG_DESC
+ def export
+ IpBlock.where(severity: :no_access).find_each do |ip_block|
+ case options[:format]
+ when 'nginx'
+ puts "deny #{ip_block.ip}/#{ip_block.ip.prefix};"
+ else
+ puts "#{ip_block.ip}/#{ip_block.ip.prefix}"
+ end
+ end
+ end
+
+ private
+
+ def color(processed, failed)
+ if !processed.zero? && failed.zero?
+ :green
+ elsif failed.zero?
+ :yellow
+ else
+ :red
+ end
+ end
+ end
+end
--- /dev/null
+Fabricator(:ip_block) do
+ ip ""
+ severity ""
+ expires_at "2020-10-08 22:20:37"
+ comment "MyText"
+end
\ No newline at end of file
--- /dev/null
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe FastIpMap do
+ describe '#include?' do
+ subject { described_class.new([IPAddr.new('20.4.0.0/16'), IPAddr.new('145.22.30.0/24'), IPAddr.new('189.45.86.3')])}
+
+ it 'returns true for an exact match' do
+ expect(subject.include?(IPAddr.new('189.45.86.3'))).to be true
+ end
+
+ it 'returns true for a range match' do
+ expect(subject.include?(IPAddr.new('20.4.45.7'))).to be true
+ end
+
+ it 'returns false for no match' do
+ expect(subject.include?(IPAddr.new('145.22.40.64'))).to be false
+ end
+ end
+end
--- /dev/null
+require 'rails_helper'
+
+RSpec.describe IpBlock, type: :model do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
RSpec.describe AppSignUpService, type: :service do
let(:app) { Fabricate(:application, scopes: 'read write') }
let(:good_params) { { username: 'alice', password: '12345678', email: 'good@email.com', agreement: true } }
+ let(:remote_ip) { IPAddr.new('198.0.2.1') }
subject { described_class.new }
it 'returns nil when registrations are closed' do
tmp = Setting.registrations_mode
Setting.registrations_mode = 'none'
- expect(subject.call(app, good_params)).to be_nil
+ expect(subject.call(app, remote_ip, good_params)).to be_nil
Setting.registrations_mode = tmp
end
it 'raises an error when params are missing' do
- expect { subject.call(app, {}) }.to raise_error ActiveRecord::RecordInvalid
+ expect { subject.call(app, remote_ip, {}) }.to raise_error ActiveRecord::RecordInvalid
end
it 'creates an unconfirmed user with access token' do
- access_token = subject.call(app, good_params)
+ access_token = subject.call(app, remote_ip, good_params)
expect(access_token).to_not be_nil
user = User.find_by(id: access_token.resource_owner_id)
expect(user).to_not be_nil
end
it 'creates access token with the app\'s scopes' do
- access_token = subject.call(app, good_params)
+ access_token = subject.call(app, remote_ip, good_params)
expect(access_token).to_not be_nil
expect(access_token.scopes.to_s).to eq 'read write'
end
it 'creates an account' do
- access_token = subject.call(app, good_params)
+ access_token = subject.call(app, remote_ip, good_params)
expect(access_token).to_not be_nil
user = User.find_by(id: access_token.resource_owner_id)
expect(user).to_not be_nil
end
it 'creates an account with invite request text' do
- access_token = subject.call(app, good_params.merge(reason: 'Foo bar'))
+ access_token = subject.call(app, remote_ip, good_params.merge(reason: 'Foo bar'))
expect(access_token).to_not be_nil
user = User.find_by(id: access_token.resource_owner_id)
expect(user).to_not be_nil