1 / 52

Authorization and Testing

Authorization and Testing. INFO 2310: Topics in Web Design and Programming. Where we are. Last time: We added the ability of users to log in, and made sure that we treated their passwords in a secure fashion. This time: How can we use user information to limit actions?. For example.

jena
Download Presentation

Authorization and Testing

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. Authorization and Testing INFO 2310:Topics in Web Design and Programming

  2. Where we are • Last time: We added the ability of users to log in, and made sure that we treated their passwords in a secure fashion. • This time: How can we use user information to limit actions?

  3. For example • Wouldn’t it be nice to ask a post whether the current user is authorized to edit it? • @post.editable_by?(current_user) • We could further add methods to correspond to each of the possible CRUD (Create/Read/Update/Destroy) operations: • viewable_by? • creatable_by? • editable_by? • destroyable_by?

  4. Try it… • We can just add this to our post model in app/models/post.rb. • def editable_by?(editor) • editor == self.author • end

  5. Then in the views/posts/show.html.erb, we can restrict access to editing. Change • <%= link_to 'Edit', edit_post_path(@post) %> • to • <%= link_to 'Edit', edit_post_path(@post) if @post.editable_by?(current_user) %> • Do the same in views/posts/index.html.erb.

  6. Extensions • Now we can add the same methods to User and Comments… • But isn’t this a little un-DRY? (Recall DRY = ‘Don’t Repeat Yourself!’) How can we add default methods to all these objects at once?

  7. The magic of open classes • All our models are subclasses of ActiveRecord::Base. So we can just add our desired extra methods to this object… which Ruby allows us to do. Then all the models will have these by default.

  8. Message passing • A bit about how Ruby method calls work: they all pass messages to the object. So • object.explode! • is actually passing the message :explode! to the object. We can also do this directly by writing • object.send(:explode!)

  9. A module for the default methods • We create the default methods in an ‘AuthorizationFu’ module, which we will save as lib/authorization_fu.rb. The final line instructs ActiveRecord to include the methods as part of the class. • module AuthorizationFu • module InstanceMethods • def viewable_by?(viewer) • true • end • def creatable_by?(creator) • true • end • def editable_by?(editor) • true • end • def destroyable_by?(destroyer) • true • end • end • end • ActiveRecord::Base.send(:include, AuthorizationFu::InstanceMethods)

  10. Loading the methods • Add a require to the end of config/environment.rb to force this to get loaded on initialization (note that you need to restart the server to get this to work). • require ‘authorization_fu’

  11. Almost • This almost works to restrict editing access to the post author. What have we overlooked? • Remember that people can navigate directly to the editing page /posts/3/edit. How can we prevent this?

  12. Raising an exception • Let’s raise an exception in the controller if someone tries to edit a post who isn’t suppose to. • In app/controllers/posts_controller.rb, in the edit action, after finding the post, add • raise SecurityTransgression unless @post.editable_by?(current_user)

  13. Adding an Exception • Now we need to add SecurityTransgression as a subclass of the Exceptions class, but we can do this in our lib/authorization_fu.rb file: • module AuthorizationFu • module InstanceMethods • … • module Exceptions • class SecurityTransgression < Exception • end • end • end • ActiveRecord::Base.send(:include, AuthorizationFu::InstanceMethods) • ActionController::Base.send(:include, AuthorizationFu::Exceptions)

  14. Recovering gracefully • Rather than blowing up with an exception whenever someone tries to do this, we should just give them a 403 error. So we need to catch them somewhere. • We can do this with a method rescue_action defined in app/controllers/application.rb. This will catch any exception raised in any of the controllers.

  15. To the rescue • Add to app/controllers/application.rb: • def rescue_action(e) • case e • when SecurityTransgression • respond_to do |format| • format.xml { head :forbidden } • format.html { render :file => "#{RAILS_ROOT}/public/403.html", :status => :forbidden } • end • else • super(e) • end • end

  16. 403 • Note that this renders the 403.html file found in public. Except there isn’t one. So take the 404 page and make one.

  17. Debugging

  18. Rails has a breakpointing/debugging ability. The server can be started with a debugging option via ‘ruby script/server –u’. Then any place the ‘debugger’ command occurs in your code will drop you into a debugger.

  19. You need to install the debugger first via ‘gem install ruby-debug’. Note that this hasn’t been done on the machines in this cluster.

  20. Testing

  21. You’re testing, right? • Rails assumes you will be writing tests to check your code. There is a separate test version of the database, and default test folder. When we created our post, user, and comment models via scaffolds, Rails automatically created test files to go along with everything else.

  22. Test types • Rails supports three different types of tests: • Unit tests: For models • Functional tests: For controllers • Integration tests: For everything altogether • Note that within the test directory, there are unit, functional, and integration folders (also a fixtures folder for creating test data).

  23. Fixtures • Some sample data from test/fixtures/user.yml: • one: • name: MyString • email: MyString • password: MyString • two: • name: MyString • email: MyString • password: MyString • Our users don’t have passwords any more, so delete these lines.

  24. A starting point • Let’s look at test/unit/post_test.rb: • require 'test_helper‘ • class PostTest < ActiveSupport::TestCase • # Replace this with your real tests. • def test_truth • assert true • end • end

  25. Unit tests • Rails tests are built on top of Ruby’s Test::Unit framework. We write tests called test_blahblahblah and assert some number of conditions. • If all conditions are true, the test passes, otherwise it fails.

  26. Running tests • We usually run tests via rake. To see what we can run from rake, try • ‘rake –T test’.

  27. rake db:test:clone # Recreate the test database from the current... • rake db:test:clone_structure # Recreate the test databases from the develo... • rake db:test:prepare # Prepare the test database and load the schema • rake db:test:purge # Empty the test database • rake test # Run all unit, functional and integration tests • rake test:functionals # Run tests for functionalsdb:test:prepare / ... • rake test:integration # Run tests for integrationdb:test:prepare / ... • rake test:plugins # Run tests for pluginsenvironment / Run the ... • rake test:recent # Run tests for recentdb:test:prepare / Test ... • rake test:uncommitted # Run tests for uncommitteddb:test:prepare / ... • rake test:units # Run tests for unitsdb:test:prepare / Run th...

  28. Let’s try it! • For this lesson, we’ll focus on unit testing. So let’s try to run unit tests via ‘rake test:units’. (From Komodo: ‘Rails Tools/Test/Unit Tests’). • Started • ... • Finished in 2.532 seconds. • 3 tests, 3 assertions, 0 failures, 0 errors • We didn’t write any tests yet! What ran?

  29. What should we write? • What kind of tests should we write? Here are some starting suggestions: • test_should_be_valid_with_valid_attributes • test_should_be_invalid_without_a_title • test_should_be_invalid_without_a_body • test_should_be_creatable_by_any_user • test_should_be_editable_by_its_author • test_should_not_be_editable_by_a_non_author

  30. Let’s try some! • Write the following tests in test/unit/post_test.rb, then run them. • def test_should_be_valid_with_valid_attributes • assert_valid Post.new(:title => 'Test Post', :body => 'Test body.') • end • def test_should_be_invalid_without_a_title • assert !Post.new(:title => nil, :body => 'Test body.').valid? • end • def test_should_be_invalid_without_a_body • assert !Post.new(:title => 'Test Post', :body => nil).valid? • end

  31. Some issues • What happens if we end up adding some new field to the post model? • In order to avoid rewriting all our tests every time this happens, we write a helper function to create a new post, such that we can override the fields as needed.

  32. Add the following to test/unit/post_test.rb at the bottom: • private • def new_post(params = {}) • Post.new({:title => 'Test Post', :body => 'Test body.'}.merge(params)) • end

  33. Now rewrite your tests to take advantage of this. E.g. • def test_should_be_valid_with_valid_attributes • assert_valid new_post • end • Try them again…

  34. Another issue • Another minor issue is that since Rails doesn’t have an ‘assert_invalid’ helper function, we have to resort to ‘assert !Model.valid?’. Let’s fix this by adding ‘assert_invalid’.

  35. test_helper • The right place to add this is test/test_helper.rb, so that it is then available to all tests. • def assert_invalid(record, message=nil) • full_msg = build_message(message, "<?> is valid.", record) • assert_block(full_msg) { !record.valid? } • end • Now rewrite (and rerun) your tests again to take advantage of the new method.

  36. More about fixtures

  37. More DRYness • Fixtures allow you to define data once and reuse it in multiple tests. For instance, let’s create some sample users more imaginative than the defaults.

  38. Add this to test/fixtures/users.yml: • <% salt = Digest::SHA256.hexdigest('salt') %> • <% pass = Digest::SHA256.hexdigest("--#{salt}--password--")%> • matz: • name: Yukihiro Matsumoto • email: matz@ruby-lang.org • salt: <%= salt %> • crypted_password: <%= pass %> • why: • name: Why the Lucky Stiff • email: why@whytheluckystiff.net • salt: <%= salt %> • crypted_password: <%= pass %> • Note that we can use ERB in these yml files…

  39. Associations • We can now have other fixtures refer to these ones. Edit test/fixtures/posts.yml: • one: • title: My First Post • body: Hurray! • author: matz • two: • title: Another Post • body: Here is another post. • author: why

  40. More tests • Now we can write tests referring to users in test/unit/post_test.rb. • def test_should_be_editable_by_its_author • assert posts(:one).editable_by?(posts(:one).author) • end • Try to write test_should_not_be_editable_by_a_non_author

  41. Functional tests

  42. Functional tests • We test the controllers via functional tests. Note that the scaffold created some reasonable functional tests in test/functional/posts_controller_test.rb such as • def test_should_get_index • get :index • assert_response :success • assert_not_nil assigns(:posts) • end • We can run these via ‘ruby –Ilib;test test/functional/posts_controller_test.rb’. We could also run ‘rake test:functionals’, but this will bomb bigtime…

  43. Problems • In fact, even here we get two problems. • The first is with creating a post, which fails. Why? • The second is with editing a post, which has an error. Why?

  44. First fix • The first is an error in test_should_create_post since a post with no title and body is created. • def test_should_create_post • assert_difference('Post.count') do • post :create, :post => {:title => 'A post', :body => 'A body' } • end • assert_redirected_to post_path(assigns(:post)) • end

  45. Second problem • In a sense, the second issue shows that things are working the way they are supposed to – but we need to test them differently. • In this case, we need to have the ability to log in a user. So let’s write a helper function to do this.

  46. In test/test_helper.rb, add (at the bottom): • def login_as(user) • @request.session[:user_id] = user ? users(user).id : nil • end

  47. Log in first • Now we can edit test_should_get_edit to login first: • def test_should_get_edit_if_editable_by • login_as(:matz) • get :edit, :id => posts(:one).id • assert_response :success • end

  48. Testing for bad guys • We can also add a test that checks to see that the exception is raised if someone does what they aren’t supposed to: • def test_should_be_forbidden_if_not_editable_by • login_as(:why) • assert_raise AuthorizationFu::Exceptions::SecurityTransgression do • get :edit, :id => posts(:one).id • end • end • Now rerun tests (‘ruby –Ilib;test test/functionals/posts_controller_test.rb’).

  49. Testing philosopy

  50. Why write tests? • So you don’t have to manually test your application every time you make a change. • So can you can be sure that a minor change in this part of the code doesn’t break that part over there. • So that when you refactor (i.e. rip up and rewrite) your code, you can be sure that the new code has the same behavior as the old code. • So that you can CYA if making changes that affect other people’s code (“It can’t be broken; all your unit tests passed!”)

More Related