attribute_access_controllable 1.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +16 -0
- data/.pairs +7 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +68 -0
- data/Rakefile +12 -0
- data/attribute_access_controllable.gemspec +35 -0
- data/cucumber.yml +1 -0
- data/doc/blog-post.md +212 -0
- data/features/read_only_attributes.feature +12 -0
- data/features/step_definitions/steps.rb +49 -0
- data/features/support/env.rb +7 -0
- data/lib/attribute_access_controllable.rb +35 -0
- data/lib/attribute_access_controllable/spec_support.rb +1 -0
- data/lib/attribute_access_controllable/spec_support/shared_examples.rb +51 -0
- data/lib/attribute_access_controllable/version.rb +3 -0
- data/lib/generators/attribute_access/USAGE +14 -0
- data/lib/generators/attribute_access/attribute_access_generator.rb +36 -0
- data/lib/generators/attribute_access/templates/migration.rb +5 -0
- data/lib/generators/attribute_access/templates/migration_create.rb +9 -0
- data/spec/attribute_access_controllable_spec.rb +52 -0
- data/spec/generators/attribute_access_generator_spec.rb +45 -0
- data/spec/spec_helper.rb +13 -0
- metadata +226 -0
data/.gitignore
ADDED
data/.pairs
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Social Chorus
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
# AttributeAccessControllable
|
2
|
+
|
3
|
+
This gem allows you to control write access at the _attribute_ level, on a per-instance basis. For example, let's say you had a model `Person` that has an attribute, `birthday`, which, for security purposes, once this attribute is set, cannot be changed again (except, perhaps by an administrator with extraordinary privileges). You would want any attempts to change this value to raise a validation error.
|
4
|
+
|
5
|
+
e.g.
|
6
|
+
|
7
|
+
alice = Person.create(:birthday => '12/12/12')
|
8
|
+
=> <Person...>
|
9
|
+
alice.birthday= '1/1/01'
|
10
|
+
=> '1/1/01'
|
11
|
+
alice.save!
|
12
|
+
=> ActiveRecord::RecordInvalid ... "birthday is invalid: birthday is read_only"
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
gem 'attribute_access_controllable'
|
19
|
+
|
20
|
+
And then execute:
|
21
|
+
|
22
|
+
$ bundle
|
23
|
+
|
24
|
+
Or install it yourself as:
|
25
|
+
|
26
|
+
$ gem install attribute_access_controllable
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
Generate a migration for your class
|
31
|
+
|
32
|
+
$ rails generate attribute_access Person
|
33
|
+
$ rake db:migrate
|
34
|
+
|
35
|
+
In your model, add the following:
|
36
|
+
|
37
|
+
class Person < ActiveRecord::Base
|
38
|
+
include AttributeAccessControllable
|
39
|
+
|
40
|
+
Add hooks to mark attributes as read_only:
|
41
|
+
|
42
|
+
before_save :mark_birthday_read_only
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def mark_birthday_read_only
|
47
|
+
attr_read_only(:birthday)
|
48
|
+
end
|
49
|
+
|
50
|
+
If, in the future, you need by-pass the validations, pass `:skip_read_only => true` to the instance's `save` or `save!` methods.
|
51
|
+
|
52
|
+
## RSpec support
|
53
|
+
|
54
|
+
Adding this to your model spec will exercise the feature.
|
55
|
+
|
56
|
+
require 'attribute_access_controllable/spec_support'
|
57
|
+
|
58
|
+
describe Person do
|
59
|
+
it_should_behave_like "it has AttributeAccessControllable", :test_column
|
60
|
+
end
|
61
|
+
|
62
|
+
## Contributing
|
63
|
+
|
64
|
+
1. Fork it
|
65
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
66
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
67
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
68
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
desc "Run all specs"
|
9
|
+
RSpec::Core::RakeTask.new do |t|
|
10
|
+
t.pattern = 'spec/**/*_spec.rb'
|
11
|
+
t.rspec_opts = ["-c", "-f progress"]
|
12
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/attribute_access_controllable/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Social Chorus", "Kenneth Mayer"]
|
6
|
+
gem.email = ["tech@socialchorus.com", "kmayer@bitwrangler.com"]
|
7
|
+
gem.description = %q{Allow attribute-level read/write access control, per instance, of any ActiveModel class.}
|
8
|
+
gem.summary = <<'EOT'
|
9
|
+
This gem allows you to control write access at the _attribute_ level, on a
|
10
|
+
per-instance basis. For example, let's say you had a model `Person` that has
|
11
|
+
an attribute, `birthday`, which, for security purposes, once this attribute is
|
12
|
+
set, cannot be changed again (except, perhaps by an administrator with
|
13
|
+
extraordinary privileges). Any attempts to change this value to
|
14
|
+
raise a validation error.
|
15
|
+
EOT
|
16
|
+
gem.homepage = "https://github.com/halogenguides/Attribute-Access-Controllable"
|
17
|
+
|
18
|
+
gem.files = `git ls-files`.split($\)
|
19
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
20
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
21
|
+
gem.name = "attribute_access_controllable"
|
22
|
+
gem.require_paths = ["lib"]
|
23
|
+
gem.version = AttributeAccessControllable::VERSION
|
24
|
+
|
25
|
+
gem.add_runtime_dependency('activesupport', ['~> 3.2.1'])
|
26
|
+
gem.add_runtime_dependency('activemodel', ['~> 3.2.1'])
|
27
|
+
gem.add_runtime_dependency('railties', ['~> 3.2.1'])
|
28
|
+
|
29
|
+
gem.add_development_dependency('rspec', ['~> 2.8.0'])
|
30
|
+
gem.add_development_dependency('pivotal_git_scripts')
|
31
|
+
gem.add_development_dependency('informal')
|
32
|
+
gem.add_development_dependency('activerecord')
|
33
|
+
gem.add_development_dependency('aruba')
|
34
|
+
gem.add_development_dependency('ammeter')
|
35
|
+
end
|
data/cucumber.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
default: --tags ~@slow_process --no-source
|
data/doc/blog-post.md
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
# Read-only access at attribute granularity
|
2
|
+
|
3
|
+
One of the many pleasures of working at Pivotal Labs is that we are encouraged to release some of our work as open source. Often during the course of our engagements, we write code that might have wide-spread use. Due to the nature of our contracts, we can not unilaterally release such code. Those rights belong to the client. And rightly so. So, it is an even greater pleasure when one of our clients believes in "giving back" to the community, as well.
|
4
|
+
|
5
|
+
One such example is this modest gem, `attribute_access_controllable` which allows you to set read-only access at the _attribute_ level, on a per-instance basis. For example, let's say that you have a model `Person` with an attribute `birthday`, which, for security purposes, cannot be changed once this attribute is set (except, perhaps, by an administrator with extraordinary privileges). Any future attempts to change this attribute will result in a validation error.
|
6
|
+
|
7
|
+
e.g.
|
8
|
+
|
9
|
+
> alice = Person.new(:birthday => '12/12/12')
|
10
|
+
=> #<Person id: nil, attr1: nil, created_at: nil, updated_at: nil, read_only_attributes: nil, birthday: "0012-12-12">
|
11
|
+
> alice.attr_read_only(:birthday)
|
12
|
+
=> #<Set: {"birthday"}>
|
13
|
+
> alice.save!
|
14
|
+
=> true
|
15
|
+
> alice.birthday = "2012-12-12"
|
16
|
+
=> "2012-12-12"
|
17
|
+
> alice.save!
|
18
|
+
ActiveRecord::RecordInvalid: Validation failed: Birthday is invalid, Birthday is read_only
|
19
|
+
> alice.save!(:skip_read_only => true)
|
20
|
+
=> true
|
21
|
+
|
22
|
+
Setting this up is trivial, thanks to a Rails generator which does most of the heavy lifting for you.
|
23
|
+
|
24
|
+
rails generate attribute_access Person
|
25
|
+
|
26
|
+
After that, you need only know about one new method added to your class:
|
27
|
+
|
28
|
+
#attr_read_only(*attributes) # Marks attributes as read-only
|
29
|
+
|
30
|
+
There are a few others, but this one, plus the new functionality added to `#save` and `#save!` will get you quite far.
|
31
|
+
|
32
|
+
And if that's all that you were looking for when you stumbled across this article, then there's no need to read any further. Go install the gem and have fun (and may your tests be green when you expect them to be).
|
33
|
+
|
34
|
+
## From customer requirements to releasable gem
|
35
|
+
|
36
|
+
On the other hand, if you are interested in how we got from the original customer story to a releasable open sourced gem, read on. The source code for the module is a [mere 34 lines long](https://github.com/halogenguides/Attribute-Access-Controllable/blob/master/lib/attribute_access_controllable.rb). It implements 2 new methods, a validator and (gently) overrides `#save` and `#save!`. Being good Test Driven Developers, we wrote our specs first, and since we wanted this behavior to be included in several models, we wrote our specs as a [_shared behavior_](https://github.com/halogenguides/Attribute-Access-Controllable/blob/master/lib/attribute_access_controllable/spec_support/shared_examples.rb) as well. The spec clocks in at 44 lines, slightly longer than our implementation. All in all, tiny. The whole commit was less than 100 lines of code.
|
37
|
+
|
38
|
+
AttributeAccessControllable
|
39
|
+
it should behave like it has AttributeAccessControllable
|
40
|
+
#attr_read_only(:attribute, ...) marks an attribute as read-only
|
41
|
+
#read_only_attribute?(:attribute) returns true when marked read-only
|
42
|
+
#read_only_attribute?(:attribute) returns false when not marked read-only (or not marked at all)
|
43
|
+
#save! raises error when :attribute is read-only
|
44
|
+
#save!(:context => :skip_read_only) is okay
|
45
|
+
#save is invalid when :attribute is read-only
|
46
|
+
#save(:context => :skip_read_only) is okay
|
47
|
+
|
48
|
+
In order to get to something "releasable" we needed a few more things, which we put on our To-Do list:
|
49
|
+
|
50
|
+
#### To do
|
51
|
+
1. MIT License
|
52
|
+
2. A gem specification
|
53
|
+
3. Basic documentation in a README file
|
54
|
+
|
55
|
+
The list got longer as we fleshed out both the documentation and the integration tests, as you'll see in a moment, but first, let's talk about
|
56
|
+
|
57
|
+
### Getting the legal issues resolved, easily and quickly
|
58
|
+
|
59
|
+
Pivotal's open sourcing policy is straightforward and simple to execute; We don't touch it. We write code for our clients, it's their code to do with as they please. My particular client liked the work we did for them and thought it would make a great open source gem. The Director of Engineering signed off on the idea and I paired with him to create the github repository during a lunch break. The first commit was tiny, just a basic directory structure and the existing code. I don't think the tests passed because they lacked a proper RSpec infrastructure.
|
60
|
+
|
61
|
+
### Creating the gem
|
62
|
+
|
63
|
+
bundler gem DIRECTORY
|
64
|
+
|
65
|
+
is your best friend. It set up the layout for us, including an MIT License and a gem specification. It had a boilerplate README, too.
|
66
|
+
|
67
|
+
### Writing the documentation for the code you wished you had
|
68
|
+
|
69
|
+
Next, we wrote a draft of the README file which documented what we knew: You needed a migration to create a column called `:read_only_attributes` and you needed to include the module into the class. Then we started thinking about the pain points of using our code as is. Wouldn't it be nice if we could create the migration automatically? Rails generators do that sort of thing, how hard could it be? (Famous last words...) It became clear that we needed to test drive out some new features of the _gem_ that supported the actual _module_.
|
70
|
+
|
71
|
+
#### To do
|
72
|
+
1. ~~MIT License~~
|
73
|
+
2. ~~A gem specification~~
|
74
|
+
3. ~~Basic documentation in a README file~~
|
75
|
+
4. Integration test
|
76
|
+
|
77
|
+
### I am not a big cucumber fan, but...
|
78
|
+
|
79
|
+
Really, I'm not. I used to write Cucumber features all the time, but nowadays, I use a combination of RSpec and Capybara to get most of my day-to-day integration testing done. There is, however, one sweet spot for Cucumber that I'm finding more and more useful; A very high-level document that describes essential features in a way that a reader will say, "Ahhh, so _that_ is how it is supposed to work!" Here's a copy of the spec I wrote:
|
80
|
+
|
81
|
+
Feature: Read only attributes
|
82
|
+
|
83
|
+
Scenario: In a simple rails application
|
84
|
+
Given a new rails application
|
85
|
+
And I generate a new migration for the class "Person"
|
86
|
+
And I generate an attribute access migration for the class "Person"
|
87
|
+
And I have a test that exercises read-only
|
88
|
+
When I run `rake spec`
|
89
|
+
Then the output should contain "7 examples, 0 failures"
|
90
|
+
|
91
|
+
You probably won't find any web-steps out there to handle these lines. I use [Aruba](https://github.com/cucumber/aruba) to handle the dirty work of executing shell commands in a safe sandbox-y way. The [step definition file](https://github.com/halogenguides/Attribute-Access-Controllable/blob/master/features/step_definitions/steps.rb) hides most of the ugliness. Even so, most readers could figure out what to do, by hand, for each step.
|
92
|
+
|
93
|
+
#### To do
|
94
|
+
1. ~~MIT License~~
|
95
|
+
2. ~~A gem specification~~
|
96
|
+
3. ~~Basic documentation in a README file~~
|
97
|
+
4. ~~Integration test~~
|
98
|
+
5. Generator
|
99
|
+
|
100
|
+
### Big generators
|
101
|
+
|
102
|
+
This gem was my first attempt at writing a generator, so it was awkward. I still don't understand [Thor](https://github.com/wycats/thor) properly. Fortunately, I happened upon [Ammeter](https://github.com/alexrothenberg/ammeter), which helped me write out test specs for the generator. If you've got good specs, then you can sometimes stumble along until you learn enough to get it right. Alex Rothenberg's original [blog post](http://www.alexrothenberg.com/2011/10/10/ammeter-the-way-to-write-specs-for-your-rails-generators.html) about the gem was quite informative, as were the test cases from the [Devise](https://github.com/plataformatec/devise/tree/master/test/generators) gem.
|
103
|
+
|
104
|
+
I have to admit; constructing the generator was more complex than the original module! There are more "moving parts;" templates, usage files, specs, in addition to the generator itself. So there is a certain amount of overhead that might overwhelm the original content. On the other hand, I learned quite a bit, and the gem is far more useful.
|
105
|
+
|
106
|
+
require "spec_helper"
|
107
|
+
require 'generators/attribute_access/attribute_access_generator'
|
108
|
+
|
109
|
+
describe AttributeAccessGenerator do
|
110
|
+
before do
|
111
|
+
prepare_destination
|
112
|
+
Rails::Generators.options[:rails][:orm] = :active_record
|
113
|
+
end
|
114
|
+
|
115
|
+
describe "the migration" do
|
116
|
+
before { run_generator %w(Person) }
|
117
|
+
subject { migration_file('db/migrate/create_people.rb') }
|
118
|
+
it { should exist }
|
119
|
+
it { should be_a_migration }
|
120
|
+
it { should contain 'class CreatePeople < ActiveRecord::Migration' }
|
121
|
+
it { should contain 'create_table :people do |t|'}
|
122
|
+
it { should contain 't.text :read_only_attributes'}
|
123
|
+
end
|
124
|
+
|
125
|
+
describe "the class" do
|
126
|
+
before { run_generator %w(Person) }
|
127
|
+
subject { file('app/models/person.rb') }
|
128
|
+
it { should exist }
|
129
|
+
it { should contain 'include AttributeAccessControllable' }
|
130
|
+
end
|
131
|
+
|
132
|
+
Some interesting things to note; you must `require` the generator, since it is not pulled in by default. The subject of each suite is a _file_, not the class `AttributeAccessGenerator`. The `migration_file` helper prepends the TIMESTAMP onto the migration file for you. If you need to set up more things for your test, `destination_root` is a helper with a path to the temporary directory. It remains after the tests have run, which makes it useful when debugging.
|
133
|
+
|
134
|
+
Here's something else that I did not know, but it might help new generator writers; the order in which you define your methods in the generator class is _significant_. I don't know how this is done, but each "method" in the generator class is executed in turn. This is important for my generator; the model class definition _must_ exist before I inject the new content that mixes in the module, so I had to write the `generate_model` method before the `inject_attribute_access_content` method. I was scratching my head over that one for quite awhile.
|
135
|
+
|
136
|
+
require "rails/generators/active_record"
|
137
|
+
|
138
|
+
class AttributeAccessGenerator < ActiveRecord::Generators::Base
|
139
|
+
source_root File.expand_path('../templates', __FILE__)
|
140
|
+
|
141
|
+
def create_migration_file
|
142
|
+
if (behavior == :invoke && model_exists?)
|
143
|
+
migration_template "migration.rb", "db/migrate/add_read_only_attributes_to_#{table_name}"
|
144
|
+
else
|
145
|
+
migration_template "migration_create.rb", "db/migrate/create_#{table_name}"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def generate_model
|
150
|
+
invoke "active_record:model", [name], :migration => false unless model_exists? && behavior == :invoke
|
151
|
+
end
|
152
|
+
|
153
|
+
def inject_attribute_access_content
|
154
|
+
class_path = class_name.to_s.split('::')
|
155
|
+
|
156
|
+
indent_depth = class_path.size
|
157
|
+
content = " " * indent_depth + 'include AttributeAccessControllable' + "\n"
|
158
|
+
|
159
|
+
inject_into_class(model_path, class_path.last, content)
|
160
|
+
end
|
161
|
+
|
162
|
+
#### To do
|
163
|
+
1. ~~MIT License~~
|
164
|
+
2. ~~A gem specification~~
|
165
|
+
3. ~~Basic documentation in a README file~~
|
166
|
+
4. ~~Integration test~~
|
167
|
+
5. ~~Generator~~
|
168
|
+
6. Shareable tests
|
169
|
+
|
170
|
+
## Yo, I hear you like tests in your tests
|
171
|
+
|
172
|
+
Lastly, we want to share the testing love. The gem consumer should not have to _write_ tests to drive out the same feature that we have already tested. That would not be very DRY. So, in order to make our shared behavior, er, um, _shareable_, we moved it into `lib` with a few wrappers, namely, the `spec_support.rb` file, which you can include in your own spec files to test drive adding the module to your own classes.
|
173
|
+
|
174
|
+
Which is where `And I have a test that exercises read-only` comes in. You can see this in the `steps.rb` file:
|
175
|
+
|
176
|
+
require 'spec_helper'
|
177
|
+
require 'attribute_access_controllable/spec_support'
|
178
|
+
|
179
|
+
describe Person do
|
180
|
+
it_should_behave_like "it has AttributeAccessControllable", :attr1
|
181
|
+
end
|
182
|
+
|
183
|
+
#### To do
|
184
|
+
1. ~~MIT License~~
|
185
|
+
2. ~~A gem specification~~
|
186
|
+
3. ~~Basic documentation in a README file~~
|
187
|
+
4. ~~Integration test~~
|
188
|
+
5. ~~Generator~~
|
189
|
+
6. ~~Shareable tests~~
|
190
|
+
|
191
|
+
## Don't be afraid to release v1.0.0
|
192
|
+
|
193
|
+
I am a strong believer in [semantic versioning](http://semver.org/). I simply can not understand why some core ruby tools are still living in version zero land, even after years and years of development and use. So, after a couple of internal commits, we released v1.0.0 of the gem, and less than a day later released v1.1.0 and then v1.1.1! (You probably shouldn't use anything less than v1.1.1)
|
194
|
+
|
195
|
+
## An interesting mix
|
196
|
+
|
197
|
+
In summary, we used a lot of tools and techniques to go from a simple commit to a shareable gem:
|
198
|
+
|
199
|
+
* Rails generators
|
200
|
+
* Cucumber
|
201
|
+
* Aruba
|
202
|
+
* Ammeter
|
203
|
+
* RSpec shared behaviors
|
204
|
+
* Integration tests
|
205
|
+
* Generator tests
|
206
|
+
* Module tests
|
207
|
+
|
208
|
+
I encourage everyone to release as much of their work as possible because it raises the state of the art for us all. There are limits, of course, but that still affords lots of wiggle room. Small gems like `attribute_access_controllable` won't change the world, but they ease the pain of staying DRY and we all get to learn a little something.
|
209
|
+
|
210
|
+
### Thanks
|
211
|
+
|
212
|
+
To Social Chorus for choosing to open source this code. And to Pivotal Labs for encouraging a better way to do software engineering.
|
@@ -0,0 +1,12 @@
|
|
1
|
+
@announce-cmd
|
2
|
+
Feature: Read only attributes
|
3
|
+
|
4
|
+
Scenario: In a simple rails application
|
5
|
+
Given a new rails application
|
6
|
+
And I generate a new migration for the class "Person"
|
7
|
+
And I generate an attribute access migration for the class "Person"
|
8
|
+
And I have a test that exercises read-only
|
9
|
+
When I run `rake spec`
|
10
|
+
Then the output should contain "8 examples, 0 failures"
|
11
|
+
|
12
|
+
|
@@ -0,0 +1,49 @@
|
|
1
|
+
World(Aruba::Api)
|
2
|
+
|
3
|
+
Given /^a new rails application$/ do
|
4
|
+
run_simple "rails new test_app --quiet --force --skip-bundle --skip-test-unit --skip-javascript --skip-sprockets"
|
5
|
+
cd '/test_app'
|
6
|
+
use_clean_gemset 'test_app'
|
7
|
+
write_file '.rvmrc', "rvm use default@test_app\n"
|
8
|
+
append_to_file 'Gemfile', <<EOT
|
9
|
+
gem 'attribute_access_controllable', :path => '../../..'
|
10
|
+
gem 'rspec-rails'
|
11
|
+
EOT
|
12
|
+
run_simple 'bundle install --quiet'
|
13
|
+
run_simple 'script/rails generate rspec:install'
|
14
|
+
end
|
15
|
+
|
16
|
+
Given /^I generate a new migration for the class "Person"$/ do
|
17
|
+
cmd = "rails generate model Person attr1:string --test-framework"
|
18
|
+
run_simple cmd
|
19
|
+
output = stdout_from cmd
|
20
|
+
assert_partial_output "invoke active_record", output
|
21
|
+
|
22
|
+
cmd = "rake db:migrate"
|
23
|
+
run_simple cmd
|
24
|
+
output = stdout_from cmd
|
25
|
+
assert_matching_output %q{== Create.+: migrating}, output
|
26
|
+
assert_matching_output %q{== Create.+: migrated}, output
|
27
|
+
end
|
28
|
+
|
29
|
+
Given /^I generate an attribute access migration for the class "Person"$/ do
|
30
|
+
cmd = "rails generate attribute_access Person"
|
31
|
+
run_simple cmd
|
32
|
+
output = stdout_from cmd
|
33
|
+
assert_matching_output "create\\s+db/migrate/\\d+_add_read_only_attributes_to_.+\.rb", output
|
34
|
+
assert_matching_output "insert\\s+app/models/.+\.rb", output
|
35
|
+
|
36
|
+
cmd = "rake db:migrate"
|
37
|
+
run_simple cmd
|
38
|
+
end
|
39
|
+
|
40
|
+
Given /^I have a test that exercises read\-only$/ do
|
41
|
+
overwrite_file 'spec/models/person_spec.rb', <<EOT
|
42
|
+
require 'spec_helper'
|
43
|
+
require 'attribute_access_controllable/spec_support'
|
44
|
+
|
45
|
+
describe Person do
|
46
|
+
it_should_behave_like "it has AttributeAccessControllable", :attr1
|
47
|
+
end
|
48
|
+
EOT
|
49
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module AttributeAccessControllable
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
include ActiveModel::Validations
|
6
|
+
serialize :read_only_attributes
|
7
|
+
attr_accessor :skip_read_only # TODO: Really should name this something private (e.g. :_#{class_name}_skip_read_only)
|
8
|
+
validate :read_only_attributes_unchanged, :on => :update, :unless => :skip_read_only
|
9
|
+
end
|
10
|
+
|
11
|
+
def attr_read_only(*attributes)
|
12
|
+
self.read_only_attributes = Set.new(attributes.map { |a| a.to_s }) + (read_only_attributes || [])
|
13
|
+
end
|
14
|
+
|
15
|
+
def read_only_attribute?(attribute)
|
16
|
+
return false if read_only_attributes.nil?
|
17
|
+
read_only_attributes.member?(attribute.to_s)
|
18
|
+
end
|
19
|
+
|
20
|
+
def read_only_attributes_unchanged
|
21
|
+
changed_attributes.each_key do |attr|
|
22
|
+
errors.add(attr) << "is read_only" if read_only_attribute?(attr)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def save(options = {})
|
27
|
+
self.skip_read_only = options.delete(:skip_read_only)
|
28
|
+
super
|
29
|
+
end
|
30
|
+
|
31
|
+
def save!(options = {})
|
32
|
+
self.skip_read_only = options.delete(:skip_read_only)
|
33
|
+
super
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'attribute_access_controllable/spec_support/shared_examples'
|
@@ -0,0 +1,51 @@
|
|
1
|
+
shared_examples_for "it has AttributeAccessControllable" do |test_column|
|
2
|
+
it "#attr_read_only(:attribute, ...) marks an attribute as read-only" do
|
3
|
+
expect {
|
4
|
+
subject.attr_read_only(test_column)
|
5
|
+
}.to change(subject, :read_only_attributes).from(nil).to(Set.new([test_column.to_s]))
|
6
|
+
end
|
7
|
+
|
8
|
+
it "#read_only_attribute?(:attribute) returns true when marked read-only" do
|
9
|
+
subject.read_only_attributes = Set.new([test_column.to_s])
|
10
|
+
subject.should be_read_only_attribute(test_column)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "#read_only_attribute?(:attribute) returns false when not marked read-only (or not marked at all)" do
|
14
|
+
subject.should_not be_read_only_attribute(test_column)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "#save! raises error when :attribute is read-only" do
|
18
|
+
subject.attr_read_only(test_column)
|
19
|
+
subject.send(test_column.to_s + '=', 0)
|
20
|
+
expect { subject.save!(context: :update) }.to raise_error ActiveRecord::RecordInvalid
|
21
|
+
subject.errors[test_column].should =~ ["is invalid", "is read_only"]
|
22
|
+
end
|
23
|
+
|
24
|
+
it "#save!(:context => :skip_read_only) is okay" do
|
25
|
+
subject.attr_read_only(test_column)
|
26
|
+
subject.send(test_column.to_s + '=', 0)
|
27
|
+
expect { subject.save!(:skip_read_only => true) }.to_not raise_error
|
28
|
+
end
|
29
|
+
|
30
|
+
it "#save is invalid when :attribute is read-only" do
|
31
|
+
subject.attr_read_only(test_column)
|
32
|
+
subject.send(test_column.to_s + '=', 0)
|
33
|
+
subject.save(context: :update).should be_false
|
34
|
+
subject.errors[test_column].should =~ ["is invalid", "is read_only"]
|
35
|
+
end
|
36
|
+
|
37
|
+
it "#save(:context => :skip_read_only) is okay" do
|
38
|
+
subject.attr_read_only(test_column)
|
39
|
+
subject.send(test_column.to_s + '=', 0)
|
40
|
+
subject.save!(:skip_read_only => true).should_not be_false
|
41
|
+
end
|
42
|
+
|
43
|
+
it "serializes :read_only_attributes" do
|
44
|
+
subject.attr_read_only(test_column)
|
45
|
+
subject.save!
|
46
|
+
subject.reload
|
47
|
+
subject.read_only_attributes.should == Set.new([test_column.to_s])
|
48
|
+
end
|
49
|
+
|
50
|
+
#it "#attr_writable(:attribute)"
|
51
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Description:
|
2
|
+
Add attribute access control to an ActiveRecord model
|
3
|
+
|
4
|
+
Example:
|
5
|
+
rails generate attribute_access Thing
|
6
|
+
|
7
|
+
This will create a migration thats adds
|
8
|
+
:read_only_attributes to your Thing model
|
9
|
+
|
10
|
+
You will then need to add
|
11
|
+
|
12
|
+
include AttributeAccessControllable
|
13
|
+
|
14
|
+
to your model to enable the new behavior
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "rails/generators/active_record"
|
2
|
+
|
3
|
+
class AttributeAccessGenerator < ActiveRecord::Generators::Base
|
4
|
+
source_root File.expand_path('../templates', __FILE__)
|
5
|
+
|
6
|
+
def create_migration_file
|
7
|
+
if (behavior == :invoke && model_exists?)
|
8
|
+
migration_template "migration.rb", "db/migrate/add_read_only_attributes_to_#{table_name}"
|
9
|
+
else
|
10
|
+
migration_template "migration_create.rb", "db/migrate/create_#{table_name}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def generate_model
|
15
|
+
invoke "active_record:model", [name], :migration => false unless model_exists? && behavior == :invoke
|
16
|
+
end
|
17
|
+
|
18
|
+
def inject_attribute_access_content
|
19
|
+
class_path = class_name.to_s.split('::')
|
20
|
+
|
21
|
+
indent_depth = class_path.size
|
22
|
+
content = " " * indent_depth + 'include AttributeAccessControllable' + "\n"
|
23
|
+
|
24
|
+
inject_into_class(model_path, class_path.last, content)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def model_exists?
|
30
|
+
File.exists?(File.join(destination_root, model_path))
|
31
|
+
end
|
32
|
+
|
33
|
+
def model_path
|
34
|
+
@model_path ||= File.join("app", "models", "#{file_path}.rb")
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'informal'
|
3
|
+
require 'active_record/errors'
|
4
|
+
require 'active_record/validations'
|
5
|
+
|
6
|
+
require 'attribute_access_controllable'
|
7
|
+
require 'attribute_access_controllable/spec_support'
|
8
|
+
|
9
|
+
class TestFakePersistance
|
10
|
+
include Informal::Model
|
11
|
+
include ActiveRecord::Validations
|
12
|
+
|
13
|
+
def save(options={})
|
14
|
+
perform_validations(options) ? :pretend_to_call_super : false
|
15
|
+
end
|
16
|
+
|
17
|
+
def save!(options={})
|
18
|
+
perform_validations(options) ? :pretend_to_call_super : raise(ActiveRecord::RecordInvalid.new(self))
|
19
|
+
end
|
20
|
+
|
21
|
+
def reload
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.serialize(*)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class TestAttributeAccessControllable < TestFakePersistance
|
30
|
+
attr_accessor :read_only_attributes # usually created via a migration
|
31
|
+
include ActiveModel::Dirty
|
32
|
+
|
33
|
+
define_attribute_methods [:test_column]
|
34
|
+
|
35
|
+
def test_column
|
36
|
+
@test_column
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_column=(val)
|
40
|
+
test_column_will_change! unless val == @test_column
|
41
|
+
@test_column = val
|
42
|
+
end
|
43
|
+
|
44
|
+
include AttributeAccessControllable # code under test
|
45
|
+
end
|
46
|
+
|
47
|
+
describe AttributeAccessControllable do
|
48
|
+
subject { TestAttributeAccessControllable.new }
|
49
|
+
|
50
|
+
it_should_behave_like "it has AttributeAccessControllable", :test_column
|
51
|
+
end
|
52
|
+
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require 'generators/attribute_access/attribute_access_generator'
|
3
|
+
|
4
|
+
describe AttributeAccessGenerator do
|
5
|
+
before do
|
6
|
+
prepare_destination
|
7
|
+
Rails::Generators.options[:rails][:orm] = :active_record
|
8
|
+
end
|
9
|
+
|
10
|
+
context "new class" do
|
11
|
+
describe "the migration" do
|
12
|
+
before { run_generator %w(Person) }
|
13
|
+
subject { migration_file('db/migrate/create_people.rb') }
|
14
|
+
it { should exist }
|
15
|
+
it { should be_a_migration }
|
16
|
+
it { should contain 'class CreatePeople < ActiveRecord::Migration' }
|
17
|
+
it { should contain 'create_table :people do |t|'}
|
18
|
+
it { should contain 't.text :read_only_attributes'}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context "existing class" do
|
23
|
+
describe "the migration" do
|
24
|
+
before do
|
25
|
+
FileUtils.mkdir_p(File.join(destination_root, "app/models"))
|
26
|
+
File.open(File.join(destination_root, "app/models/person.rb"), "w") {|f|
|
27
|
+
f.puts "class Person < ActiveRecord::Base\nend\n"
|
28
|
+
}
|
29
|
+
run_generator %w(Person)
|
30
|
+
end
|
31
|
+
subject { migration_file('db/migrate/add_read_only_attributes_to_people.rb') }
|
32
|
+
it { should exist }
|
33
|
+
it { should be_a_migration }
|
34
|
+
it { should contain 'class AddReadOnlyAttributesToPeople < ActiveRecord::Migration' }
|
35
|
+
it { should contain 'add_column :people, :read_only_attributes, :text'}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "the class" do
|
40
|
+
before { run_generator %w(Person) }
|
41
|
+
subject { file('app/models/person.rb') }
|
42
|
+
it { should exist }
|
43
|
+
it { should contain 'include AttributeAccessControllable' }
|
44
|
+
end
|
45
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.setup
|
3
|
+
require 'ammeter/init'
|
4
|
+
|
5
|
+
SPEC_ROOT = File.dirname(__FILE__)
|
6
|
+
|
7
|
+
Dir[File.join(SPEC_ROOT, "support/**/*.rb")].each {|f| require f}
|
8
|
+
|
9
|
+
RSpec.configure do |config|
|
10
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
11
|
+
config.run_all_when_everything_filtered = true
|
12
|
+
config.filter_run :focus
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,226 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: attribute_access_controllable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Social Chorus
|
9
|
+
- Kenneth Mayer
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-05-08 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activesupport
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ~>
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 3.2.1
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ~>
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: 3.2.1
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: activemodel
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ~>
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: 3.2.1
|
39
|
+
type: :runtime
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ~>
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 3.2.1
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: railties
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.2.1
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ~>
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 3.2.1
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: rspec
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ~>
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: 2.8.0
|
71
|
+
type: :development
|
72
|
+
prerelease: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ~>
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: 2.8.0
|
79
|
+
- !ruby/object:Gem::Dependency
|
80
|
+
name: pivotal_git_scripts
|
81
|
+
requirement: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
type: :development
|
88
|
+
prerelease: false
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: informal
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ! '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: activerecord
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ! '>='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
none: false
|
123
|
+
requirements:
|
124
|
+
- - ! '>='
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
- !ruby/object:Gem::Dependency
|
128
|
+
name: aruba
|
129
|
+
requirement: !ruby/object:Gem::Requirement
|
130
|
+
none: false
|
131
|
+
requirements:
|
132
|
+
- - ! '>='
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
type: :development
|
136
|
+
prerelease: false
|
137
|
+
version_requirements: !ruby/object:Gem::Requirement
|
138
|
+
none: false
|
139
|
+
requirements:
|
140
|
+
- - ! '>='
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
- !ruby/object:Gem::Dependency
|
144
|
+
name: ammeter
|
145
|
+
requirement: !ruby/object:Gem::Requirement
|
146
|
+
none: false
|
147
|
+
requirements:
|
148
|
+
- - ! '>='
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: '0'
|
151
|
+
type: :development
|
152
|
+
prerelease: false
|
153
|
+
version_requirements: !ruby/object:Gem::Requirement
|
154
|
+
none: false
|
155
|
+
requirements:
|
156
|
+
- - ! '>='
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
description: Allow attribute-level read/write access control, per instance, of any
|
160
|
+
ActiveModel class.
|
161
|
+
email:
|
162
|
+
- tech@socialchorus.com
|
163
|
+
- kmayer@bitwrangler.com
|
164
|
+
executables: []
|
165
|
+
extensions: []
|
166
|
+
extra_rdoc_files: []
|
167
|
+
files:
|
168
|
+
- .gitignore
|
169
|
+
- .pairs
|
170
|
+
- .rspec
|
171
|
+
- Gemfile
|
172
|
+
- LICENSE
|
173
|
+
- README.md
|
174
|
+
- Rakefile
|
175
|
+
- attribute_access_controllable.gemspec
|
176
|
+
- cucumber.yml
|
177
|
+
- doc/blog-post.md
|
178
|
+
- features/read_only_attributes.feature
|
179
|
+
- features/step_definitions/steps.rb
|
180
|
+
- features/support/env.rb
|
181
|
+
- lib/attribute_access_controllable.rb
|
182
|
+
- lib/attribute_access_controllable/spec_support.rb
|
183
|
+
- lib/attribute_access_controllable/spec_support/shared_examples.rb
|
184
|
+
- lib/attribute_access_controllable/version.rb
|
185
|
+
- lib/generators/attribute_access/USAGE
|
186
|
+
- lib/generators/attribute_access/attribute_access_generator.rb
|
187
|
+
- lib/generators/attribute_access/templates/migration.rb
|
188
|
+
- lib/generators/attribute_access/templates/migration_create.rb
|
189
|
+
- spec/attribute_access_controllable_spec.rb
|
190
|
+
- spec/generators/attribute_access_generator_spec.rb
|
191
|
+
- spec/spec_helper.rb
|
192
|
+
homepage: https://github.com/halogenguides/Attribute-Access-Controllable
|
193
|
+
licenses: []
|
194
|
+
post_install_message:
|
195
|
+
rdoc_options: []
|
196
|
+
require_paths:
|
197
|
+
- lib
|
198
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
199
|
+
none: false
|
200
|
+
requirements:
|
201
|
+
- - ! '>='
|
202
|
+
- !ruby/object:Gem::Version
|
203
|
+
version: '0'
|
204
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
205
|
+
none: false
|
206
|
+
requirements:
|
207
|
+
- - ! '>='
|
208
|
+
- !ruby/object:Gem::Version
|
209
|
+
version: '0'
|
210
|
+
requirements: []
|
211
|
+
rubyforge_project:
|
212
|
+
rubygems_version: 1.8.21
|
213
|
+
signing_key:
|
214
|
+
specification_version: 3
|
215
|
+
summary: This gem allows you to control write access at the _attribute_ level, on
|
216
|
+
a per-instance basis. For example, let's say you had a model `Person` that has an
|
217
|
+
attribute, `birthday`, which, for security purposes, once this attribute is set,
|
218
|
+
cannot be changed again (except, perhaps by an administrator with extraordinary
|
219
|
+
privileges). Any attempts to change this value to raise a validation error.
|
220
|
+
test_files:
|
221
|
+
- features/read_only_attributes.feature
|
222
|
+
- features/step_definitions/steps.rb
|
223
|
+
- features/support/env.rb
|
224
|
+
- spec/attribute_access_controllable_spec.rb
|
225
|
+
- spec/generators/attribute_access_generator_spec.rb
|
226
|
+
- spec/spec_helper.rb
|