...
 
Commits (162)
This diff is collapsed.
inherit_from: .rubocop_todo.yml
AllCops:
Exclude:
- 'db/**/*'
- 'vendor/**/*'
Style/StringLiterals:
Enabled: true
Layout/EmptyLineBetweenDefs:
Enabled: true
Layout/SpaceAfterComma:
Enabled: true
This diff is collapsed.
......@@ -3,19 +3,26 @@ Change Log
This project adheres to [Semantic Versioning](http://semver.org/).
## [3.4.0] / 2019-02-14
## [4.0] / 2019-01-XX
### Fixed
* Stop leaking keywords to third parties by stripping HTML from multipart/alternative messages if they contain keywords. (#399)
* Avoid shelling out in a test-case to avoid an occasional error occurring in CI runs that complains about invalid data in ASCII-8BIT strings.
* Show error message if X-RESEND (and family) is forbidden by the list-config.
### Added
* 'X-STOP': To use any keyword, you *must* now also use the new keyword 'X-STOP' to mark where to stop looking for keywords. This enables looking for keyword arguments in multiple lines, e.g. for X-RESEND with long, wrapped lines.
* Keywords for getting (new) passwords for accounts. 'X-GET-NEW-PASSWORD' sets and sends back a new password for the account of the subscribed email-address. 'X-GET-NEW-PASSWORD-FOR: subscription1@example.org' sets and sends back a new password for the account of the given email-address; this is allowed for admins only and allows to get a password for people that have no key associated with their subscription, yet.
### Changed
* Update the dependency 'mail' to version 2.7.x., and allow carriage returns (CR) in test-cases as mail-2.7 puts those out.
* Update the dependency 'sqlite3' to version 1.3.x.
* Adapt fixtures and factories for factorybot version 5.x.
* Let schleuder-code load the filter files in test-mode, avoid explicit path names (which make headaches when running tests on installed packages).
* Drop support for GPG 2.0, require GPG 2.2.
* Drop support to migrate lists from version 2. This includes pin_keys code, which looked for subscriptions without an associated key, and tried to find a distinctly matching key. Originally, this was implemented to help with a shortcoming of code which handled version 2 to version 3 migration. (#411)
* "Plugins" are now called "keyword handlers", and they are implemented differently. If you use custom plugins you have to rewrite them (see an included keyword handler for implementation hints, it's rather simple). If you don't, this change doesn't affect you. One positive effect of this: if a message contains an unknown keyword, no keyword is being handled but the sender is sent an error message; thus we avoid half-handled messages.
* Allow only fingerprints as argument to X-DELETE-KEY. We want to reference keys only by fingerprint, if possible (as we do with other keywords already).
* The list-option `keywords_admin_only` has been removed. The functionality will be replaced by some other means of configuration. [TODO: explain/adapt when replacement is implemented]
* Drop deprecated X-LISTNAME keyword. (#374)
## [3.3.0] / 2018-09-04
......
......@@ -10,8 +10,8 @@ For more details see <https://schleuder.org/docs/>.
Requirements
------------
* ruby >=2.1
* gnupg 2.0.x, or >=2.1.16
* ruby >=2.3
* gnupg >=2.2
* gpgme
* sqlite3
* openssl
......@@ -47,15 +47,15 @@ Additionally these **rubygems** are required (will be installed automatically un
Installing Schleuder
------------
1. Download [the gem](https://schleuder.org/download/schleuder-3.4.0.gem) and [the OpenPGP-signature](https://schleuder.org/download/schleuder-3.4.0.gem.sig) and verify:
1. Download [the gem](https://schleuder.org/download/schleuder-3.3.0.gem) and [the OpenPGP-signature](https://schleuder.org/download/schleuder-3.3.0.gem.sig) and verify:
```
gpg --recv-key 0xB3D190D5235C74E1907EACFE898F2C91E2E6E1F3
gpg --verify schleuder-3.4.0.gem.sig
gpg --verify schleuder-3.3.0.gem.sig
```
2. If all went well install the gem:
```
gem install schleuder-3.4.0.gem
gem install schleuder-3.3.0.gem
```
3. Set up schleuder:
......@@ -145,4 +145,4 @@ GNU GPL 3.0. Please see [LICENSE.txt](LICENSE.txt).
Alternative Download
--------------------
Alternatively to the gem-files you can download the latest release as [a tarball](https://schleuder.org/download/schleuder-3.4.0.tar.gz) and [its OpenPGP-signature](https://schleuder.org/download/schleuder-3.4.0.tar.gz.sig).
Alternatively to the gem-files you can download the latest release as [a tarball](https://schleuder.org/download/schleuder-3.3.0.tar.gz) and [its OpenPGP-signature](https://schleuder.org/download/schleuder-3.3.0.tar.gz.sig).
......@@ -7,7 +7,7 @@ require_relative "lib/#{project}.rb"
@filename_gem = "#{@tagname}.gem"
@filename_tarball = "#{@tagname}.tar.gz"
load "active_record/railties/databases.rake"
load 'active_record/railties/databases.rake'
# Configure ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.tap do |config|
......@@ -19,7 +19,7 @@ ActiveRecord::Tasks::DatabaseTasks.tap do |config|
end
# ActiveRecord requires this task to be present
Rake::Task.define_task("db:environment")
Rake::Task.define_task('db:environment')
namespace :db do
# A shortcut.
......@@ -42,7 +42,7 @@ end
task :publish_gem => :website
task :git_tag => :check_version
desc "Build new version: git-tag and gem-file"
desc 'Build new version: git-tag and gem-file'
task :new_version => [
:check_version,
:edit_readme, :edit_changelog,
......@@ -57,12 +57,12 @@ task :new_version => [
] do
end
desc "Edit CHANGELOG.md"
desc 'Edit CHANGELOG.md'
task :edit_changelog do
edit_and_add_file('CHANGELOG')
end
desc "Edit README"
desc 'Edit README'
task :edit_readme do
edit_and_add_file('README')
end
......@@ -72,12 +72,12 @@ task :git_tag do
`git tag -u #{@gpguid} -s -m "Version #{@version}" #{@tagname}`
end
desc "Add changed version to git-index"
desc 'Add changed version to git-index'
task :git_add_version do
`git add lib/#{project}/version.rb`
end
desc "Commit changes as new version"
desc 'Commit changes as new version'
task :git_commit do
`git commit -m "Version #{@version}"`
end
......@@ -111,10 +111,10 @@ desc 'Publish gem-file to rubygems.org'
task :publish_gem do
puts "Really push #{@filename_gem} to rubygems.org? [yN]"
if gets.match(/^y/i)
puts "Pushing..."
puts 'Pushing...'
`gem push #{@filename_gem}`
else
puts "Not pushed."
puts 'Not pushed.'
end
end
......@@ -125,7 +125,7 @@ end
desc 'Describe manual release-tasks'
task :website do
puts "Please remember to publish the release-notes on the website and on schleuder-announce."
puts 'Please remember to publish the release-notes on the website and on schleuder-announce.'
end
desc 'Check if version-tag already exists'
......@@ -133,7 +133,7 @@ task :check_version do
# Check if Schleuder::VERSION has been updated since last release
if `git tag`.match?(/^#{@tagname}$/)
$stderr.puts "Warning: Tag '#{@tagname}' already exists. Did you forget to update #{project}/version.rb?"
$stderr.print "Delete tag to continue? [yN] "
$stderr.print 'Delete tag to continue? [yN] '
if $stdin.gets.match(/^y/i)
`git tag -d #{@tagname}`
else
......
#!/usr/bin/env ruby
# This file can be deleted once we cease to support gnupg 2.0.
require 'fileutils'
require 'cgi'
require 'openssl'
def respond(msg, flush=true)
$stdout.puts msg
if flush
$stdout.flush
end
end
def send_ok(flush=true)
respond 'OK', flush
end
def send_password
if File.exist?(OLDPWDSENTFILE)
pwd = ''
if File.exist?(EMPTYPWDSENTFILE1)
FileUtils.touch(EMPTYPWDSENTFILE2)
else
FileUtils.touch(EMPTYPWDSENTFILE1)
end
else
pwd = OLDPASSWD
FileUtils.touch(OLDPWDSENTFILE)
end
respond "D #{pwd}"
end
def do_exit
if File.exist?(EMPTYPWDSENTFILE2)
FileUtils.rm_rf(TMPDIR)
end
exit 0
end
OLDPASSWD = CGI.escape(ENV['PINENTRY_USER_DATA'].to_s)
if OLDPASSWD.empty?
respond "Fatal error: passed PINENTRY_USER_DATA was empty, cannot continue"
exit 1
end
# We need a static directory name to maintain the state across invocations of
# this file.
TMPDIR = File.join(ENV['GNUPGHOME'], '.tmp-pinentry-clearpassphrase')
OLDPWDSENTFILE = File.join(TMPDIR, '1')
EMPTYPWDSENTFILE1 = File.join(TMPDIR, '2')
EMPTYPWDSENTFILE2 = File.join(TMPDIR, '3')
if ! Dir.exist?(TMPDIR)
Dir.mkdir(TMPDIR)
end
respond "OK - what's up?"
while line = $stdin.gets do
case line
when /^GETPIN/
send_password
send_ok
when /^BYE/
send_ok false
do_exit
else
send_ok
end
end
......@@ -4,7 +4,7 @@
# error-message.
$VERBOSE=nil
trap("INT") { exit 1 }
trap('INT') { exit 1 }
begin
......
......@@ -7,7 +7,7 @@
require 'socket'
require 'open3'
trap ("INT") { exit 0 }
trap ('INT') { exit 0 }
def usage
puts "Usage: #{File.basename(__FILE__)} [-p portnum]"
......@@ -30,28 +30,28 @@ schleuderbin = File.join(File.dirname(__FILE__), 'schleuder')
begin
# run the server
server = TCPServer.new("127.0.0.1", port)
server = TCPServer.new('127.0.0.1', port)
# receive input
while (connection = server.accept)
input = ''
recipient = ''
connection.puts "220 localhost SMTP"
connection.puts '220 localhost SMTP'
begin
while line = connection.gets
line.chomp!
case line[0..3].downcase
when 'ehlo', 'helo'
connection.puts "250 localhost"
connection.puts '250 localhost'
when 'mail', 'rset'
connection.puts "250 ok"
connection.puts '250 ok'
when 'rcpt'
recipient = line.split(':').last.gsub(/[<>\s]*/, '')
connection.puts "250 ok"
connection.puts '250 ok'
when 'data'
connection.puts "354 go ahead"
connection.puts '354 go ahead'
when 'quit'
connection.puts "221 localhost"
connection.puts '221 localhost'
when '.'
puts "New message to #{recipient}"
err, status = Open3.capture2e("#{schleuderbin} work #{recipient}", {stdin_data: input})
......@@ -59,7 +59,7 @@ begin
puts "Error from schleuder: #{err}."
connection.puts "550 #{err}"
else
connection.puts "250 ok"
connection.puts '250 ok'
end
else
input << line + "\n"
......
class CreateLists < ActiveRecord::Migration
class CreateLists < ActiveRecord::Migration[5.2]
def up
if ! table_exists?(:lists)
create_table :lists do |t|
......
class CreateSubscriptions < ActiveRecord::Migration
class CreateSubscriptions < ActiveRecord::Migration[5.2]
def up
if ! table_exists?(:subscriptions)
create_table :subscriptions do |t|
......
class AddLanguageToLists < ActiveRecord::Migration
class AddLanguageToLists < ActiveRecord::Migration[5.2]
def up
if ! column_exists?(:lists, :language)
add_column :lists, :language, :string, default: 'en'
......
class ChangeKeywordsAdminOnlyDefaults < ActiveRecord::Migration
class ChangeKeywordsAdminOnlyDefaults < ActiveRecord::Migration[5.2]
def up
change_column_default :lists, :keywords_admin_only, "[\"subscribe\", \"unsubscribe\", \"delete-key\"]"
end
......
class AddForwardAllIncomingToAdmins < ActiveRecord::Migration
class AddForwardAllIncomingToAdmins < ActiveRecord::Migration[5.2]
def up
if ! column_exists?(:lists, :forward_all_incoming_to_admins)
add_column :lists, :forward_all_incoming_to_admins, :boolean, default: false
......
class ChangeSendEncryptedOnlyDefault < ActiveRecord::Migration
class ChangeSendEncryptedOnlyDefault < ActiveRecord::Migration[5.2]
def up
change_column_default :lists, :send_encrypted_only, true
end
......
class AddLogfilesToKeepToLists < ActiveRecord::Migration
class AddLogfilesToKeepToLists < ActiveRecord::Migration[5.2]
def up
if ! column_exists?(:lists, :logfiles_to_keep)
add_column :lists, :logfiles_to_keep, :integer, default: 2
......
class RenameDeliveryDisabledToDeliveryEnabledAndChangeDefault < ActiveRecord::Migration
class RenameDeliveryDisabledToDeliveryEnabledAndChangeDefault < ActiveRecord::Migration[5.2]
def up
if column_exists?(:subscriptions, :delivery_disabled)
rename_column :subscriptions, :delivery_disabled, :delivery_enabled
......
class StripGpgPassphrase < ActiveRecord::Migration
class StripGpgPassphrase < ActiveRecord::Migration[5.2]
def up
if column_exists?(:lists, :gpg_passphrase)
remove_column :lists, :gpg_passphrase
......
class RemoveDefaultMime < ActiveRecord::Migration
class RemoveDefaultMime < ActiveRecord::Migration[5.2]
def up
remove_column :lists, :default_mime
end
......
class FixHeadersToMetaDefaults < ActiveRecord::Migration
class FixHeadersToMetaDefaults < ActiveRecord::Migration[5.2]
def up
change_column_default :lists, :headers_to_meta, '["from", "to", "date", "cc"]'
end
......
class AddInternalFooterToList < ActiveRecord::Migration
class AddInternalFooterToList < ActiveRecord::Migration[5.2]
def up
if ! column_exists?(:lists, :internal_footer)
add_column :lists, :internal_footer, :text, default: ''
......
class AddSigEncToHeadersToMetaDefaults < ActiveRecord::Migration
class AddSigEncToHeadersToMetaDefaults < ActiveRecord::Migration[5.2]
def up
change_column_default :lists, :headers_to_meta, '["from", "to", "cc", "date", "sig", "enc"]'
list_klass = create_list_klass
......
class CreateAccounts < ActiveRecord::Migration[5.2]
def up
create_table :accounts do |t|
t.string :email, null: false
t.string :password_digest, null: false
t.boolean :api_superadmin, null: false, default: false
end
add_index :accounts, :email, unique: true
end
def down
drop_table(:accounts)
end
end
class RemoveKeywordsAdminOnly < ActiveRecord::Migration[5.2]
def up
remove_column :lists, :keywords_admin_only
end
def down
add_column :lists, :keywords_admin_only, :text, default: "[\"subscribe\", \"unsubscribe\", \"delete-key\"]"
end
end
# encoding: UTF-8
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
......@@ -11,52 +10,57 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180110203100) do
ActiveRecord::Schema.define(version: 2019_02_20_111200) do
create_table "accounts", force: :cascade do |t|
t.string "email", null: false
t.string "password_digest", null: false
t.boolean "api_superadmin", default: false, null: false
t.index ["email"], name: "index_accounts_on_email", unique: true
end
create_table "lists", force: :cascade do |t|
t.datetime "created_at"
t.datetime "updated_at"
t.string "email", limit: 255
t.string "fingerprint", limit: 255
t.string "log_level", limit: 255, default: "warn"
t.string "subject_prefix", limit: 255, default: ""
t.string "subject_prefix_in", limit: 255, default: ""
t.string "subject_prefix_out", limit: 255, default: ""
t.string "openpgp_header_preference", limit: 255, default: "signencrypt"
t.text "public_footer", default: ""
t.text "headers_to_meta", default: "[\"from\", \"to\", \"cc\", \"date\", \"sig\", \"enc\"]"
t.text "bounces_drop_on_headers", default: "{\"x-spam-flag\":\"yes\"}"
t.text "keywords_admin_only", default: "[\"subscribe\", \"unsubscribe\", \"delete-key\"]"
t.text "keywords_admin_notify", default: "[\"add-key\"]"
t.boolean "send_encrypted_only", default: true
t.boolean "receive_encrypted_only", default: false
t.boolean "receive_signed_only", default: false
t.boolean "receive_authenticated_only", default: false
t.boolean "receive_from_subscribed_emailaddresses_only", default: false
t.boolean "receive_admin_only", default: false
t.boolean "keep_msgid", default: true
t.boolean "bounces_drop_all", default: false
t.boolean "bounces_notify_admins", default: true
t.boolean "include_list_headers", default: true
t.boolean "include_openpgp_header", default: true
t.integer "max_message_size_kb", default: 10240
t.string "language", limit: 255, default: "en"
t.boolean "forward_all_incoming_to_admins", default: false
t.integer "logfiles_to_keep", default: 2
t.text "internal_footer", default: ""
t.string "email", limit: 255
t.string "fingerprint", limit: 255
t.string "log_level", limit: 255, default: "warn"
t.string "subject_prefix", limit: 255, default: ""
t.string "subject_prefix_in", limit: 255, default: ""
t.string "subject_prefix_out", limit: 255, default: ""
t.string "openpgp_header_preference", limit: 255, default: "signencrypt"
t.text "public_footer", default: ""
t.text "headers_to_meta", default: "[\"from\", \"to\", \"cc\", \"date\", \"sig\", \"enc\"]"
t.text "bounces_drop_on_headers", default: "{\"x-spam-flag\":\"yes\"}"
t.text "keywords_admin_notify", default: "[\"add-key\"]"
t.boolean "send_encrypted_only", default: true
t.boolean "receive_encrypted_only", default: false
t.boolean "receive_signed_only", default: false
t.boolean "receive_authenticated_only", default: false
t.boolean "receive_from_subscribed_emailaddresses_only", default: false
t.boolean "receive_admin_only", default: false
t.boolean "keep_msgid", default: true
t.boolean "bounces_drop_all", default: false
t.boolean "bounces_notify_admins", default: true
t.boolean "include_list_headers", default: true
t.boolean "include_openpgp_header", default: true
t.integer "max_message_size_kb", default: 10240
t.string "language", limit: 255, default: "en"
t.boolean "forward_all_incoming_to_admins", default: false
t.integer "logfiles_to_keep", default: 2
t.text "internal_footer", default: ""
end
create_table "subscriptions", force: :cascade do |t|
t.integer "list_id"
t.string "email", limit: 255
t.string "fingerprint", limit: 255
t.boolean "admin", default: false
t.boolean "delivery_enabled", default: true
t.integer "list_id"
t.string "email", limit: 255
t.string "fingerprint", limit: 255
t.boolean "admin", default: false
t.boolean "delivery_enabled", default: true
t.datetime "created_at"
t.datetime "updated_at"
t.index ["email", "list_id"], name: "index_subscriptions_on_email_and_list_id", unique: true
t.index ["list_id"], name: "index_subscriptions_on_list_id"
end
add_index "subscriptions", ["email", "list_id"], name: "index_subscriptions_on_email_and_list_id", unique: true
add_index "subscriptions", ["list_id"], name: "index_subscriptions_on_list_id"
end
......@@ -26,7 +26,7 @@ function abort {
[ -z ${SCHLOCKER_CONFIG_PATH+x} ] && SCHLOCKER_CONFIG_PATH="/etc/schleuder/schleuder.yml"
[ -z ${SCHLOCKER_CONFIG_SUPERADMIN+x} ] && SCHLOCKER_CONFIG_SUPERADMIN="root@localhost"
[ -z ${SCHLOCKER_CONFIG_LISTS_DIR+x} ] && SCHLOCKER_CONFIG_LISTS_DIR="$SCHLOCKER_HOMEDIR/lists"
[ -z ${SCHLOCKER_CONFIG_PLUGINS_DIR+x} ] && SCHLOCKER_CONFIG_PLUGINS_DIR="/etc/schleuder/plugins"
[ -z ${SCHLOCKER_CONFIG_KEYWORD_HANDLERS_DIR+x} ] && SCHLOCKER_CONFIG_KEYWORD_HANDLERS_DIR="/usr/local/lib/schleuder/keyword_handlers"
[ -z ${SCHLOCKER_CONFIG_LOG_LEVEL+x} ] && SCHLOCKER_CONFIG_LOG_LEVEL="warn"
[ -z ${SCHLOCKER_CONFIG_SMTP_HOST+x} ] && SCHLOCKER_CONFIG_SMTP_HOST="localhost"
[ -z ${SCHLOCKER_CONFIG_SMTP_PORT+x} ] && SCHLOCKER_CONFIG_SMTP_PORT="25"
......@@ -229,7 +229,7 @@ if [ ! -e "$SCHLOCKER_CONFIG_PATH" ]; then
SCHLOCKER_CONFIG="---
superadmin: $SCHLOCKER_CONFIG_SUPERADMIN
lists_dir: $SCHLOCKER_CONFIG_LISTS_DIR
plugins_dir: $SCHLOCKER_CONFIG_PLUGINS_DIR
keyword_handlers_dir: $SCHLOCKER_CONFIG_KEYWORD_HANDLERS_DIR
log_level: $SCHLOCKER_CONFIG_LOG_LEVEL
smtp_settings:
# For explanation see documentation for ActionMailer::smtp_settings, e.g. <http://api.rubyonrails.org/classes/ActionMailer/Base.html>.
......
......@@ -51,12 +51,6 @@ headers_to_meta:
# and a thread of (encrypted) messages can be built by an eavesdropper.
keep_msgid: true
# Which keywords ("email-commands") should be restricted to list-admins?
keywords_admin_only:
- subscribe
- unsubscribe
- delete-key
# For which keywords should the list-admins receive a notice whenever it
# triggers a command.
keywords_admin_notify:
......
......@@ -4,8 +4,8 @@ lists_dir: /var/lib/schleuder/lists
# Where to write list-logs. The actual log-file will be <lists_logs_base_dir>/<hostname>/<listname>/list.log.
listlogs_dir: /var/lib/schleuder/lists
# Schleuder reads plugins also from this directory.
plugins_dir: /etc/schleuder/plugins
# Schleuder looks for additional, custom keyword-handlers in this directory.
keyword_handlers_dir: /usr/local/lib/schleuder/keyword_handlers
# Schleuder reads filters also from this directory path,
# in the specific pre_decryption or post_decryption subdirectory.
......
......@@ -48,6 +48,26 @@ class SchleuderApiDaemon < Sinatra::Base
ActiveRecord::Base.connection.close
end
error Errors::KeyNotFound do
status 404
body 'Key not found.'
end
error Errors::SubscriptionNotFound do
status 404
body 'Subscription not found.'
end
error Errors::ListNotFound do
status 404
body 'List not found.'
end
error Errors::Unauthorized do
status 403
body('Not authorized')
end
error do
exc = env['sinatra.error']
logger.error "Error: #{env['sinatra.error'].message}"
......@@ -63,6 +83,14 @@ class SchleuderApiDaemon < Sinatra::Base
'Not found'
end
error 401 do
'Not authenticated'
end
error 403 do
'Not authorized'
end
def self.run!
super do |server|
server.ssl = true
......
module SchleuderApiDaemonHelper
def valid_credentials?
@auth ||= Rack::Auth::Basic::Request.new(request.env)
if @auth.provided? && @auth.basic? && @auth.credentials.present?
username, api_key = @auth.credentials
username == 'schleuder' && Conf.api_valid_api_keys.include?(api_key)
if ! @auth.provided? || ! @auth.basic? || @auth.credentials.blank?
return false
end
email, password = @auth.credentials
account = Account.find_by(email: email)
if account.try(:authenticate, password)
@current_account = account
true
else
false
end
......@@ -18,41 +23,12 @@ module SchleuderApiDaemonHelper
end
end
def list(id_or_email=nil)
if id_or_email.blank?
if params[:list_id].present?
id_or_email = params[:list_id]
else
client_error "Parameter list_id is required"
end
end
if is_an_integer?(id_or_email)
list = List.where(id: id_or_email).first
else
# list_id is actually an email address
list = List.where(email: id_or_email).first
end
list || halt(404)
end
def subscription(id_or_email)
if is_an_integer?(id_or_email)
sub = Subscription.where(id: id_or_email.to_i).first
else
# Email
if params[:list_id].blank?
client_error "Parameter list_id is required when using email as identifier for subscriptions."
else
sub = list.subscriptions.where(email: id_or_email).first
end
end
sub || halt(404)
def authorize!(resource, action)
current_account.authorize!(resource, action) || halt(404)
end
def requested_list_id
# ActiveResource doesn't want to use query-params with create(), so here
# list_id might be included in the request-body.
params['list_id'] || parsed_body['list_id'] || client_error('Need list_id')
def current_account
@current_account
end
def parsed_body
......
class SchleuderApiDaemon < Sinatra::Base
register Sinatra::Namespace
namespace '/keys' do
get '.json' do
keys = list.keys.sort_by(&:email).map do |key|
namespace '/lists' do
get '/:list_email/keys.json' do |list_email|
keys = keys_controller.find_all(list_email)
keys_hash = keys.sort_by(&:email).map do |key|
key_to_hash(key)
end
json keys
json keys_hash
end
post '.json' do
post '/:list_email/keys.json' do |list_email|
input = parsed_body['keymaterial']
if ! input.match('BEGIN PGP')
if !input.match('BEGIN PGP')
input = Base64.decode64(input)
end
json list(requested_list_id).import_key(input)
json keys_controller.import(list_email, input)
end
get '/check_keys.json' do
json result: list.check_keys
get '/:list_email/keys/check.json' do |list_email|
json result: keys_controller.check(list_email)
end
get '/:fingerprint.json' do |fingerprint|
if key = list.key(fingerprint)
json key_to_hash(key, true)
else
404
end
get '/:list_email/keys/:fingerprint.json' do |list_email, fingerprint|
key = keys_controller.find(list_email, fingerprint)
json key_to_hash(key, true)
end
delete '/:fingerprint.json' do |fingerprint|
if list.delete_key(fingerprint)
200
else
404
end
delete '/:list_email/keys/:fingerprint.json' do |list_email, fingerprint|
keys_controller.delete(list_email, fingerprint)
end
end
private
def keys_controller
Schleuder::KeysController.new(current_account)
end
end
......@@ -3,16 +3,16 @@ class SchleuderApiDaemon < Sinatra::Base
namespace '/lists' do
get '.json' do
json List.all, include: :subscriptions
json(lists_controller.find_all, include: :subscriptions)
end
post '.json' do
listname = parsed_body['email']
list_email = parsed_body['email']
fingerprint = parsed_body['fingerprint']
adminaddress = parsed_body['adminaddress']
adminfingerprint = parsed_body['adminfingerprint']
adminkey = parsed_body['adminkey']
list, messages = ListBuilder.new({email: listname, fingerprint: fingerprint}, adminaddress, adminfingerprint, adminkey).run
list, messages = lists_controller.create(list_email, fingerprint, adminaddress, adminfingerprint, adminkey)
if list.nil?
client_error(messages, 422)
elsif ! list.valid?
......@@ -24,46 +24,47 @@ class SchleuderApiDaemon < Sinatra::Base
end
get '/configurable_attributes.json' do
json(List.configurable_attributes) + "\n"
json(lists_controller.get_configurable_attributes) + "\n"
end
post '/send_list_key_to_subscriptions.json' do
json(result: list.send_list_key_to_subscriptions)
post '/:list_email/send_list_key_to_subscriptions.json' do |list_email|
json(result: lists_controller.send_list_key_to_subscriptions(list_email))
end
get '/new.json' do
json List.new
json lists_controller.new_list
end
get '/:id.json' do |id|
json list(id)
get '/:email.json' do |email|
json lists_controller.find(email)
end
put '/:id.json' do |id|
list = list(id)
if list.update(parsed_body)
put '/:email.json' do |email|
if lists_controller.update(email, parsed_body)
204
else
client_error(list)
end
end
patch '/:id.json' do |id|
list = list(id)
if list.update(parsed_body)
patch '/:email.json' do |email|
if lists_controller.update(email, parsed_body)
204
else
client_error(list)
end
end
delete '/:id.json' do |id|
list = list(id)
if list.destroy
delete '/:email.json' do |email|
if lists_controller.delete(email)
200
else
client_error(list)
end
end
end
def lists_controller
Schleuder::ListsController.new(current_account)
end
end
class SchleuderApiDaemon < Sinatra::Base
register Sinatra::Namespace
namespace '/subscriptions' do
get '.json' do
filterkeys = Subscription.configurable_attributes + [:list_id, :email]
namespace '/lists' do
get '/:list_email/subscriptions.json' do |list_email|
filterkeys = subscriptions_controller.get_configurable_attributes + ['email']
filter = params.select do |param|
filterkeys.include?(param.to_sym)
filterkeys.include?(param)
end
logger.debug "Subscription filter: #{filter.inspect}"
if filter['list_id'] && ! is_an_integer?(filter['list_id'])
# Value is an email-address
if list = List.where(email: filter['list_id']).first
filter['list_id'] = list.id
else
status 404
return json(errors: 'No such list')
end
end
json Subscription.where(filter)
json subscriptions_controller.find_all(list_email, filter)
end
post '.json' do
post '/:list_email/subscriptions.json' do |list_email|
begin
list = list(requested_list_id)
# We don't have to care about nil-values, subscribe() does that for us.
sub, msgs = list.subscribe(
parsed_body['email'],
parsed_body['fingerprint'],
parsed_body['admin'],
parsed_body['delivery_enabled'],
find_key_material
)
set_x_messages(msgs)
logger.debug "subcription: #{sub.inspect}"
if sub.valid?
logger.debug "Subscribed: #{sub.inspect}"
# TODO: why redirect instead of respond with result?
redirect to("/subscriptions/#{sub.id}.json"), 201
attributes = find_attributes_from_body(%w[email fingerprint admin delivery_enabled])
subscription, messages = subscriptions_controller.subscribe(list_email, attributes, find_key_material)
set_x_messages(messages)
logger.debug "subcription: #{subscription.inspect}"
if subscription.valid?
logger.debug "Subscribed: #{subscription.inspect}"
status 201
json subscription
else
client_error(sub, 422)
client_error(subscription, 422)
end
rescue ActiveRecord::RecordNotUnique
logger.error "Already subscribed"
logger.error 'Already subscribed'
status 422
json errors: {email: ['is already subscribed']}
end
end
get '/configurable_attributes.json' do
json(Subscription.configurable_attributes) + "\n"
get '/:list_email/subscriptions/configurable_attributes.json' do
json(subscriptions_controller.get_configurable_attributes) + "\n"
end
get '/new.json' do
json Subscription.new
get '/:list_email/subscriptions/new.json' do
json subscriptions_controller.new_subscription
end
get '/:id.json' do |id|
json subscription(id)
get '/:list_email/subscriptions/:email.json' do |list_email, email|
json subscriptions_controller.find(list_email, email)
end
put '/:id.json' do |id|
sub = subscription(id)
list = sub.list
args = find_attributes_from_body(%w[email fingerprint admin delivery_enabled])
fingerprint, messages = list.import_key_and_find_fingerprint(find_key_material)
set_x_messages(messages)
# For an already existing subscription, only update fingerprint if a
# new one has been selected from the upload.
if fingerprint.present?
args["fingerprint"] = fingerprint
put '/:list_email/subscriptions/:email.json' do |list_email, email|
attributes = find_attributes_from_body(subscriptions_controller.get_configurable_attributes)
required_parameters = subscriptions_controller.get_configurable_attributes
if attributes.keys.sort != required_parameters.sort
status 422
return json(errors: 'The request is missing a required parameter')
end
if sub.update(args)
subscription = subscriptions_controller.update(list_email, email, parsed_body)
if subscription.valid?
200
else
client_error(sub, 422)
client_error(subscription)
end
end
patch '/:id.json' do |id|
sub = subscription(id)
if sub.update(parsed_body)
patch '/:list_email/subscriptions/:email.json' do |list_email, email|
subscription = subscriptions_controller.update(list_email, email, parsed_body)
if subscription.valid?
200
else
client_error(sub)
client_error(subscription)
end
end
delete '/:id.json' do |id|
if sub = subscription(id).destroy
delete '/:list_email/subscriptions/:email.json' do |list_email, email|
subscription = subscriptions_controller.delete(list_email, email)
if subscription
200
else
client_error(sub)
client_error(subscription)
end
end
end
private
def subscriptions_controller
Schleuder::SubscriptionsController.new(current_account)
end
def lists_controller
Schleuder::ListsController.new(current_account)
end
end
......@@ -10,6 +10,8 @@ require 'open3'
# Require mandatory libs. The database-layer-lib is required below.
require 'mail-gpg'
require 'active_record'
require 'active_support'
require 'active_support/core_ext/string'
# An extra from mail-gpg
require 'hkp'
......@@ -41,10 +43,9 @@ require 'schleuder/version'
require 'schleuder/logger_notifications'
require 'schleuder/logger'
require 'schleuder/listlogger'
require 'schleuder/plugin_runners/base'
require 'schleuder/plugin_runners/list_plugins_runner'
require 'schleuder/plugin_runners/request_plugins_runner'
Dir["#{libdir}/schleuder/plugins/*.rb"].each do |file|
require 'schleuder/keyword_handlers_runner'
require 'schleuder/keyword_handlers/base'
Dir["#{libdir}/schleuder/keyword_handlers/*.rb"].each do |file|
require file
end
require 'schleuder/filters_runner'
......@@ -55,12 +56,24 @@ require 'schleuder/runner'
require 'schleuder/list'
require 'schleuder/list_builder'
require 'schleuder/subscription'
require 'schleuder/account'
require 'schleuder/authorizer_policies/base_policy'
require 'schleuder/authorizer_policies/subscription_policy'
require 'schleuder/authorizer_policies/list_policy'
require 'schleuder/authorizer_policies/key_policy'
require 'schleuder/authorizer'
require 'schleuder/controllers/base_controller'
require 'schleuder/controllers/keys_controller'
require 'schleuder/controllers/lists_controller'
require 'schleuder/controllers/subscriptions_controller'
# Setup
ENV["SCHLEUDER_CONFIG"] ||= '/etc/schleuder/schleuder.yml'
ENV["SCHLEUDER_LIST_DEFAULTS"] ||= '/etc/schleuder/list-defaults.yml'
ENV["SCHLEUDER_ENV"] ||= 'production'
ENV["SCHLEUDER_ROOT"] = rootdir.to_s
ENV['SCHLEUDER_CONFIG'] ||= '/etc/schleuder/schleuder.yml'
ENV['SCHLEUDER_LIST_DEFAULTS'] ||= '/etc/schleuder/list-defaults.yml'
ENV['SCHLEUDER_ENV'] ||= 'production'
ENV['SCHLEUDER_ROOT'] = rootdir.to_s
GPGME::Ctx.set_gpg_path_from_env
GPGME::Ctx.check_gpg_version
......
module Schleuder
class Account < ActiveRecord::Base
PASSWORD_CHARS = [
('a'..'z').to_a,
('A'..'Z').to_a,
(0..9).to_a,
%w[! @ # $ % ^ & * ( ) _ - + = { [ } ] : ; < , > . ? /]
].flatten
PASSWORD_LENGTH_RANGE = (10..12).freeze
has_secure_password
has_many :subscriptions, foreign_key: 'email', primary_key: 'email'
validates :email, presence: true, email: true, uniqueness: true, allow_nil: false
validates :password, presence: true, allow_nil: false
before_save { email.downcase! }
def lists
List.joins(:subscriptions).where(subscriptions: {email: email})
end
def admin_lists
List.joins(:subscriptions).where(subscriptions: {email: email, admin: true})
end
def admin_list_subscriptions
Subscription.where(list_id: admin_lists.pluck(:id))
end
def set_new_password!
new_password = generate_password
update!(password: new_password)
new_password
end
def subscribed_to_list?(list)
subscriptions.where(list_id: list.id).exists?
end
def admin_of_list?(list)
list.admins.where(email: email).exists?
end
def authorize!(resource, action)
authorizer.authorize!(resource, action)
end
def scoped(resource)
authorizer.scoped(resource)
end
private
def authorizer
@authorizer ||= Authorizer.new(self)
end
def generate_password
length = rand(PASSWORD_LENGTH_RANGE)
PASSWORD_CHARS.shuffle.take(length).join
end
end
end
module Schleuder
class Authorizer
attr_reader :account
def initialize(account)
@policies = {}
@policy_scopes = {}
@account = account
end
def authorize!(resource, action)
raise Errors::ResourceNotFound.new if resource.nil?
action = action.to_s
action << '?' unless action.last == '?'
policy(resource).public_send(action) || raise(Errors::Unauthorized.new(resource))
end
def scoped(klass)
policy_scope(klass)
end
private
def policy(resource)
@policies[resource] ||= find_policy(resource)
end
def find_policy(resource)
find_policy_class(resource).new(account, resource)
end
def find_policy_class(resource)
klass_name = infer_class_name(resource)
"AuthorizerPolicies::#{klass_name}Policy".constantize
rescue NameError
raise "No policy-class found for #{resource.inspect}"
end
def policy_scope(klass)
@policy_scopes[klass] ||= find_policy_scope(klass).resolve
end
def find_policy_scope(klass)
find_policy_scope_class(klass).new(account)
end
def find_policy_scope_class(klass)
class_name = find_policy_class(klass)
if class_name
class_name::Scope
end
end
def infer_class_name(resource)
if resource.respond_to?(:model_name)
resource.model_name
elsif resource.class.respond_to?(:model_name)
resource.class.model_name
elsif resource.is_a?(Class)
resource
elsif resource.is_a?(Symbol)
resource.to_s.camelize
else
resource.class
end.to_s.split('::').last
end
end
end
module Schleuder
module AuthorizerPolicies
class BasePolicy
attr_reader :account, :object
class BaseScope
attr_reader :account
def initialize(account)
@account = account
end
end
def initialize(account, object)
@account = account
@object = object
end
private
def admin?(list)
account.admin_of_list?(list)
end
def superadmin?
account.api_superadmin?
end
# This includes list-admins.
def subscribed?(list)
account.subscribed_to_list?(list)
end
end
end
end
module Schleuder
module AuthorizerPolicies
class KeyPolicy < BasePolicy
# list is not defined: it must be checked via ListPolicy#list_keys (because we need the list-context).
def read?
superadmin? || subscribed?(object.list)
end
# create is not defined: it must be checked via ListPolicy#add_keys (because we need the list-context).
# update is not defined: we can't update keys.
def delete?
superadmin? || admin?(object.list)
end
end
end
end
module Schleuder
module AuthorizerPolicies
class ListPolicy < BasePolicy
class Scope < BaseScope
def resolve
if account.api_superadmin?
List.all
else
account.lists
end
end
end
def list?
true
end
def read?
superadmin? || subscribed?(object)
end
def update?
superadmin? || admin?(object)
end
def create?
superadmin?
end
def delete?
superadmin?
end
def list_subscriptions?
superadmin? || admin?(object)
end
def subscribe?
superadmin? || admin?(object)
end
def list_keys?
superadmin? || subscribed?(object)
end
def add_keys?
superadmin? || subscribed?(object)