From 9f4b1bcf315f09fd6d302ad187281ec4ed443f04 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 17 Oct 2013 12:05:26 +0200 Subject: blacklist system logins for aliases and logins We blacklist based on three things: * blacklist in APP_CONFIG[:handle_blacklist] * emails in RFC 2142 * usernames in /etc/passwd The latter two can be allowed by explicitly whitelisting them in APP_CONFIG[:handle_whitelist]. We stick to blocking names that have been configured as both blacklisted and whitelisted - better be save than sorry. --- users/app/models/local_email.rb | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) (limited to 'users/app/models') diff --git a/users/app/models/local_email.rb b/users/app/models/local_email.rb index 6303bb6..2b4c65e 100644 --- a/users/app/models/local_email.rb +++ b/users/app/models/local_email.rb @@ -1,5 +1,10 @@ class LocalEmail < Email + BLACKLIST_FROM_RFC2142 = [ + 'postmaster', 'hostmaster', 'domainadmin', 'webmaster', 'www', + 'abuse', 'noc', 'security', 'usenet', 'news', 'uucp', + 'ftp', 'sales', 'marketing', 'support', 'info' + ] def self.domain APP_CONFIG[:domain] @@ -11,6 +16,8 @@ class LocalEmail < Email :message => "needs to end in @#{domain}" } + validate :handle_allowed + def initialize(s) super append_domain_if_needed @@ -32,4 +39,30 @@ class LocalEmail < Email end end + def handle_allowed + errors.add(:handle, "is reserved.") if handle_reserved? + end + + def handle_reserved? + # *ARRAY in a case statement tests if ARRAY includes the handle. + case handle + when *APP_CONFIG[:handle_blacklist] + true + when *APP_CONFIG[:handle_whitelist] + false + when *BLACKLIST_FROM_RFC2142 + true + else + handle_in_passwd? + end + end + + def handle_in_passwd? + begin + !!Etc.getpwnam(handle) + rescue ArgumentError + # handle was not found + return false + end + end end -- cgit v1.2.3 From b4ca13257341792f5e6496264c421af1888bcdb8 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 5 Nov 2013 11:40:25 +0100 Subject: refactor: Identity.disable_all_for(user) on user destruction This way the identity model defines how identities should be disabled. We currently still destroy them. But it will be easy and nicely isolated to change this next. --- users/app/models/account.rb | 4 +--- users/app/models/identity.rb | 12 ++++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) (limited to 'users/app/models') diff --git a/users/app/models/account.rb b/users/app/models/account.rb index 5368a1b..726f642 100644 --- a/users/app/models/account.rb +++ b/users/app/models/account.rb @@ -29,9 +29,7 @@ class Account def destroy return unless @user - Identity.by_user_id.key(@user.id).each do |identity| - identity.destroy - end + Identity.disable_all_for(@user) @user.destroy end diff --git a/users/app/models/identity.rb b/users/app/models/identity.rb index e0a24e9..c24af73 100644 --- a/users/app/models/identity.rb +++ b/users/app/models/identity.rb @@ -50,6 +50,12 @@ class Identity < CouchRest::Model::Base identity end + def self.disable_all_for(user) + Identity.by_user_id.key(user.id).each do |identity| + identity.disable + end + end + def self.attributes_from_user(user) { user_id: user.id, address: user.email_address, @@ -57,6 +63,12 @@ class Identity < CouchRest::Model::Base } end + # + # about to change towards actually disabling the identity instead of + # destroying it. + # + alias_method :disable, :destroy + def keys read_attribute('keys') || HashWithIndifferentAccess.new end -- cgit v1.2.3 From 99ecdbf71632970d4c83f99beea325e5d213e4c6 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 5 Nov 2013 12:12:13 +0100 Subject: disabled identities to block handles after a user was deleted --- users/app/models/identity.rb | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) (limited to 'users/app/models') diff --git a/users/app/models/identity.rb b/users/app/models/identity.rb index c24af73..40ce4ae 100644 --- a/users/app/models/identity.rb +++ b/users/app/models/identity.rb @@ -53,6 +53,7 @@ class Identity < CouchRest::Model::Base def self.disable_all_for(user) Identity.by_user_id.key(user.id).each do |identity| identity.disable + identity.save end end @@ -63,11 +64,14 @@ class Identity < CouchRest::Model::Base } end - # - # about to change towards actually disabling the identity instead of - # destroying it. - # - alias_method :disable, :destroy + def enabled? + self.destination && self.user_id + end + + def disable + self.destination = nil + self.user_id = nil + end def keys read_attribute('keys') || HashWithIndifferentAccess.new @@ -105,7 +109,8 @@ class Identity < CouchRest::Model::Base end def destination_email - return if destination.valid? #this ensures it is Email + return if destination.nil? # this identity is disabled + return if destination.valid? # this ensures it is Email self.errors.add(:destination, destination.errors.messages[:email].first) #assumes only one error #TODO end -- cgit v1.2.3 From 4a2490cc5eac1803be80fade65bbe9d32fa0bd9b Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 5 Nov 2013 17:03:55 +0100 Subject: Identity.destroy_all_disabled will clean up disabled identities This is mostly for cleaning up after tests so far. But we might expand this to destroy all identities disabled before a certain date. --- users/app/models/identity.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'users/app/models') diff --git a/users/app/models/identity.rb b/users/app/models/identity.rb index 40ce4ae..97966d0 100644 --- a/users/app/models/identity.rb +++ b/users/app/models/identity.rb @@ -27,6 +27,17 @@ class Identity < CouchRest::Model::Base emit(doc.address, doc.keys["pgp"]); } EOJS + view :disabled, + map: <<-EOJS + function(doc) { + if (doc.type != 'Identity') { + return; + } + if (typeof doc.user_id === "undefined") { + emit(doc._id, 1); + } + } + EOJS end @@ -57,6 +68,12 @@ class Identity < CouchRest::Model::Base end end + def self.destroy_all_disabled + Identity.disabled.each do |identity| + identity.destroy + end + end + def self.attributes_from_user(user) { user_id: user.id, address: user.email_address, -- cgit v1.2.3 From 24598b5c5e4df20c423ec74ea8e9df1592483c6b Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 6 Nov 2013 11:26:46 +0100 Subject: destroy all tickets created by a user when account is destroyed In order to keep the users engine independent of the tickets engine i added a generic load hook to the account model. The tickets engine then monkeypatches the account destruction and destroys all tickets before the user is destroyed. The tickets are destroyed first so that even if things break there should never be tickets with an outdated user id. I would have prefered to use super over using an alias_method_chain but I have not been able to figure out a way to make account a superclass of the account extension and still refer to Account from the users engine. --- users/app/models/account.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'users/app/models') diff --git a/users/app/models/account.rb b/users/app/models/account.rb index 726f642..5c943bb 100644 --- a/users/app/models/account.rb +++ b/users/app/models/account.rb @@ -1,5 +1,10 @@ # -# A Composition of a User record and it's identity records. +# The Account model takes care of the livecycle of a user. +# It composes a User record and it's identity records. +# It also allows for other engines to hook into the livecycle by +# monkeypatching the create, update and destroy methods. +# There's an ActiveSupport load_hook at the end of this file to +# make this more easy. # class Account @@ -52,4 +57,7 @@ class Account @new_identity.try(:save) && @old_identity.try(:save) end + # You can hook into the account lifecycle from different engines using + # ActiveSupport.on_load(:account) do ... + ActiveSupport.run_load_hooks(:account, self) end -- cgit v1.2.3 From e2c0962077cf759b23639276cca42606ea2135ec Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 7 Nov 2013 23:27:27 +0100 Subject: Token.destroy_all_expired to cleanup expired tokens (#4411) --- users/app/models/token.rb | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) (limited to 'users/app/models') diff --git a/users/app/models/token.rb b/users/app/models/token.rb index dd87344..bf9b0d0 100644 --- a/users/app/models/token.rb +++ b/users/app/models/token.rb @@ -11,6 +11,24 @@ class Token < CouchRest::Model::Base validates :user_id, presence: true + design do + view :by_last_seen_at + end + + def self.expires_after + APP_CONFIG[:auth] && APP_CONFIG[:auth][:token_expires_after] + end + + def self.expired + self.by_last_seen_at.endkey(expires_after.minutes.ago) + end + + def self.destroy_all_expired + self.expired.each do |token| + token.destroy + end + end + def authenticate if expired? destroy @@ -27,21 +45,16 @@ class Token < CouchRest::Model::Base end def expired? - expires_after and - last_seen_at + expires_after.minutes < Time.now - end - - def expires_after - APP_CONFIG[:auth] && APP_CONFIG[:auth][:token_expires_after] + Token.expires_after and + last_seen_at < Token.expires_after.minutes.ago end def initialize(*args) super - self.id = SecureRandom.urlsafe_base64(32).gsub(/^_*/, '') - self.last_seen_at = Time.now - end - - design do + if new_record? + self.id = SecureRandom.urlsafe_base64(32).gsub(/^_*/, '') + self.last_seen_at = Time.now + end end end -- cgit v1.2.3 From a7cd2ef0877e79302f27fb175384a0cf4ded52d9 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 7 Nov 2013 23:36:37 +0100 Subject: fix cornercase of non expiring tokens --- users/app/models/token.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'users/app/models') diff --git a/users/app/models/token.rb b/users/app/models/token.rb index bf9b0d0..001eb40 100644 --- a/users/app/models/token.rb +++ b/users/app/models/token.rb @@ -20,7 +20,8 @@ class Token < CouchRest::Model::Base end def self.expired - self.by_last_seen_at.endkey(expires_after.minutes.ago) + return [] unless expires_after + by_last_seen_at.endkey(expires_after.minutes.ago) end def self.destroy_all_expired -- cgit v1.2.3 From 8e9b65b01bbd9d44d4077d94f2dc4ac375cf8e85 Mon Sep 17 00:00:00 2001 From: jessib Date: Mon, 18 Nov 2013 15:44:54 -0800 Subject: Start of service level code, which will be tweaked * stores desired & effective service level * whenever desired level is changed, effective level will be updated * allows user to set their desired service level * allow admin to update desired & effective service level --- users/app/models/unauthenticated_user.rb | 2 ++ users/app/models/user.rb | 12 ++++++++++++ 2 files changed, 14 insertions(+) (limited to 'users/app/models') diff --git a/users/app/models/unauthenticated_user.rb b/users/app/models/unauthenticated_user.rb index 99a6874..0fc17d2 100644 --- a/users/app/models/unauthenticated_user.rb +++ b/users/app/models/unauthenticated_user.rb @@ -1,4 +1,6 @@ # The nil object for the user class class UnauthenticatedUser < Object + # will probably want something here to return service level as APP_CONFIG[:service_levels][0] but not sure how will be accessing. + end diff --git a/users/app/models/user.rb b/users/app/models/user.rb index a14fcb5..35212a1 100644 --- a/users/app/models/user.rb +++ b/users/app/models/user.rb @@ -9,6 +9,12 @@ class User < CouchRest::Model::Base property :enabled, TrueClass, :default => true + # these will be null by default. should we set to APP_CONFIG[:default_service_level] by default, or have code assume that until these get set?: + property :desired_service_level, Integer, :accessible => true + property :effective_service_level, Integer, :accessible => true + + before_save :update_effective_service_level + validates :login, :password_salt, :password_verifier, :presence => true @@ -116,4 +122,10 @@ class User < CouchRest::Model::Base def serverside? true end + + def update_effective_service_level + if self.desired_service_level_changed? + self.effective_service_level = self.desired_service_level + end + end end -- cgit v1.2.3 From 7de12c71ce7eb4eeb6e0795275434ed4a4120c25 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 26 Nov 2013 11:22:47 +0100 Subject: ignore attempts to empty public_key, refactor refactor: prepare validations of the uploaded pgp keys --- users/app/models/account.rb | 11 ++++++++--- users/app/models/identity.rb | 6 +++--- users/app/models/pgp_key.rb | 25 +++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 users/app/models/pgp_key.rb (limited to 'users/app/models') diff --git a/users/app/models/account.rb b/users/app/models/account.rb index 5c943bb..cf998e4 100644 --- a/users/app/models/account.rb +++ b/users/app/models/account.rb @@ -27,7 +27,8 @@ class Account @user.update_attributes attrs.slice(:password_verifier, :password_salt) end # TODO: move into identity controller - update_pgp_key(attrs[:public_key]) if attrs.has_key? :public_key + key = update_pgp_key(attrs[:public_key]) + @user.errors.set :public_key, key.errors.full_messages @user.save && save_identities @user.refresh_identity end @@ -49,8 +50,12 @@ class Account end def update_pgp_key(key) - @new_identity ||= Identity.for(@user) - @new_identity.set_key(:pgp, key) + PgpKey.new(key).tap do |key| + if key.present? && key.valid? + @new_identity ||= Identity.for(@user) + @new_identity.set_key(:pgp, key) + end + end end def save_identities diff --git a/users/app/models/identity.rb b/users/app/models/identity.rb index 97966d0..cbb540e 100644 --- a/users/app/models/identity.rb +++ b/users/app/models/identity.rb @@ -94,9 +94,9 @@ class Identity < CouchRest::Model::Base read_attribute('keys') || HashWithIndifferentAccess.new end - def set_key(type, value) - return if keys[type] == value - write_attribute('keys', keys.merge(type => value)) + def set_key(type, key) + return if keys[type] == key.to_s + write_attribute('keys', keys.merge(type => key.to_s)) end # for LoginFormatValidation diff --git a/users/app/models/pgp_key.rb b/users/app/models/pgp_key.rb new file mode 100644 index 0000000..fddec1e --- /dev/null +++ b/users/app/models/pgp_key.rb @@ -0,0 +1,25 @@ +class PgpKey + include ActiveModel::Validations + + # mostly for testing. + attr_accessor :key_block + + def initialize(key_block = nil) + @key_block = key_block + end + + def to_s + @key_block + end + + def present? + @key_block.present? + end + + # let's allow comparison with plain key_block strings. + def ==(other) + self.equal?(other) or + self.to_s == other + end + +end -- cgit v1.2.3 From e34141c3265c6daeda92bcb83fa508de00551bc3 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 26 Nov 2013 14:39:42 +0100 Subject: simple validation for pgp key format --- users/app/models/pgp_key.rb | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) (limited to 'users/app/models') diff --git a/users/app/models/pgp_key.rb b/users/app/models/pgp_key.rb index fddec1e..66f8660 100644 --- a/users/app/models/pgp_key.rb +++ b/users/app/models/pgp_key.rb @@ -1,25 +1,48 @@ class PgpKey include ActiveModel::Validations + KEYBLOCK_IDENTIFIERS = [ + '-----BEGIN PGP PUBLIC KEY BLOCK-----', + '-----END PGP PUBLIC KEY BLOCK-----', + ] + # mostly for testing. - attr_accessor :key_block + attr_accessor :keyblock + + validate :validate_keyblock_format - def initialize(key_block = nil) - @key_block = key_block + def initialize(keyblock = nil) + @keyblock = keyblock end def to_s - @key_block + @keyblock end def present? - @key_block.present? + @keyblock.present? end - # let's allow comparison with plain key_block strings. + # allow comparison with plain keyblock strings. def ==(other) self.equal?(other) or - self.to_s == other + # relax the comparison on line ends. + self.to_s.tr_s("\n\r", '') == other.tr_s("\r\n", '') + end + + protected + + def validate_keyblock_format + if keyblock_identifier_missing? + errors.add :public_key_block, + "does not look like an armored pgp public key block" + end + end + + def keyblock_identifier_missing? + KEYBLOCK_IDENTIFIERS.find do |identify| + !@keyblock.include?(identify) + end end end -- cgit v1.2.3 From 35761333404e3cc2c93bca23036d0fd8e47fd10b Mon Sep 17 00:00:00 2001 From: jessib Date: Tue, 3 Dec 2013 12:17:51 -0800 Subject: Add ServiceLevel class to wrap config and give accessors. Has some hacky parts, but seems like okay generic start for now. --- users/app/models/service_level.rb | 31 +++++++++++++++++++++++++++++++ users/app/models/user.rb | 22 +++++++++++++++++----- 2 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 users/app/models/service_level.rb (limited to 'users/app/models') diff --git a/users/app/models/service_level.rb b/users/app/models/service_level.rb new file mode 100644 index 0000000..ac5244f --- /dev/null +++ b/users/app/models/service_level.rb @@ -0,0 +1,31 @@ +class ServiceLevel + + def initialize(attributes = {}) + @level = attributes[:level] || APP_CONFIG[:default_service_level] + end + + def level + @level + end + + def name + APP_CONFIG[:service_levels][@level][:name] + end + + def cert_prefix + APP_CONFIG[:service_levels][@level][:cert_prefix] + end + + def description + APP_CONFIG[:service_levels][@level][:description] + end + + def cost + APP_CONFIG[:service_levels][@level][:cost] + end + + def quota + APP_CONFIG[:service_levels][@level][:quota] + end + +end diff --git a/users/app/models/user.rb b/users/app/models/user.rb index 35212a1..621ff4e 100644 --- a/users/app/models/user.rb +++ b/users/app/models/user.rb @@ -9,9 +9,9 @@ class User < CouchRest::Model::Base property :enabled, TrueClass, :default => true - # these will be null by default. should we set to APP_CONFIG[:default_service_level] by default, or have code assume that until these get set?: - property :desired_service_level, Integer, :accessible => true - property :effective_service_level, Integer, :accessible => true + # these will be null by default but we shouldn't ever pull them directly, but only via the methods that will return the full ServiceLevel + property :desired_service_level_code, Integer, :accessible => true + property :effective_service_level_code, Integer, :accessible => true before_save :update_effective_service_level @@ -100,6 +100,16 @@ class User < CouchRest::Model::Base @identity = Identity.for(self) end + def desired_service_level + code = self.desired_service_level_code || APP_CONFIG[:default_service_level] + ServiceLevel.new({level: code}) + end + + def effective_service_level + code = self.effective_service_level_code || self.desired_service_level.level + ServiceLevel.new({level: code}) + end + protected ## @@ -124,8 +134,10 @@ class User < CouchRest::Model::Base end def update_effective_service_level - if self.desired_service_level_changed? - self.effective_service_level = self.desired_service_level + # TODO: Is this always the case? Might there be a situation where the admin has set the effective service level and we don't want it changed to match the desired one? + if self.desired_service_level_code_changed? + self.effective_service_level_code = self.desired_service_level_code end end + end -- cgit v1.2.3 From 7d4a9658c29cad526cfe5c952f71109e8eb304e7 Mon Sep 17 00:00:00 2001 From: jessib Date: Tue, 3 Dec 2013 15:07:11 -0800 Subject: Some simplification of code. --- users/app/models/service_level.rb | 26 +++++++------------------- users/app/models/user.rb | 6 +++--- 2 files changed, 10 insertions(+), 22 deletions(-) (limited to 'users/app/models') diff --git a/users/app/models/service_level.rb b/users/app/models/service_level.rb index ac5244f..299aaf1 100644 --- a/users/app/models/service_level.rb +++ b/users/app/models/service_level.rb @@ -1,31 +1,19 @@ class ServiceLevel def initialize(attributes = {}) - @level = attributes[:level] || APP_CONFIG[:default_service_level] + @id = attributes[:id] || APP_CONFIG[:default_service_level] end - def level - @level + def self.authenticated_select_options + APP_CONFIG[:service_levels].map { |id,config_hash| [config_hash[:description], id] if config_hash[:name] != 'anonymous'}.compact end - def name - APP_CONFIG[:service_levels][@level][:name] + def id + @id end - def cert_prefix - APP_CONFIG[:service_levels][@level][:cert_prefix] - end - - def description - APP_CONFIG[:service_levels][@level][:description] - end - - def cost - APP_CONFIG[:service_levels][@level][:cost] - end - - def quota - APP_CONFIG[:service_levels][@level][:quota] + def config_hash + APP_CONFIG[:service_levels][@id] end end diff --git a/users/app/models/user.rb b/users/app/models/user.rb index 621ff4e..720f5a9 100644 --- a/users/app/models/user.rb +++ b/users/app/models/user.rb @@ -102,12 +102,12 @@ class User < CouchRest::Model::Base def desired_service_level code = self.desired_service_level_code || APP_CONFIG[:default_service_level] - ServiceLevel.new({level: code}) + ServiceLevel.new({id: code}) end def effective_service_level - code = self.effective_service_level_code || self.desired_service_level.level - ServiceLevel.new({level: code}) + code = self.effective_service_level_code || self.desired_service_level.id + ServiceLevel.new({id: code}) end protected -- cgit v1.2.3