مراية لـ
https://github.com/postalserver/postal.git
تم المزامنة 2025-12-01 05:43:04 +00:00
Add IP-based SMTP authentication (#1149)
هذا الالتزام موجود في:
ملتزم من قبل
GitHub
الأصل
5c7802d218
التزام
9a3d568b27
@@ -53,3 +53,21 @@ $ ->
|
|||||||
$(element, $link.parent()).toggle()
|
$(element, $link.parent()).toggle()
|
||||||
false
|
false
|
||||||
|
|
||||||
|
toggleCredentialInputs = (type)->
|
||||||
|
$('[data-credential-key-type]').hide()
|
||||||
|
$('[data-credential-key-type] input').attr('disabled', true)
|
||||||
|
if type == 'SMTP-IP'
|
||||||
|
$('[data-credential-key-type=smtp-ip]').show()
|
||||||
|
$('[data-credential-key-type=smtp-ip] input').attr('disabled', false)
|
||||||
|
else
|
||||||
|
$('[data-credential-key-type=all]').show()
|
||||||
|
|
||||||
|
|
||||||
|
$(document).on 'change', 'select#credential_type', ->
|
||||||
|
value = $(this).val()
|
||||||
|
toggleCredentialInputs(value)
|
||||||
|
|
||||||
|
$(document).on 'turbolinks:load', ->
|
||||||
|
credentialTypeInput = $('select#credential_type')
|
||||||
|
if credentialTypeInput.length
|
||||||
|
toggleCredentialInputs(credentialTypeInput.val())
|
||||||
|
|||||||
@@ -95,6 +95,10 @@
|
|||||||
background-color:$turquoise;
|
background-color:$turquoise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label--credentialType-smtp_ip {
|
||||||
|
background-color:$orange;
|
||||||
|
}
|
||||||
|
|
||||||
.label--spamStatus-not_checked {
|
.label--spamStatus-not_checked {
|
||||||
background:#aaa;
|
background:#aaa;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ class CredentialsController < ApplicationController
|
|||||||
include WithinOrganization
|
include WithinOrganization
|
||||||
|
|
||||||
before_action { @server = organization.servers.present.find_by_permalink!(params[:server_id]) }
|
before_action { @server = organization.servers.present.find_by_permalink!(params[:server_id]) }
|
||||||
before_action { params[:id] && @credential = @server.credentials.find_by_key!(params[:id]) }
|
before_action { params[:id] && @credential = @server.credentials.find_by_uuid!(params[:id]) }
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@credentials = @server.credentials.order(:name).to_a
|
@credentials = @server.credentials.order(:name).to_a
|
||||||
@@ -14,7 +14,7 @@ class CredentialsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@credential = @server.credentials.build(params.require(:credential).permit(:type, :name, :hold))
|
@credential = @server.credentials.build(params.require(:credential).permit(:type, :name, :key, :hold))
|
||||||
if @credential.save
|
if @credential.save
|
||||||
redirect_to_with_json [organization, @server, :credentials]
|
redirect_to_with_json [organization, @server, :credentials]
|
||||||
else
|
else
|
||||||
@@ -23,7 +23,7 @@ class CredentialsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
if @credential.update(params.require(:credential).permit(:name, :hold))
|
if @credential.update(params.require(:credential).permit(:name, :key, :hold))
|
||||||
redirect_to_with_json [organization, @server, :credentials]
|
redirect_to_with_json [organization, @server, :credentials]
|
||||||
else
|
else
|
||||||
render_form_errors 'edit', @credential
|
render_form_errors 'edit', @credential
|
||||||
|
|||||||
@@ -12,24 +12,37 @@
|
|||||||
# created_at :datetime
|
# created_at :datetime
|
||||||
# updated_at :datetime
|
# updated_at :datetime
|
||||||
# hold :boolean default(FALSE)
|
# hold :boolean default(FALSE)
|
||||||
|
# uuid :string(255)
|
||||||
#
|
#
|
||||||
|
|
||||||
class Credential < ApplicationRecord
|
class Credential < ApplicationRecord
|
||||||
|
|
||||||
|
include HasUUID
|
||||||
|
|
||||||
belongs_to :server
|
belongs_to :server
|
||||||
|
|
||||||
TYPES = ['SMTP', 'API']
|
TYPES = ['SMTP', 'API', 'SMTP-IP']
|
||||||
|
|
||||||
validates :key, :presence => true, :uniqueness => true
|
validates :key, :presence => true, :uniqueness => true
|
||||||
validates :type, :inclusion => {:in => TYPES}
|
validates :type, :inclusion => {:in => TYPES}
|
||||||
validates :name, :presence => true
|
validates :name, :presence => true
|
||||||
|
validate :validate_key_cannot_be_changed
|
||||||
random_string :key, :type => :chars, :length => 24, :unique => true
|
validate :validate_key_for_smtp_ip
|
||||||
|
|
||||||
serialize :options, Hash
|
serialize :options, Hash
|
||||||
|
|
||||||
|
before_validation :generate_key
|
||||||
|
|
||||||
|
|
||||||
|
def generate_key
|
||||||
|
return if self.type == 'SMTP-IP'
|
||||||
|
return if self.persisted?
|
||||||
|
|
||||||
|
self.key = SecureRandomString.new(24)
|
||||||
|
end
|
||||||
|
|
||||||
def to_param
|
def to_param
|
||||||
key
|
uuid
|
||||||
end
|
end
|
||||||
|
|
||||||
def use
|
def use
|
||||||
@@ -54,4 +67,30 @@ class Credential < ApplicationRecord
|
|||||||
Base64.encode64("\0XX\0#{self.key}").strip
|
Base64.encode64("\0XX\0#{self.key}").strip
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ipaddr
|
||||||
|
return unless type == 'SMTP-IP'
|
||||||
|
|
||||||
|
@ipaddr ||= IPAddr.new(self.key)
|
||||||
|
rescue IPAddr::InvalidAddressError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def validate_key_cannot_be_changed
|
||||||
|
return if new_record?
|
||||||
|
return unless key_changed?
|
||||||
|
return if type == 'SMTP-IP'
|
||||||
|
|
||||||
|
errors.add :key, "cannot be changed"
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_key_for_smtp_ip
|
||||||
|
return unless type == 'SMTP-IP'
|
||||||
|
|
||||||
|
IPAddr.new(self.key.to_s)
|
||||||
|
rescue IPAddr::InvalidAddressError
|
||||||
|
errors.add :key, "must be a valid IPv4 or IPv6 address"
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,14 +15,23 @@
|
|||||||
%p.fieldSet__text
|
%p.fieldSet__text
|
||||||
This is a friendly name so you can identify this credential later. You can enter anything
|
This is a friendly name so you can identify this credential later. You can enter anything
|
||||||
you want here, the more descriptive the better.
|
you want here, the more descriptive the better.
|
||||||
.fieldSet__field
|
|
||||||
|
.fieldSet__field{data: {credential_key_type: 'all'}}
|
||||||
= f.label :key, :class => 'fieldSet__label'
|
= f.label :key, :class => 'fieldSet__label'
|
||||||
.fieldSet__input
|
.fieldSet__input
|
||||||
= f.text_field :key, :readonly => true, :class => 'input input--text input--code', :placeholder => "Automatically generated", :tabindex => 1000, :value => (@credential.new_record? ? '' : @credential.key)
|
= f.text_field :key, :readonly => false, :class => 'input input--text input--code', :placeholder => "Automatically generated", :tabindex => 1000, :value => (@credential.new_record? ? '' : @credential.key)
|
||||||
%p.fieldSet__text
|
%p.fieldSet__text
|
||||||
This is the unique key which will be used to authenticate any requests to the API or our SMTP servers.
|
This is the unique key which will be used to authenticate any requests to the API or the SMTP servers.
|
||||||
It will be generated randomly and cannot be changed. If you need a new token, you can create a new one and then
|
It will be generated randomly and cannot be changed. If you need a new token, you can create a new one and then
|
||||||
delete the old one when you're ready.
|
delete the old one when you're ready.
|
||||||
|
|
||||||
|
.fieldSet__field{data: {credential_key_type: 'smtp-ip'}}
|
||||||
|
= f.label :key, "Network", :class => 'fieldSet__label'
|
||||||
|
.fieldSet__input
|
||||||
|
= f.text_field :key, :class => 'input input--text input--code'
|
||||||
|
%p.fieldSet__text
|
||||||
|
This is the IP address or network that you wish to allow to authenticate to this mail server.
|
||||||
|
|
||||||
.fieldSet__field
|
.fieldSet__field
|
||||||
= f.label :hold, :class => 'fieldSet__label'
|
= f.label :hold, :class => 'fieldSet__label'
|
||||||
.fieldSet__input
|
.fieldSet__input
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
%li.credentialList__item
|
%li.credentialList__item
|
||||||
= link_to [:edit, organization, @server, credential], :class => 'credentialList__link' do
|
= link_to [:edit, organization, @server, credential], :class => 'credentialList__link' do
|
||||||
.credentialList__type
|
.credentialList__type
|
||||||
%span.label{:class => "label--credentialType-#{credential.type.underscore}"}= credential.type
|
%span.label{:class => "label--credentialType-#{credential.type.underscore}"}= credential.type.split('-').last
|
||||||
.credentialList__properties
|
.credentialList__properties
|
||||||
%p.credentialList__name
|
%p.credentialList__name
|
||||||
= credential.name
|
= credential.name
|
||||||
|
|||||||
8
db/migrate/20200717083943_add_uuid_to_credentials.rb
Normal file
8
db/migrate/20200717083943_add_uuid_to_credentials.rb
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
class AddUUIDToCredentials < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_column :credentials, :uuid, :string
|
||||||
|
Credential.find_each do |c|
|
||||||
|
c.update_column(:uuid, SecureRandom.uuid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2018_02_16_114344) do
|
ActiveRecord::Schema.define(version: 2020_07_17_083943) do
|
||||||
|
|
||||||
create_table "additional_route_endpoints", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
|
create_table "additional_route_endpoints", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
|
||||||
t.integer "route_id"
|
t.integer "route_id"
|
||||||
@@ -68,6 +68,7 @@ ActiveRecord::Schema.define(version: 2018_02_16_114344) do
|
|||||||
t.datetime "created_at", precision: 6
|
t.datetime "created_at", precision: 6
|
||||||
t.datetime "updated_at", precision: 6
|
t.datetime "updated_at", precision: 6
|
||||||
t.boolean "hold", default: false
|
t.boolean "hold", default: false
|
||||||
|
t.string "uuid"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "domains", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
|
create_table "domains", id: :integer, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
|
||||||
|
|||||||
@@ -312,10 +312,20 @@ module Postal
|
|||||||
end
|
end
|
||||||
|
|
||||||
else
|
else
|
||||||
# This is unaccepted mail
|
# User is trying to relay but is not authenticated. Try to authenticate by IP address
|
||||||
|
@credential = Credential.where(:type => 'SMTP-IP').all.sort_by { |c| c.ipaddr&.prefix || 0 }.reverse.find do |credential|
|
||||||
|
credential.ipaddr.include?(@ip_address)
|
||||||
|
end
|
||||||
|
|
||||||
|
if @credential
|
||||||
|
# Retry with credential
|
||||||
|
@credential.use
|
||||||
|
rcpt_to(data)
|
||||||
|
else
|
||||||
'530 Authentication required'
|
'530 Authentication required'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def data(data)
|
def data(data)
|
||||||
unless in_state(:rcpt_to_received)
|
unless in_state(:rcpt_to_received)
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم