active_url 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'yaml'
4
+
5
+ begin
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gem|
8
+ gem.name = "active_url"
9
+ gem.summary = %Q{A Rails library for generating secret URLs.}
10
+ gem.description = <<-EOF
11
+ ActiveUrl enables the storing of a model in an encrypted URL. It facilitates implementation
12
+ of secret URLs for user (e.g. feed URLs) that can be accessed without logging in, and URLs
13
+ for confirming the email address of a new user.
14
+ EOF
15
+ gem.email = "mdholling@gmail.com"
16
+ gem.homepage = "http://github.com/mholling/active_url"
17
+ gem.authors = ["Matthew Hollingworth"]
18
+ gem.add_dependency 'activerecord'
19
+ gem.has_rdoc = false
20
+
21
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
22
+ end
23
+ Jeweler::GemcutterTasks.new
24
+ rescue LoadError
25
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
26
+ end
27
+
28
+ require 'spec/rake/spectask'
29
+ Spec::Rake::SpecTask.new(:spec) do |spec|
30
+ spec.libs << 'lib' << 'spec'
31
+ spec.spec_files = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
35
+ spec.libs << 'lib' << 'spec'
36
+ spec.pattern = 'spec/**/*_spec.rb'
37
+ spec.rcov = true
38
+ end
39
+
40
+
41
+ task :default => :spec
42
+
43
+ require 'rake/rdoctask'
44
+ Rake::RDocTask.new do |rdoc|
45
+ if File.exist?('VERSION.yml')
46
+ config = YAML.load(File.read('VERSION.yml'))
47
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
48
+ else
49
+ version = ""
50
+ end
51
+
52
+ rdoc.rdoc_dir = 'rdoc'
53
+ rdoc.title = "active_url #{version}"
54
+ rdoc.rdoc_files.include('README*')
55
+ rdoc.rdoc_files.include('lib/**/*.rb')
56
+ end
57
+
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 4
3
+ :major: 0
4
+ :minor: 1
@@ -0,0 +1,72 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{active_url}
8
+ s.version = "0.1.4"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Matthew Hollingworth"]
12
+ s.date = %q{2009-10-10}
13
+ s.description = %q{ ActiveUrl enables the storing of a model in an encrypted URL. It facilitates implementation
14
+ of secret URLs for user (e.g. feed URLs) that can be accessed without logging in, and URLs
15
+ for confirming the email address of a new user.
16
+ }
17
+ s.email = %q{mdholling@gmail.com}
18
+ s.extra_rdoc_files = [
19
+ "LICENSE",
20
+ "README.textile"
21
+ ]
22
+ s.files = [
23
+ ".document",
24
+ ".gitignore",
25
+ "LICENSE",
26
+ "README.textile",
27
+ "Rakefile",
28
+ "VERSION.yml",
29
+ "active_url.gemspec",
30
+ "lib/active_url.rb",
31
+ "lib/active_url/base.rb",
32
+ "lib/active_url/belongs_to.rb",
33
+ "lib/active_url/callbacks.rb",
34
+ "lib/active_url/configuration.rb",
35
+ "lib/active_url/crypto.rb",
36
+ "lib/active_url/errors.rb",
37
+ "lib/active_url/validations.rb",
38
+ "rails/init.rb",
39
+ "spec/belongs_to_spec.rb",
40
+ "spec/callbacks_spec.rb",
41
+ "spec/crypto_spec.rb",
42
+ "spec/instance_spec.rb",
43
+ "spec/spec_helper.rb",
44
+ "spec/validations_spec.rb"
45
+ ]
46
+ s.homepage = %q{http://github.com/mholling/active_url}
47
+ s.rdoc_options = ["--charset=UTF-8"]
48
+ s.require_paths = ["lib"]
49
+ s.rubygems_version = %q{1.3.5}
50
+ s.summary = %q{A Rails library for generating secret URLs.}
51
+ s.test_files = [
52
+ "spec/belongs_to_spec.rb",
53
+ "spec/callbacks_spec.rb",
54
+ "spec/crypto_spec.rb",
55
+ "spec/instance_spec.rb",
56
+ "spec/spec_helper.rb",
57
+ "spec/validations_spec.rb"
58
+ ]
59
+
60
+ if s.respond_to? :specification_version then
61
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
62
+ s.specification_version = 3
63
+
64
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
65
+ s.add_runtime_dependency(%q<activerecord>, [">= 0"])
66
+ else
67
+ s.add_dependency(%q<activerecord>, [">= 0"])
68
+ end
69
+ else
70
+ s.add_dependency(%q<activerecord>, [">= 0"])
71
+ end
72
+ end
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+ require 'active_record'
3
+ ActiveRecord::Base # hack to get ActiveRecord::Validations to load..?
4
+
5
+ require 'active_url/errors'
6
+ require 'active_url/configuration'
7
+ require 'active_url/crypto'
8
+ require 'active_url/belongs_to'
9
+ require 'active_url/validations'
10
+ require 'active_url/callbacks'
11
+ require 'active_url/base'
12
+
13
+ # module ActiveUrl
14
+ # autoload :Base, 'active_url/base'
15
+ # autoload :Configuration, 'active_url/configuration'
16
+ # autoload :Crypto, 'active_url/crypto'
17
+ # autoload :BelongsTo, 'active_url/belongs_to'
18
+ # autoload :Validations, 'active_url/validations'
19
+ # autoload :Callbacks, 'active_url/callbacks'
20
+ # end
@@ -0,0 +1,103 @@
1
+ module ActiveUrl
2
+ class RecordNotFound < ActiveUrlError
3
+ end
4
+
5
+ class Base
6
+ class_inheritable_reader :attribute_names
7
+ class_inheritable_reader :accessible_attributes
8
+
9
+ def self.attribute(*attribute_names)
10
+ options = attribute_names.extract_options!
11
+ attribute_names.map(&:to_sym).each { |attribute_name| add_attribute(attribute_name, options) }
12
+ end
13
+
14
+ def self.attr_accessible(*attribute_names)
15
+ self.accessible_attributes += attribute_names.map(&:to_sym)
16
+ end
17
+
18
+ attr_reader :id
19
+
20
+ def initialize(attributes = nil)
21
+ attributes ||= {}
22
+ self.attributes = attributes
23
+ end
24
+
25
+ def attributes=(attributes)
26
+ attributes.symbolize_keys.select do |key, value|
27
+ self.class.accessible_attributes.include? key
28
+ end.map do |key, value|
29
+ [ "#{key}=", value ]
30
+ end.each do |setter, value|
31
+ send setter, value if respond_to? setter
32
+ end
33
+ end
34
+
35
+ def attributes
36
+ attribute_names.inject({}) do |hash, name|
37
+ hash.merge(name => send(name))
38
+ end
39
+ end
40
+
41
+ def create
42
+ serialized = [ self.class.to_s, attributes ].to_yaml
43
+ @id = Crypto.encrypt(serialized)
44
+ end
45
+
46
+ def save
47
+ !create.blank?
48
+ end
49
+
50
+ def save!
51
+ save
52
+ end
53
+
54
+ def self.find(id)
55
+ raise RecordNotFound unless id.is_a?(String) && !id.blank?
56
+ serialized = begin
57
+ Crypto.decrypt(id)
58
+ rescue Crypto::CipherError
59
+ raise RecordNotFound
60
+ end
61
+ type, attributes = YAML.load(serialized)
62
+ raise RecordNotFound unless type == self.to_s && attributes.is_a?(Hash)
63
+ active_url = new
64
+ attributes.each { |key, value| active_url.send "#{key}=", value }
65
+ active_url.create
66
+ active_url
67
+ rescue RecordNotFound
68
+ raise RecordNotFound.new("Couldn't find #{self.name} with id=#{id}")
69
+ end
70
+
71
+ def to_param
72
+ @id.to_s
73
+ end
74
+
75
+ def new_record?
76
+ @id.nil?
77
+ end
78
+
79
+ def ==(other)
80
+ attributes == other.attributes && self.class == other.class
81
+ end
82
+
83
+ private
84
+
85
+ class_inheritable_writer :attribute_names
86
+ class_inheritable_writer :accessible_attributes
87
+ self.attribute_names = Set.new
88
+ self.accessible_attributes = Set.new
89
+
90
+ def self.add_attribute(attribute_name, options)
91
+ self.attribute_names << attribute_name
92
+ self.accessible_attributes << attribute_name if options[:accessible]
93
+ public
94
+ attr_accessor attribute_name
95
+ end
96
+ end
97
+
98
+ Base.class_eval do
99
+ extend BelongsTo
100
+ include Validations
101
+ include Callbacks
102
+ end
103
+ end
@@ -0,0 +1,32 @@
1
+ module ActiveUrl
2
+ module BelongsTo
3
+ def belongs_to(object_name)
4
+ begin
5
+ object_name.to_s.classify.constantize
6
+
7
+ attribute_name = "#{object_name}_id"
8
+ attribute attribute_name
9
+
10
+ define_method object_name do
11
+ begin
12
+ object_name.to_s.classify.constantize.find(send(attribute_name))
13
+ rescue ActiveRecord::RecordNotFound
14
+ nil
15
+ end
16
+ end
17
+
18
+ define_method "#{object_name}=" do |object|
19
+ if object.nil?
20
+ self.send "#{object_name}_id=", nil
21
+ elsif object.is_a?(object_name.to_s.classify.constantize)
22
+ self.send "#{object_name}_id=", object.id
23
+ else
24
+ raise TypeError.new("object is not of type #{object_name.to_s.classify}")
25
+ end
26
+ end
27
+ rescue NameError
28
+ raise ArgumentError.new("#{object_name.to_s.classify} is not an ActiveRecord class.")
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ module ActiveUrl
2
+ module Callbacks
3
+ def save_with_callbacks
4
+ returning(save_without_callbacks) do |result|
5
+ run_callbacks(:after_save) if result
6
+ end
7
+ end
8
+
9
+ def self.included(base)
10
+ base.class_eval do
11
+ include ActiveSupport::Callbacks # Already included by ActiveRecord.
12
+ alias_method_chain :save, :callbacks
13
+ define_callbacks :after_save
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveUrl
2
+ module Config
3
+ mattr_accessor :secret
4
+ end
5
+ end
@@ -0,0 +1,34 @@
1
+ require 'openssl'
2
+ require 'digest/sha2'
3
+ require 'base64'
4
+
5
+ module ActiveUrl
6
+ module Crypto
7
+ CipherError = OpenSSL::Cipher.const_defined?(:CipherError) ? OpenSSL::Cipher::CipherError : OpenSSL::CipherError
8
+
9
+ PADDING = { 2 => "==", 3 => "=" }
10
+
11
+ def self.encrypt(clear)
12
+ crypto = start(:encrypt)
13
+ cipher = crypto.update(clear)
14
+ cipher << crypto.final
15
+ Base64.encode64(cipher).gsub(/[\s=]+/, "").gsub("+", "-").gsub("/", "_")
16
+ end
17
+
18
+ def self.decrypt(b64)
19
+ cipher = Base64.decode64("#{b64.gsub("-", "+").gsub("_", "/")}#{PADDING[b64.length % 4]}")
20
+ crypto = start(:decrypt)
21
+ clear = crypto.update(cipher)
22
+ clear << crypto.final
23
+ end
24
+
25
+ private
26
+
27
+ def self.start(mode)
28
+ raise ::ArgumentError.new("Set a secret key using ActiveUrl::Config.secret = 'your-secret'") if ActiveUrl::Config.secret.blank?
29
+ crypto = OpenSSL::Cipher::Cipher.new('aes-256-ecb').send(mode)
30
+ crypto.key = Digest::SHA256.hexdigest(ActiveUrl::Config.secret)
31
+ return crypto
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,4 @@
1
+ module ActiveUrl
2
+ class ActiveUrlError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,57 @@
1
+ module ActiveUrl
2
+ class RecordInvalid < ActiveUrlError
3
+ attr_reader :record
4
+ def initialize(record)
5
+ @record = record
6
+ super("Validation failed: #{@record.errors.full_messages.join(", ")}")
7
+ end
8
+ end
9
+
10
+ module Validations
11
+ module ClassMethods
12
+ def self_and_descendants_from_active_record
13
+ [self]
14
+ end
15
+ alias_method :self_and_descendents_from_active_record, :self_and_descendants_from_active_record
16
+
17
+ def human_name()
18
+ end
19
+
20
+ def human_attribute_name(name, options = {})
21
+ name.to_s.humanize
22
+ end
23
+
24
+ def find_with_validation(id)
25
+ active_url = find_without_validation(id)
26
+ raise ActiveUrl::RecordNotFound unless active_url.valid?
27
+ active_url
28
+ end
29
+
30
+ private
31
+
32
+ def add_attribute_with_validation(attribute_name, options)
33
+ add_attribute_without_validation(attribute_name, options)
34
+ alias_method "#{attribute_name}_before_type_cast", attribute_name
35
+ end
36
+
37
+ end
38
+
39
+ def save_with_active_url_exception!
40
+ save_without_active_url_exception!
41
+ rescue ActiveRecord::RecordInvalid => e
42
+ raise ActiveUrl::RecordInvalid.new(e.record)
43
+ end
44
+
45
+ def self.included(base)
46
+ base.class_eval do
47
+ extend ClassMethods
48
+ include ActiveRecord::Validations
49
+ alias_method_chain :save!, :active_url_exception
50
+ class << self
51
+ alias_method_chain :find, :validation
52
+ alias_method_chain :add_attribute, :validation
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1 @@
1
+ require 'active_url'
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveUrl do
4
+ before(:each) do
5
+ ActiveUrl::Config.stub!(:secret).and_return("secret")
6
+ end
7
+
8
+ context "instance with belongs_to association" do
9
+ before(:all) do
10
+ # a simple pretend-ActiveRecord model for testing belongs_to without setting up a db:
11
+ class ::User < ActiveRecord::Base
12
+ def self.columns() @columns ||= []; end
13
+ def self.column(name, sql_type = nil, default = nil, null = true)
14
+ columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
15
+ end
16
+ end
17
+
18
+ class ::Secret < ActiveUrl::Base
19
+ belongs_to :user
20
+ end
21
+ end
22
+
23
+ after(:all) do
24
+ Object.send(:remove_const, "Secret")
25
+ end
26
+
27
+ before(:each) do
28
+ @url = Secret.new
29
+ @user = User.new
30
+ @user.stub!(:id).and_return(1)
31
+ end
32
+
33
+ it "should raise ArgumentError if the association name is not an ActiveRecord class" do
34
+ lambda { Secret.belongs_to :foo }.should raise_error(ArgumentError)
35
+ end
36
+
37
+ it "should respond to association_id, association_id=, association & association=" do
38
+ @url.attribute_names.should include(:user_id)
39
+ @url.should respond_to(:user)
40
+ @url.should respond_to(:user=)
41
+ end
42
+
43
+ it "should have nil association if association or association_id not set" do
44
+ @url.user.should be_nil
45
+ end
46
+
47
+ it "should not allow mass assignment of association_id" do
48
+ @url = Secret.new(:user_id => @user.id)
49
+ @url.user_id.should be_nil
50
+ @url.user.should be_nil
51
+ end
52
+
53
+ it "should not allow mass assignment of association" do
54
+ @url = Secret.new(:user => @user)
55
+ @url.user_id.should be_nil
56
+ @url.user.should be_nil
57
+ end
58
+
59
+ it "should be able to have its association set to nil" do
60
+ @url.user_id = @user.id
61
+ @url.user = nil
62
+ @url.user_id.should be_nil
63
+ end
64
+
65
+ it "should raise ArgumentError if association is set to wrong type" do
66
+ lambda { @url.user = Object.new }.should raise_error(TypeError)
67
+ end
68
+
69
+ it "should find its association_id if association is set" do
70
+ @url.user = @user
71
+ @url.user_id.should == @user.id
72
+ end
73
+
74
+ it "should find its association if association_id is set" do
75
+ User.should_receive(:find).with(@user.id).and_return(@user)
76
+ @url.user_id = @user.id
77
+ @url.user.should == @user
78
+ end
79
+
80
+ it "should return nil association if association_id is unknown" do
81
+ User.should_receive(:find).and_raise(ActiveRecord::RecordNotFound)
82
+ @url.user_id = 10
83
+ @url.user.should be_nil
84
+ end
85
+
86
+ it "should know its association when found by id" do
87
+ User.should_receive(:find).with(@user.id).and_return(@user)
88
+ @url.user_id = @user.id
89
+ @url.save
90
+ @found = Secret.find(@url.id)
91
+ @found.user.should == @user
92
+ end
93
+ end
94
+ end