mirror of
https://github.com/discourse/discourse.git
synced 2025-05-22 21:21:19 +08:00
Screened ip address can be edited, deleted, and changed to allow or block.
This commit is contained in:
@ -9,6 +9,7 @@
|
|||||||
Discourse.AdminLogsScreenedIpAddressesController = Ember.ArrayController.extend(Discourse.Presence, {
|
Discourse.AdminLogsScreenedIpAddressesController = Ember.ArrayController.extend(Discourse.Presence, {
|
||||||
loading: false,
|
loading: false,
|
||||||
content: [],
|
content: [],
|
||||||
|
itemController: 'adminLogsScreenedIpAddress',
|
||||||
|
|
||||||
show: function() {
|
show: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
@ -19,3 +20,72 @@ Discourse.AdminLogsScreenedIpAddressesController = Ember.ArrayController.extend(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Discourse.AdminLogsScreenedIpAddressController = Ember.ObjectController.extend({
|
||||||
|
editing: false,
|
||||||
|
savedIpAddress: null,
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
allow: function(record) {
|
||||||
|
record.set('action', 'do_nothing');
|
||||||
|
this.send('save', record);
|
||||||
|
},
|
||||||
|
|
||||||
|
block: function(record) {
|
||||||
|
record.set('action', 'block');
|
||||||
|
this.send('save', record);
|
||||||
|
},
|
||||||
|
|
||||||
|
edit: function() {
|
||||||
|
if (!this.get('editing')) {
|
||||||
|
this.savedIpAddress = this.get('ip_address');
|
||||||
|
}
|
||||||
|
this.set('editing', true);
|
||||||
|
},
|
||||||
|
|
||||||
|
cancel: function() {
|
||||||
|
if (this.get('savedIpAddress') && this.get('editing')) {
|
||||||
|
this.set('ip_address', this.get('savedIpAddress'));
|
||||||
|
}
|
||||||
|
this.set('editing', false);
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function(record) {
|
||||||
|
var self = this;
|
||||||
|
var wasEditing = this.get('editing');
|
||||||
|
this.set('editing', false);
|
||||||
|
record.save().then(function(saved){
|
||||||
|
if (saved.success) {
|
||||||
|
self.set('savedIpAddress', null);
|
||||||
|
} else {
|
||||||
|
bootbox.alert(saved.errors);
|
||||||
|
if (wasEditing) self.set('editing', true);
|
||||||
|
}
|
||||||
|
}, function(e){
|
||||||
|
if (e.responseJSON && e.responseJSON.errors) {
|
||||||
|
bootbox.alert(I18n.t("generic_error_with_reason", {error: e.responseJSON.errors.join('. ')}));
|
||||||
|
} else {
|
||||||
|
bootbox.alert(I18n.t("generic_error"));
|
||||||
|
}
|
||||||
|
if (wasEditing) self.set('editing', true);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy: function(record) {
|
||||||
|
var self = this;
|
||||||
|
return bootbox.confirm(I18n.t("admin.logs.screened_ips.delete_confirm", {ip_address: record.get('ip_address')}), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
|
||||||
|
if (result) {
|
||||||
|
record.destroy().then(function(deleted) {
|
||||||
|
if (deleted) {
|
||||||
|
self.get("parentController.content").removeObject(record);
|
||||||
|
} else {
|
||||||
|
bootbox.alert(I18n.t("generic_error"));
|
||||||
|
}
|
||||||
|
}, function(e){
|
||||||
|
bootbox.alert(I18n.t("generic_error_with_reason", {error: "http: " + e.status + " - " + e.body}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@ -8,14 +8,40 @@
|
|||||||
@module Discourse
|
@module Discourse
|
||||||
**/
|
**/
|
||||||
Discourse.ScreenedIpAddress = Discourse.Model.extend({
|
Discourse.ScreenedIpAddress = Discourse.Model.extend({
|
||||||
// TODO: this is repeated in all 3 screened models. move it.
|
|
||||||
actionName: function() {
|
actionName: function() {
|
||||||
if (this.get('action') === 'do_nothing') {
|
return I18n.t("admin.logs.screened_ips.actions." + this.get('action'));
|
||||||
return I18n.t("admin.logs.screened_ips.allow");
|
}.property('action'),
|
||||||
|
|
||||||
|
isBlocked: function() {
|
||||||
|
return (this.get('action') === 'block');
|
||||||
|
}.property('action'),
|
||||||
|
|
||||||
|
actionIcon: function() {
|
||||||
|
if (this.get('action') === 'block') {
|
||||||
|
return this.get('blockIcon');
|
||||||
} else {
|
} else {
|
||||||
return I18n.t("admin.logs.screened_actions." + this.get('action'));
|
return this.get('doNothingIcon');
|
||||||
}
|
}
|
||||||
}.property('action')
|
}.property('action'),
|
||||||
|
|
||||||
|
blockIcon: function() {
|
||||||
|
return 'icon-remove';
|
||||||
|
}.property(),
|
||||||
|
|
||||||
|
doNothingIcon: function() {
|
||||||
|
return 'icon-ok';
|
||||||
|
}.property(),
|
||||||
|
|
||||||
|
save: function() {
|
||||||
|
return Discourse.ajax("/admin/logs/screened_ip_addresses/" + this.get('id') + ".json", {
|
||||||
|
type: 'PUT',
|
||||||
|
data: {ip_address: this.get('ip_address'), action_name: this.get('action')}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy: function() {
|
||||||
|
return Discourse.ajax("/admin/logs/screened_ip_addresses/" + this.get('id') + ".json", {type: 'DELETE'});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Discourse.ScreenedIpAddress.reopenClass({
|
Discourse.ScreenedIpAddress.reopenClass({
|
||||||
|
@ -5,13 +5,14 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
{{#if model.length}}
|
{{#if model.length}}
|
||||||
|
|
||||||
<div class='table screened-ip-addresses'>
|
<div class='table admin-logs-table screened-ip-addresses'>
|
||||||
<div class="heading-container">
|
<div class="heading-container">
|
||||||
<div class="col heading first ip_address">{{i18n admin.logs.ip_address}}</div>
|
<div class="col heading first ip_address">{{i18n admin.logs.ip_address}}</div>
|
||||||
<div class="col heading action">{{i18n admin.logs.action}}</div>
|
<div class="col heading action">{{i18n admin.logs.action}}</div>
|
||||||
<div class="col heading match_count">{{i18n admin.logs.match_count}}</div>
|
<div class="col heading match_count">{{i18n admin.logs.match_count}}</div>
|
||||||
<div class="col heading last_match_at">{{i18n admin.logs.last_match_at}}</div>
|
<div class="col heading last_match_at">{{i18n admin.logs.last_match_at}}</div>
|
||||||
<div class="col heading created_at">{{i18n admin.logs.created_at}}</div>
|
<div class="col heading created_at">{{i18n admin.logs.created_at}}</div>
|
||||||
|
<div class="col heading actions"></div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,5 +1,14 @@
|
|||||||
<div class="col first ip_address">{{ip_address}}</div>
|
<div class="col first ip_address">
|
||||||
<div class="col action">{{actionName}}</div>
|
{{#if editing}}
|
||||||
|
{{textField value=ip_address autofocus="autofocus"}}
|
||||||
|
{{else}}
|
||||||
|
<span {{action edit this}}>{{ip_address}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<div class="col action">
|
||||||
|
<i {{bindAttr class=":icon actionIcon"}}></i>
|
||||||
|
{{actionName}}
|
||||||
|
</div>
|
||||||
<div class="col match_count">{{match_count}}</div>
|
<div class="col match_count">{{match_count}}</div>
|
||||||
<div class="col last_match_at">
|
<div class="col last_match_at">
|
||||||
{{#if last_match_at}}
|
{{#if last_match_at}}
|
||||||
@ -7,4 +16,18 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
<div class="col created_at">{{unboundAgeWithTooltip created_at}}</div>
|
<div class="col created_at">{{unboundAgeWithTooltip created_at}}</div>
|
||||||
|
<div class="col actions">
|
||||||
|
{{#unless editing}}
|
||||||
|
<button {{action destroy this}}>{{i18n admin.logs.delete}}</button>
|
||||||
|
<button {{action edit this}}>{{i18n admin.logs.edit}}</button>
|
||||||
|
{{#if isBlocked}}
|
||||||
|
<button {{action allow this}}><i {{bindAttr class=":icon doNothingIcon"}}></i> {{i18n admin.logs.screened_ips.actions.do_nothing}}</button>
|
||||||
|
{{else}}
|
||||||
|
<button {{action block this}}><i {{bindAttr class=":icon blockIcon"}}></i> {{i18n admin.logs.screened_ips.actions.block}}</button>
|
||||||
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
|
<button {{action save this}}>{{i18n admin.logs.save}}</button>
|
||||||
|
<a {{action cancel this}}>{{i18n cancel}}</a>
|
||||||
|
{{/unless}}
|
||||||
|
</div>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
@ -700,16 +700,44 @@ table {
|
|||||||
|
|
||||||
// Logs
|
// Logs
|
||||||
|
|
||||||
|
.admin-logs-table {
|
||||||
|
input.ember-text-field {
|
||||||
|
padding: 1px 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.screened-emails, .screened-urls, .screened-ip-addresses {
|
.screened-emails, .screened-urls, .screened-ip-addresses {
|
||||||
width: 900px;
|
width: 900px;
|
||||||
.email, .url {
|
.email, .url {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
}
|
}
|
||||||
.action, .match_count, .last_match_at, .created_at, .ip_address {
|
.action, .match_count, .last_match_at, .created_at {
|
||||||
|
text-align: center;
|
||||||
|
width: 110px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.screened-emails, .screened-urls {
|
||||||
|
.ip_address {
|
||||||
width: 110px;
|
width: 110px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.screened-ip-addresses {
|
||||||
|
.ip_address {
|
||||||
|
width: 150px;
|
||||||
|
text-align: left;
|
||||||
|
input {
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
width: 275px;
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.staff-actions {
|
.staff-actions {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -1,8 +1,34 @@
|
|||||||
class Admin::ScreenedIpAddressesController < Admin::AdminController
|
class Admin::ScreenedIpAddressesController < Admin::AdminController
|
||||||
|
|
||||||
|
before_filter :fetch_screened_ip_address, only: [:update, :destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
screened_emails = ScreenedIpAddress.limit(200).order('last_match_at desc').to_a
|
screened_ip_addresses = ScreenedIpAddress.limit(200).order('last_match_at desc').to_a
|
||||||
render_serialized(screened_emails, ScreenedIpAddressSerializer)
|
render_serialized(screened_ip_addresses, ScreenedIpAddressSerializer)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @screened_ip_address.update_attributes(allowed_params)
|
||||||
|
render json: success_json
|
||||||
|
else
|
||||||
|
render_json_error(@screened_ip_address)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@screened_ip_address.destroy
|
||||||
|
render json: success_json
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def allowed_params
|
||||||
|
params.require(:ip_address)
|
||||||
|
params.permit(:ip_address, :action_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_screened_ip_address
|
||||||
|
@screened_ip_address = ScreenedIpAddress.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -8,7 +8,7 @@ class ScreenedIpAddress < ActiveRecord::Base
|
|||||||
|
|
||||||
default_action :block
|
default_action :block
|
||||||
|
|
||||||
validates :ip_address, presence: true, uniqueness: true
|
validates :ip_address, ip_address_format: true, presence: true
|
||||||
|
|
||||||
def self.watch(ip_address, opts={})
|
def self.watch(ip_address, opts={})
|
||||||
match_for_ip_address(ip_address) || create(opts.slice(:action_type).merge(ip_address: ip_address))
|
match_for_ip_address(ip_address) || create(opts.slice(:action_type).merge(ip_address: ip_address))
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
class ScreenedIpAddressSerializer < ApplicationSerializer
|
class ScreenedIpAddressSerializer < ApplicationSerializer
|
||||||
attributes :ip_address,
|
attributes :id,
|
||||||
|
:ip_address,
|
||||||
:action,
|
:action,
|
||||||
:match_count,
|
:match_count,
|
||||||
:last_match_at,
|
:last_match_at,
|
||||||
|
@ -1223,6 +1223,9 @@ en:
|
|||||||
last_match_at: "Last Matched"
|
last_match_at: "Last Matched"
|
||||||
match_count: "Matches"
|
match_count: "Matches"
|
||||||
ip_address: "IP"
|
ip_address: "IP"
|
||||||
|
delete: 'Delete'
|
||||||
|
edit: 'Edit'
|
||||||
|
save: 'Save'
|
||||||
screened_actions:
|
screened_actions:
|
||||||
block: "block"
|
block: "block"
|
||||||
do_nothing: "do nothing"
|
do_nothing: "do nothing"
|
||||||
@ -1260,7 +1263,10 @@ en:
|
|||||||
screened_ips:
|
screened_ips:
|
||||||
title: "Screened IPs"
|
title: "Screened IPs"
|
||||||
description: "IP addresses that are being watched."
|
description: "IP addresses that are being watched."
|
||||||
allow: "allow"
|
delete_confirm: "Are you sure you want to remove the rule for %{ip_address}?"
|
||||||
|
actions:
|
||||||
|
block: "Block"
|
||||||
|
do_nothing: "Allow"
|
||||||
|
|
||||||
impersonate:
|
impersonate:
|
||||||
title: "Impersonate User"
|
title: "Impersonate User"
|
||||||
|
@ -67,7 +67,7 @@ Discourse::Application.routes.draw do
|
|||||||
scope '/logs' do
|
scope '/logs' do
|
||||||
resources :staff_action_logs, only: [:index]
|
resources :staff_action_logs, only: [:index]
|
||||||
resources :screened_emails, only: [:index]
|
resources :screened_emails, only: [:index]
|
||||||
resources :screened_ip_addresses, only: [:index]
|
resources :screened_ip_addresses, only: [:index, :update, :destroy]
|
||||||
resources :screened_urls, only: [:index]
|
resources :screened_urls, only: [:index]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -23,6 +23,11 @@ module ScreeningModel
|
|||||||
self.action_type ||= self.class.actions[self.class.df_action]
|
self.action_type ||= self.class.actions[self.class.df_action]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def action_name=(arg)
|
||||||
|
raise ArgumentError.new("Invalid action type #{arg}") if arg.nil? or !self.class.actions.has_key?(arg.to_sym)
|
||||||
|
self.action_type = self.class.actions[arg.to_sym]
|
||||||
|
end
|
||||||
|
|
||||||
def record_match!
|
def record_match!
|
||||||
self.match_count += 1
|
self.match_count += 1
|
||||||
self.last_match_at = Time.zone.now
|
self.last_match_at = Time.zone.now
|
||||||
|
11
lib/validators/ip_address_format_validator.rb
Normal file
11
lib/validators/ip_address_format_validator.rb
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Allows unique IP address (10.0.1.20), and IP addresses with a mask (10.0.0.0/8).
|
||||||
|
# Useful when storing in a Postgresql inet column.
|
||||||
|
class IpAddressFormatValidator < ActiveModel::EachValidator
|
||||||
|
|
||||||
|
def validate_each(record, attribute, value)
|
||||||
|
unless record.ip_address.nil? or record.ip_address.split('/').first =~ Resolv::AddressRegex
|
||||||
|
record.errors.add(attribute, :invalid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
@ -30,4 +30,5 @@ describe AllowedIpAddressValidator do
|
|||||||
record.errors[:ip_address].should_not be_present
|
record.errors[:ip_address].should_not be_present
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
@ -0,0 +1,22 @@
|
|||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe IpAddressFormatValidator do
|
||||||
|
|
||||||
|
let(:record) { Fabricate.build(:screened_ip_address, ip_address: '99.232.23.123') }
|
||||||
|
let(:validator) { described_class.new({attributes: :ip_address}) }
|
||||||
|
subject(:validate) { validator.validate_each(record, :ip_address, record.ip_address) }
|
||||||
|
|
||||||
|
[nil, '99.232.23.123', '99.232.0.0/16', 'fd12:db8::ff00:42:8329', 'fc00::/7'].each do |arg|
|
||||||
|
it "should not add an error for #{arg}" do
|
||||||
|
record.ip_address = arg
|
||||||
|
validate
|
||||||
|
record.errors[:ip_address].should_not be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should add an error for invalid IP address' do
|
||||||
|
record.ip_address = '99.99.99'
|
||||||
|
validate
|
||||||
|
record.errors[:ip_address].should be_present
|
||||||
|
end
|
||||||
|
end
|
@ -7,7 +7,7 @@ describe Admin::ScreenedIpAddressesController do
|
|||||||
|
|
||||||
let!(:user) { log_in(:admin) }
|
let!(:user) { log_in(:admin) }
|
||||||
|
|
||||||
context '.index' do
|
describe 'index' do
|
||||||
before do
|
before do
|
||||||
xhr :get, :index
|
xhr :get, :index
|
||||||
end
|
end
|
||||||
|
@ -8,6 +8,29 @@ describe ScreenedIpAddress do
|
|||||||
it 'sets a default action_type' do
|
it 'sets a default action_type' do
|
||||||
described_class.create(valid_params).action_type.should == described_class.actions[:block]
|
described_class.create(valid_params).action_type.should == described_class.actions[:block]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'sets an error when ip_address is invalid' do
|
||||||
|
described_class.create(valid_params.merge(ip_address: '99.99.99')).errors[:ip_address].should be_present
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can set action_type using the action_name virtual attribute' do
|
||||||
|
described_class.new(valid_params.merge(action_name: :do_nothing)).action_type.should == described_class.actions[:do_nothing]
|
||||||
|
described_class.new(valid_params.merge(action_name: :block)).action_type.should == described_class.actions[:block]
|
||||||
|
described_class.new(valid_params.merge(action_name: 'do_nothing')).action_type.should == described_class.actions[:do_nothing]
|
||||||
|
described_class.new(valid_params.merge(action_name: 'block')).action_type.should == described_class.actions[:block]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises a useful exception when action is invalid' do
|
||||||
|
expect {
|
||||||
|
described_class.new(valid_params.merge(action_name: 'dance'))
|
||||||
|
}.to raise_error(ArgumentError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises a useful exception when action is nil' do
|
||||||
|
expect {
|
||||||
|
described_class.new(valid_params.merge(action_name: nil))
|
||||||
|
}.to raise_error(ArgumentError)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#watch' do
|
describe '#watch' do
|
||||||
|
Reference in New Issue
Block a user