[Chapter 12](/guide/testing-authentication/) added users, sessions, sign-in tests, and read-only access for guests. Guests may view the list and a recipe detail page only but cannot create, edit, or destroy, and they do not see change buttons.

Right now one user can sign in, open another user's recipe, and change the title, or even destroy the recipe. That's because Chapter 12 did not add ownership to the recipes (no `user_id` in each recipe row) so there was no way to check if the signed-in user is authorized to perform those actions or not.

This chapter is about authorization (can this user perform that action?) and ownership (who owns this resource?). After this chapter, users can sign in, create their own recipe and browse another user's recipes but will not be able to edit or delete them.

<%= render Guide::ChapterProgress.new(
  feature: "Recipe ownership (`user_id` in the recipe table), a short ownership check suitable for hobby projects, then add Action Policy gem for owner-only edit, update, and destroy on recipes plus owner-only remove on ingredients and steps, with UI that hides change actions for non-owners.",
  why: "Authentication ended guest writes but signed-in users can still change any recipe. Authorization ensures only the owner can edit, update, or destroy a recipe while others can only view it.",
  test: "Integration: grow `recipe_access_integration_test.rb` with non-owner denials over HTTP for modify, destroy, and nested ingredient or step removal, plus assertions to ensure non-owner does not see Destroy on the list and no Edit, Destroy, or Remove on another user's recipe show.",
  later: "Mailers and jobs reuse owned recipes from fixtures in [Chapter 14](/guide/testing-mailers/) and [Chapter 15](/guide/testing-background-jobs/)."
) %>

## What you will do in this chapter

1. Add `user_id` to recipes, associate `User` and `Recipe`, give fixture recipes an owner, and assign the signed-in user on create with `current_user.recipes.build`.
2. Add one deny test to the existing access file and make it green with a short `ensure_recipe_owner` guard (sample authorization for hobby projects).
3. Put flash notices and alerts in the application layout so authorization (and the rest of the app) can show messages on every page, then remove the one-off flash markup from individual views.
4. Install Action Policy, put the same rule in `RecipePolicy`, and replace the inline `ensure_recipe_owner` guard.
5. Grow `recipe_access_integration_test.rb` with non-owner denials for modify, destroy, and ingredient/step Remove. Access tests for owners stay in `recipes_integration_test.rb` (unchanged from the previous chapter).
6. Hide buttons for Edit, Destroy, and Remove with `authenticated? && allowed_to?` so guests skip Action Policy and non-owners fail ownership.
7. Assert in the same access integration file that a non-owner does not see Destroy button on the list nor see Edit, Destroy, or Remove buttons on show.
8. Run `bin/rails test:all` to ensure everything is green, then commit.

## Scenarios to automate

| Scenario | Expected |
| --- | --- |
| Owner can modify their own recipe | Already covered by `updates a recipe` in `recipes_integration_test.rb` (Alice signed in). Do not re-test the happy path here. |
| Non-owner cannot modify another user's recipe | Bob GETs edit and PATCHes on Alice's recipe; land on the recipes list at the root path; title unchanged |
| Owner can destroy their own recipe | Already covered by `destroys a recipe` in `recipes_integration_test.rb` (Alice deletes lentil soup). Do not re-test the happy path here. |
| Non-owner cannot destroy another user's recipe | Bob DELETE on Alice's recipe; redirects to the recipe list page; no row removed |
| Owner can remove an ingredient and a step from the detail page | Already covered by `removes ingredients and steps from the detail page` in `recipes_integration_test.rb` (Alice, Turbo Stream DELETEs). Do not re-test the happy path here. |
| Owner can remove an ingredient and a step when updating a recipe | Already covered by `removes existing ingredients and steps when updating a recipe` in `recipes_integration_test.rb` (Alice, PATCH with `_destroy`). Do not re-test the happy path here. |
| Non-owner cannot remove an ingredient or a step from the detail page | Bob removes ingredient or step from Alice's pancakes; redirects to the recipe list page; no rows removed |
| Non-owner cannot remove an ingredient or a step when updating a recipe | Bob PATCHes with `_destroy` on Alice's Salt and preheat; redirects to the recipe list page; no rows removed |
| Guest cannot edit, destroy, or remove | Unsigned requests redirect to sign-in (Chapter 12 covers the recipe actions: create, edit and destroy; this chapter adds the nested ones: remove ingredients and steps) |
| Owner can see Edit, Destroy, and Remove buttons on their own recipe | Already covered by `signed-in user sees edit and destroy on show` and Destroy on the list by `signed-in user sees new recipe on the list` (Alice). |
| Non-owner cannot see Edit, Destroy, or Remove on another user's recipe | Bob does not see Destroy button on the recipes list nor any Edit, Destroy, or Remove buttons on Alice's show (`assert_select` with `count: 0`) |

## Authentication vs authorization (quick look)

You added Authentication in Chapter 12 and this chapter adds Authorization to the Cookbook app. These two concepts sound similar but are different. Authentication answers who is signed in. Authorization answers what that signed-in user is allowed to do.

| Question | Layer | Chapter | Example actor |
| --- | --- | --- | --- |
| Is anyone signed in? | Authentication | 12 | Guest vs Alice session |
| Can this signed-in user change this recipe? | Authorization | 13 | Bob vs Alice on `pancakes` |

Authentication failures send guests to sign-in. Authorization failures happen after sign-in: Bob is signed-in, but not the owner of the recipe so they should be redirected to the recipe list page and shown a denial flash message.

## Why ownership waited

Chapter 12 taught sessions and guest access on purpose without `user_id` on recipes. That helped keep the chapter focused only on authentication without drifting to ownership of the recipe. This chapter will now introduce ownership that is required for authorization. Once every recipe has a user, you can compare the recipe's owner to `current_user` and write allow and deny tests for Bob (non-owner) and Alice (owner).

You will see four labels show up in the rest of this chapter frequently: guest, signed-in user, owner and non-owner.

- Guest is a user that is not signed in to the app.
- Signed-in users have an active session in the app.
- For any given recipe, a signed-in user is either the owner or a non-owner and permissions differ for each four actors.

Following table summarizes the permissions for each actor:

| Actor | Who this is | Browse list and show | Create a new recipe | Edit, update, or destroy this recipe |
| --- | --- | --- | --- | --- |
| Guest | No session | Yes | No (redirect to sign-in) | No (redirect to sign-in) |
| Signed-in user | Has a session | Yes | Yes | Only when they own this recipe |
| Owner | Signed-in and owns this recipe (Alice on pancakes) | Yes | Yes | Yes |
| Non-owner | Signed-in but someone else owns this recipe (Bob on pancakes) | Yes | Yes | No |

## Add `user_id` and model associations

To start testing authorization, you first need to introduce the concept of ownership for each recipe. This is done by adding a `user_id` column to the recipes table and a foreign key to the users table.

Run the following command to add the `user_id` column to the recipes table:

```bash
bin/rails generate migration AddUserToRecipes user:references
```

Now persist the new column to the database by migrating the database:

```bash
bin/rails db:migrate
```

If you didn't have any record in the database for recipes then you will be fine and that command should have worked without any errors.

But if you had some recipes in the database then you will see an error like this:

```bash
bin/rails aborted!
StandardError: An error has occurred, this and all later migrations canceled: (StandardError)

SQLite3::ConstraintException: NOT NULL constraint failed: recipes.user_id

Caused by:
ActiveRecord::NotNullViolation: SQLite3::ConstraintException: NOT NULL constraint failed: recipes.user_id (ActiveRecord::NotNullViolation)

Caused by:
SQLite3::ConstraintException: NOT NULL constraint failed: recipes.user_id (SQLite3::ConstraintException)
```

If you check the migration file, you will see something like this:

```ruby
add_reference :recipes, :user, null: false, foreign_key: true
```

Notice the `null: false` part? That's the "not null" constraint causing the error. With that constraint in place, the new migration requires every recipe to have an owner, but the old rows have no owner. So, what's next?

The easiest way to fix this is to wipe the development database and rebuild the schema from migrations. Wipe out the database? Are you out of your mind?

Nope I am not out of my mind, it's fine to wipe out the database for this Cookbook app because it's just a sample app and not used in production. But you should **never run these commands against a real production database**.

```bash
bin/rails db:drop db:create db:migrate
```

This is what's happening in the commands above:

- `db:drop` and `db:create` clear the development database, wiping out all the data in the database and creates a new one back again.
- `db:migrate` applies `AddUserToRecipes` on an empty table, so the NOT NULL constraint succeeds.

You might be wondering: What if this was a production app?

In that case, you would add a nullable `user_id`, backfill every existing recipe with an owner, then add the NOT NULL constraint in a follow-up migration so new recipes always have an owner going forward.

Now that you have the `user_id` column, you need to add the associations on both User and Recipe models.

User model in `app/models/user.rb` already has `has_many :sessions` added by the generator, append `has_many :recipes` association next to it:

```ruby
# app/models/user.rb
class User < ApplicationRecord
  # ... existing code
  has_many :sessions, dependent: :destroy
  has_many :recipes, dependent: :destroy

  # ... existing code
end
```

Also update the Recipe model at `app/models/recipe.rb` to include the user association just above the `has_many :ingredients` association:

```ruby
# app/models/recipe.rb
class Recipe < ApplicationRecord
  belongs_to :user

  has_many :ingredients, dependent: :destroy
  # ... existing code
end
```

This is what's happening in the code above:

- `has_many :recipes` in the User model lets you get all recipes of that user by calling `current_user.recipes`. It is also very useful when creating a new recipe with an owner by calling `current_user.recipes.build`, you will see this in action when working with recipes controller later in this chapter.
- `belongs_to :user` ensures every recipe to have an owner. This also allows you to access the owner of a recipe by calling `recipe.user`.

## Wire Bob and recipe owners in fixtures

Chapter 12 only needed one user so Alice was enough for the Authentication chapter. But authorization is different, it needs two users because authorization is about checking permission of one user against another user's resource. That's where Bob comes in, a signed-in user who does not own the recipe you are testing.

Open the `test/fixtures/users.yml` and replace the leftover `two:` row from the generator with **bob**. Replace the whole file with the following:

```yaml
# test/fixtures/users.yml
<%% password_digest = BCrypt::Password.create("password") %>

alice:
  email_address: alice@example.com
  password_digest: <%%= password_digest %>

bob:
  email_address: bob@example.com
  password_digest: <%%= password_digest %>
```

Also update `test/fixtures/recipes.yml` so each recipe has Alice as the owner. Replace the whole file with the following:

```yaml
# test/fixtures/recipes.yml
pancakes:
  title: Fluffy pancakes
  description: Weekend breakfast
  prep_time: 15
  servings: 4
  user: alice

lentil_soup:
  title: Lentil soup
  description: Simple dinner
  prep_time: 30
  servings: 6
  user: alice
```

This is what's happening in the fixture files above:

- `user: alice` is the fixture label from `users.yml`, this is how you let Rails fixtures know that the pancakes and lentil soup recipes belong to Alice.
- Both fixture recipes belong to Alice so your existing integration and system tests keep a stable owner. Bob exists and does not own any recipes, a perfect candidate to test deny paths for authorization.

## Fix failing tests before moving to authorization

Associations and fixture owners are now in place. But before you move to authorization, you need to fix the tests that break due to the introduction of ownership on recipes. Run the test suite to see the failures:

```bash
bin/rails test
```

These are the failures you will see:

```bash
F

Failure:
RecipeTest#test_strips_whitespace_from_title_before_validation [test/models/recipe_test.rb:43]:
Expected false to be truthy.

F

Failure:
RecipesIntegrationTest#test_creates_a_recipe_with_an_ingredient_and_a_step [test/integration/recipes_integration_test.rb:60]:
`Recipe.count` didn't change by 1, but by 0.
Expected: 3
  Actual: 2

F

Failure:
RecipesIntegrationTest#test_creates_a_recipe [test/integration/recipes_integration_test.rb:42]:
`Recipe.count` didn't change by 1, but by 0.
Expected: 3
  Actual: 2
```

You will see failures in two main test files:

| Where | Why it fails |
| --- | --- |
| Model tests that call `Recipe.new` (`test/models/recipe_test.rb`) | `belongs_to :user` is required. A new recipe with no owner is invalid, so assertions that used to isolate title or prep time now fail on `user` too. |
| Integration tests for creating recipes (`test/integration/recipes_integration_test.rb`) | The controller still builds with `Recipe.new(recipe_params)`, so save fails when there is no `user_id` in the recipe enforced by the not null constraint. |

Fixture-backed examples that only read `recipes(:pancakes)` should stay green since you have already wired Alice as the owner for those recipes.

### Model tests: pass an owner on `Recipe.new`

Open `test/models/recipe_test.rb` you added in [Chapter 8](/guide/testing-models/), search for `Recipe.new` and pass `, user: users(:alice)` (owner of the recipe) as an argument. Following is an example for the `rejects a blank title` test:

```ruby
# test/models/recipe_test.rb
test "rejects a blank title" do
  recipe = Recipe.new(title: "", user: users(:alice))
  assert_not recipe.valid?
  assert_includes recipe.errors[:title], "can't be blank"
end
```

Do the same for all other `Recipe.new` examples in that file (negative prep time, negative servings, whitespace-only description, `printable?` false case and strip title). Keep `recipes(:pancakes)` as-is for fixture-based tests since you have already updated the fixture to make pancakes belong to Alice.

`user: users(:alice)` satisfies the not null constraint on `user_id` (or `belongs_to :user`) so the failure you assert is still prep time (or title, servings, description), not a missing owner.

Run the model tests to ensure they pass, you should see 0 failures and 0 errors:

```bash
bin/rails test test/models
```

### Controller create: assign the signed-in owner

If a signed-in user creates a new recipe, it should belong to that user (or owner of the recipe). Open `app/controllers/recipes_controller.rb` and update `create` action so that instead of using `Recipe.new` on create, it uses `current_user.recipes.build`:

```ruby
# app/controllers/recipes_controller.rb
class RecipesController < ApplicationController
  # ... existing code

  def create
    @recipe = current_user.recipes.build(recipe_params)
    # ... existing save and respond logic
  end

  # ... existing actions
end
```

`current_user.recipes.build` sets `user_id` from the session before saving the recipe to the database. Create tests (both integration and system) that already sign in as Alice should start passing again.

`current_user` comes from the `Authentication` concern we had added in [Chapter 12](/guide/testing-authentication/#add-a-current_user-helper) so controllers and views can call `current_user` instead of `Current.user`.

Run the full test suite to ensure everything is green, you should see 0 failures and 0 errors:

```bash
bin/rails test:all
```

With the tests now passing again, commit the changes as a checkpoint before you start authorization:

```bash
git add .
git commit -m "Assign recipe owners in fixtures, model tests, and while creating a new recipe"
```

## A hobby-sized ownership check

For a tiny app like Cookbook, you can get by with a simple DIY check in the controller for authorization. Compare `@recipe.user == current_user`, redirect when it fails, and stop there. No gem and no policy file required: just a simple custom authorization check. That is a fair trade when the app is small and you know it will not grow into a multi-resource product.

In this chapter, you will start with that DIY check first so the deny test turns green. Then you will move the same rule into a separate file provided by an authorization gem, the way a production app usually encodes authorization, and delete `ensure_recipe_owner`.

### What counts as working?

| Flow | Expected? |
| --- | --- |
| Non-owner cannot update another user's recipe | Bob GETs edit for Alice's recipe and updates the title to "Hacked pancakes"; redirects to the recipe show page; title unchanged |

### Add scenarios to the test file

Open the existing integration test file `test/integration/recipe_access_integration_test.rb` you added in [Chapter 12](/guide/testing-authentication/#add-scenarios-to-the-test-file-for-guest-access-restrictions) and add the following scenario to the file:

```ruby
# test/integration/recipe_access_integration_test.rb
  # Actor: Bob, signed in
  # Starting point: Alice owns recipes(:pancakes)
  # Action: GET edit, PATCH new title
  # Expected outcome: redirect to recipe show; title unchanged
  # test "non-owner cannot update a recipe" do
  # end
```

### Red: add the deny test

Replace the body of `test "non-owner cannot update a recipe"` with the following:

```ruby
# test/integration/recipe_access_integration_test.rb
  test "non-owner cannot update a recipe" do
    sign_in_as users(:bob)
    recipe = recipes(:pancakes)

    patch recipe_url(recipe), params: { recipe: { title: "Hacked pancakes" } }
    assert_redirected_to recipe_url(recipe)
    assert_not_equal "Hacked pancakes", recipe.reload.title
  end
```

<%= render Shared::Tip.new(
  title: "New assertion",
  markdown: <<~MD
    **`assert_not_equal expected, actual`** asks "do these two values differ?"
    Syntax: `assert_not_equal` with expected first, actual second (`assert_not_equal "Hacked pancakes", recipe.reload.title`). The test fails if the database title became the hacked value.
  MD
) %>

This is what's happening in the code above:

- The test signs in as Bob first.
- It then tries to patch the title of a recipe owned by another user to "Hacked pancakes".
- Finally it asserts that the title has not changed and Bob is redirected to the recipe show page again.

Run the test:

```bash
bin/rails test test/integration/recipe_access_integration_test.rb -i test_non-owner_cannot_update_a_recipe
```

You want a failure because the controller hasn't enforced ownership which means Bob can still PATCH Alice's recipe.

```bash
F

Failure:
RecipeAccessIntegrationTest#test_non-owner_cannot_update_a_recipe [test/integration/recipe_access_integration_test.rb:58]:
Expected response to be a <3XX: redirect>, but was a <200: OK>
```

### Green: add `ensure_recipe_owner` on the controller

Open `app/controllers/recipes_controller.rb` and add an owner check on edit, update, and destroy after the `before_action :set_recipe`. Also add `ensure_recipe_owner` as a private method just below the `set_recipe` method:

```ruby
# app/controllers/recipes_controller.rb
class RecipesController < ApplicationController
  # ... existing code
  before_action :set_recipe, only: %i[show edit update destroy]
  before_action :ensure_recipe_owner, only: %i[edit update destroy]

  # ... existing actions

  private

  def set_recipe
    @recipe = Recipe.find(params[:id])
  end

  def ensure_recipe_owner
    return if @recipe.user == current_user

    redirect_to @recipe, alert: "You are not authorized to modify this recipe."
  end

  # ... existing private methods
end
```

This is what's happening in the code above:

- `ensure_recipe_owner` runs after `set_recipe` so `@recipe` is loaded and you can compare `@recipe.user` to `current_user`. It is only called on edit, update, and destroy actions.
- Non-owners are redirected to the recipe show page with an authorization error flash message.
- `create` and `new` do not use `ensure_recipe_owner` since any signed-in user can still create a new recipe.

Run the deny test again, you should see 0 failures and 0 errors:

```bash
bin/rails test test/integration/recipe_access_integration_test.rb -i test_non-owner_cannot_update_a_recipe
```

This inline guard is enough for a tiny app with one owned resource. On a client project, or anything you expect to grow, I would not leave the comparison in the controller. After one more UI fix for flash messages, you will move the authorization check to a separate file provided by an authorization gem and delete `ensure_recipe_owner`.

<%= render Guide::LoadDevelopmentFixtures.new %>

But before moving on, quickly surf the feature in the browser to ensure it works as expected.

1. Start the server with `bin/dev` (if not already running).
2. Sign in as Bob with `bob@example.com` and `password`.
3. Open Alice's Fluffy pancakes show page, then append `/edit` to the URL.
4. You should be redirected back to the recipe show page which means the deny is working.

The denial and authorization is working but if you look carefully, you probably did not see the authorization message set by the controller `alert: "You are not authorized to modify this recipe."`. Sign-in failures showed alerts on the login form in [Chapter 12](/guide/testing-authentication/), so it is easy to assume flash already works everywhere. It does not, it worked in the login page because it has a custom code to render the alert. You will fix that next so authorization messages or any other flash messages are displayed on every page.

## Show flash messages app-wide

Today the Cookbook app only prints flash messages in a few views:

- `app/views/sessions/new.html.erb` (alert and notice for sign-in)
- `app/views/passwords/new.html.erb` and `app/views/passwords/edit.html.erb` (alerts for reset)
- `app/views/recipes/index.html.erb` and `app/views/recipes/show.html.erb` (scaffold `notice` only, in green; no alerts)

That is why Bob's authorization alert never appeared on the recipe show page. The message was in the session, but the show template never printed `flash[:alert]`.

### Add flash to the application layout

Open `app/views/layouts/application.html.erb` and render both keys just above `<%%= yield %>`, next to the Sign in / Sign out block you added in Chapter 12:

```erb
<%%# app/views/layouts/application.html.erb %>
<body>
  <%% if authenticated? %>
    <p>Signed in as <%%= current_user.email_address %></p>
    <%%= button_to "Sign out", session_path, method: :delete %>
  <%% else %>
    <%%= link_to "Sign in", new_session_path if request.path != new_session_path %>
  <%% end %>

  <%%= tag.div(flash[:alert], style: "color: red") if flash[:alert] %>
  <%%= tag.div(flash[:notice], style: "color: green") if flash[:notice] %>

  <%%= yield %>
</body>
```

This is what's happening in the code above:

- `flash[:alert]` is the red path (failed sign-in, authorization deny, mismatched password confirmation).
- `flash[:notice]` is the green path (signed-in success messages, "Recipe was successfully created.", password reset sent).
- Putting both in the layout means every page can show either key after a redirect, including recipe show after `ensure_recipe_owner`.

### Remove one-off flash from individual views

With the application layout owning flash, you can leave the per-page copies behind so messages do not print twice.

Delete these two lines from the top of `app/views/sessions/new.html.erb`:

```erb
<%%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
<%%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
```

Delete the alert line from `app/views/passwords/new.html.erb` and from `app/views/passwords/edit.html.erb`:

```erb
<%%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
```

Delete the scaffold notice line from the top of `app/views/recipes/index.html.erb` and `app/views/recipes/show.html.erb`:

```erb
<p style="color: green"><%%= notice %></p>
```

### Surf deny checks again

Surf the deny path for Bob to confirm the authorization message is visible on a recipe page:

1. Start the server with `bin/dev` (if not already running).
2. Sign in as Bob with `bob@example.com` and `password`.
3. Open Alice's Fluffy pancakes show page, then append `/edit` to the URL.
4. You should be redirected back to the recipe show page and also see the red alert with the message: "You are not authorized to modify this recipe."

While you are at it, also confirm the sign-in failures and password resets still show alerts on the login form.

For the sign-in failures:

1. Sign out of the app.
2. Try to sign in with an email address `bob@example.com` and a bad password `wrong-pass`.
3. You should be redirected back to the sign-in page and also see the red alert with the message: "Try another email address or password."

For the password resets:

1. From the login page, click the "Forgot your password?" link.
2. You should be redirected to the password reset request form.
3. Enter the email address `bob@example.com` and click the "Email reset instructions" button.
4. You should be redirected to the sign-in page and also see the green notice with the message: "Password reset instructions sent (if user with that email address exists)."

For the recipe updates:

1. Sign in as Alice with `alice@example.com` and `password`.
2. Open Alice's Fluffy pancakes show page, then append `/edit` to the URL.
3. You should be redirected to the recipe edit page.
4. Update the title to "Fluffy pancakes (updated)" and click the "Update Recipe" button.
5. You should be redirected to the recipe show page and also see the green notice with the message: "Recipe was successfully updated."

Before you move on to adding an authorization gem, ensure everything is green before committing:

```bash
bin/rails test:all
```

You want 0 failures and 0 errors. Then:

```bash
git add .
git commit -m "Render flash alerts and notices in the application layout"
```

## When to use an authorization gem

If you are building a small app or a hobby project, you can just use the inline guard like `ensure_recipe_owner` in the controller. It works when there is only one or two controllers, but if you have more resources then this customized check will start being repetitive and hard to maintain. Thus you use an authorization gem to encapsulate the authorization logic and reuse it across multiple controllers.

The two authorization gems you will hear about most in the Rails ecosystem are Pundit and CanCanCan. Pundit is a simple gem that keeps one plain Ruby policy class per model. CanCanCan is a more powerful gem that puts ability rules in one (often large) class and is a bit more complex than Pundit.

| Gem | Shape |
| --- | --- |
| [Pundit](https://github.com/varvet/pundit?utm_source=minitestrails.com) | One plain Ruby policy class per model (`RecipePolicy#update?`) |
| [CanCanCan](https://github.com/CanCanCommunity/cancancan?utm_source=minitestrails.com) | Ability rules in one (often large) class |

I mostly prefer Pundit in my projects due to its simplicity. But recently I have also seen people (and me) adopt another similar gem called [Action Policy](https://actionpolicy.evilmartians.io?utm_source=minitestrails.com) developed and maintained by [Evil Martians](https://evilmartians.com?utm_source=minitestrails.com). It was inspired by Pundit and brings some additional features also from CanCanCan. This is what their website says about it:

> Pundit has been our framework of choice for a long time. Being too dead-simple, it required a lot of hacking to fulfill business logic requirements.
>
> These hacks later became Action Policy (initially, we even called it "Pundit, re-visited").
>
> We also took a few ideas from CanCanCan—such as default rules and rule aliases.

This guide also uses Action Policy for Authorization. Next, you will migrate the custom authorization check to Action Policy so you can see how you can test authorization in a production app.

## Add Action Policy

Install the gem and generate the base policy class:

```bash
bundle add action_policy
bin/rails generate action_policy:install
```

This is what's happening in the commands above:

- `bundle add action_policy` puts the gem in the Gemfile and installs it.
- `action_policy:install` creates `app/policies/application_policy.rb` that can be used for adding global permissions and rules for all policies. The idea is to keep the global rules in one place and then override them in the specific policies for each resource.

### Handle unauthorized actions

Once you have installed the gem, it exposes `authorize!` and `allowed_to?` helper methods in controllers and views. When you use `authorize!` in a controller, it will raise `ActionPolicy::Unauthorized` if the user is not authorized to perform the action (similar to `ensure_recipe_owner`). You can then rescue from this exception and handle it in the application controller by adding the following code to the `app/controllers/application_controller.rb`.

Handling it in the application controller means other controllers will automatically inherit the same behavior so you don't need to repeat the same code in each controller.

Update the application controller to rescue from `ActionPolicy::Unauthorized` and redirect the user back to the previous page with a flash message:

```ruby
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # ... existing code

  # Changes to the importmap will invalidate the etag for HTML responses
  stale_when_importmap_changes

  rescue_from ActionPolicy::Unauthorized, with: :user_not_authorized

  private

  def user_not_authorized(_exception)
    flash[:alert] = "You are not authorized to perform this action."

    redirect_back fallback_location: root_path
  end
end
```

This is what's happening in the code above:

- Action Policy uses `current_user` as the policy user; the helper you added in Chapter 12.
- `rescue_from ActionPolicy::Unauthorized` turns a failed `authorize!` into a flash message to notify users that they are not authorized to perform the action.
- `redirect_back fallback_location: root_path` redirects the user back to the previous page with a flash message. It falls back to the root path if the previous page is not available.

### Rules and permissions refresher

Before you generate the policy for recipes, refresh your memory on the permissions and rules in the Cookbook app so you understand what to put in the policy file.

| Resource | Action | Rules | Guest | Signed-in | Owner | Non-owner |
| --- | --- | --- | --- | --- | --- | --- |
| Recipes | Index, Show | Anyone can view the list and details of all recipes. | Allowed | Allowed | Allowed | Allowed |
| Recipes | New, Create | Only signed-in users can create new recipes. | Denied | Allowed | - | - |
| Recipes | Update | Only owners can update their own recipes. | Denied | Denied | Allowed | Denied |
| Recipes | Destroy | Only owners can destroy their own recipes. | Denied | Denied | Allowed | Denied |
| Ingredients | Destroy | Only owners can destroy their own ingredients. | Denied | Denied | Allowed | Denied |
| Steps | Destroy | Only owners can destroy their own steps. | Denied | Denied | Allowed | Denied |

<%= render Shared::Tip.new(
  title: "Tip",
  markdown: <<~MD
    The dash (**`-`**) in New, Create means it doesn't apply to the owner/non-owner as any signed in users can create a new recipe. And owner only gets established after the recipe is created.
  MD
) %>

### Tighten application policy rules

Open `app/policies/application_policy.rb` and update the rules to ensure none of the actions are allowed by default (more on the why below). Replace the existing code with the following:

```ruby
# app/policies/application_policy.rb
# Base class for application policies
class ApplicationPolicy < ActionPolicy::Base
  # Configure additional authorization contexts here
  # (`user` is added by default).
  #
  #   authorize :account, optional: true
  #
  # Read more about authorization context: https://actionpolicy.evilmartians.io/#/authorization_context

  def index?
    false
  end

  def show?
    false
  end

  def create?
    false
  end

  def new?
    create?
  end

  def update?
    false
  end

  def edit?
    update?
  end

  def destroy?
    false
  end

  private

  def owner?
    record.user_id == user.id
  end
end
```

Denying all actions is a good default because it's fine if a user with permission gets blocked but it's a huge security risk if a user without permission gets through. That's why global policy should always be very strict and narrow.

`owner?` is a shared method that you can use in other policies to check if the owner (or user_id) of the record is the logged-in / current user or not.

### Generate a policy for recipes

Run the following command to generate the policy file for recipes:

```bash
bin/rails generate action_policy:policy Recipe
```

The generator also creates a test file for the policy at `test/policies/recipe_policy_test.rb`. This chapter proves authorization through HTTP redirects, row counts, and rendered HTML in the access integration file, instead of calling policy methods from a unit test. So, I recommend you to delete the file with the following command:

```bash
rm test/policies/recipe_policy_test.rb
```

Replace the generated `app/policies/recipe_policy.rb` with the following:

```ruby
# app/policies/recipe_policy.rb
class RecipePolicy < ApplicationPolicy
  # See https://actionpolicy.evilmartians.io/#/writing_policies
  #

  def index?
    true
  end

  def show?
    true
  end

  def create?
    user.present?
  end

  def new?
    create?
  end

  def update?
    owner?
  end

  def edit?
    update?
  end

  def destroy?
    owner?
  end

  # Scoping
  # See https://actionpolicy.evilmartians.io/#/scoping
  #
  # relation_scope do |relation|
  #   next relation if user.admin?
  #   relation.where(user: user)
  # end
end
```

This is what's happening in the code above:

- `RecipePolicy` inherits from `ApplicationPolicy`, `owner?` method comes from there.
- `index?` and `show?` are true for everyone. The Cookbook app has no restrictions on viewing the list of recipes or the details of a recipe.
- `create?` and `new?` are true for any signed-in user (`user.present?`). Guests never reach those actions because Authentication still runs. `user` is provided by the Action Policy by default so you can call it here without any additional setup.
- `edit?`, `update?`, and `destroy?` are true only when the current user is an owner (recipe's user is the current user). That is the same comparison as `ensure_recipe_owner` you previously implemented in the recipes controller.
- `update?` and `destroy?` both call `owner?` directly. Normally in blogs and production apps, you will see `destroy?` calling the `update?` permission instead of `owner?`. But we are not following that here because editing a recipe and deleting a recipe are two features, and it's best to keep each rule answering its own question. Reason: if you later loosen `update?` (say a collaborator may rename a recipe), you don't want that change to quietly hand the same person the ability to delete the recipe! `edit?` staying an alias of `update?` is fine, since the edit form and the PATCH it submits are one feature.

`relation_scope` is not used in this guide but it's a good way to filter out (or scope) the records the user can see in the list view. For example, in many apps you may want to show only the recipes that the current user owns. You can do this by adding the following code to the `RecipePolicy`:

```ruby
relation_scope do |relation|
  next relation if user.admin?

  relation.where(user: user)
end
```

That will allow user with admin role to see all recipes but only allows the user with regular role to see the recipes they own.

<%= render Shared::Tip.new(
  title: "Tip",
  markdown: <<~MD
    Integration tests in this chapter assert page redirects, title of the page, row counts, and HTML part of the page for testing authorization (outcomes). They do not call `RecipePolicy` from the test nor assert Action Policy method return values (implementation). The HTTP and the rendered HTML are the actual proof of the authorization logic.
  MD
) %>

### Replace `ensure_recipe_owner` with `authorize!`

Open RecipesController at `app/controllers/recipes_controller.rb` and remove `before_action :ensure_recipe_owner, only: %i[edit update destroy]` you had previously added after `before_action :set_recipe`. Also search for the method `def ensure_recipe_owner` and remove it. This guard is no longer needed because you will use `authorize!` to ensure only the owner can modify or destroy the recipe.

Done with it? Onto the next step then.

Now, update new, create, edit, update, and destroy actions to use `authorize!`:

```ruby
# app/controllers/recipes_controller.rb
class RecipesController < ApplicationController
  allow_unauthenticated_access only: %i[index show]
  before_action :set_recipe, only: %i[show edit update destroy]

  def index
    # ... existing code
  end

  def show
    # ... existing code
  end

  def new
    @recipe = Recipe.new

    authorize! @recipe

    # ... existing code
  end

  def create
    @recipe = current_user.recipes.build(recipe_params)

    authorize! @recipe

    # ... existing code
  end

  def edit
    authorize! @recipe

    # ... existing code
  end

  def update
    authorize! @recipe

    # ... existing code
  end

  def destroy
    authorize! @recipe

    # ... existing code
  end

  private

  # ... existing code
end
```

This is what's happening in the code above:

- `authorize! @recipe` infers `new?`, `create?`, `edit?`, `update?`, or `destroy?` from the controller action name (`update` maps to `update?`). You can also pass the rule explicitly with `to:` (for example `authorize! @recipe, to: :destroy?`) which helps when the action name and the rule name for that same resource do not match (for example: you had `archive` action for recipes that used `destroy?` rule).
- Failed checks raise `ActionPolicy::Unauthorized`, which you have already rescued globally in the application controller and shows a flash message to the user.
- Show and index stay public. You do not need to call `authorize!` there.

Re-run the authorization test you wrote previously for "Non-owner cannot update a recipe". It should fail at the redirect assertion because Action Policy now redirects with `redirect_back fallback_location: root_path` instead of sending them to the recipe show page:

```bash
bin/rails test test/integration/recipe_access_integration_test.rb -i test_non-owner_cannot_update_a_recipe
```

You will see 1 failure:

```
F

Failure:
RecipeAccessIntegrationTest#test_non-owner_cannot_update_a_recipe [test/integration/recipe_access_integration_test.rb:58]:
Expected response to be a redirect to <http://www.example.com/recipes/507601372?utm_source=minitestrails.com> but was a redirect to <http://www.example.com?utm_source=minitestrails.com>.
Expected "http://www.example.com/recipes/507601372" to be === "http://www.example.com/".
```

Update the test at `test/integration/recipe_access_integration_test.rb` to expect the redirect to the root path instead of the recipe show page:

```ruby
# test/integration/recipe_access_integration_test.rb
test "non-owner cannot update a recipe" do
  # ... existing code
  get recipe_url(recipe)
  assert_redirected_to root_path

  patch recipe_url(recipe), params: { recipe: { title: "Hacked pancakes" } }
  assert_redirected_to root_path
  get recipe_url(recipe)
  assert_response :success
  assert_not_equal "Hacked pancakes", recipe.reload.title
end
```

You also need to add a `get recipe_url(recipe)` to ensure the recipe wasn't updated since we are now redirecting to the root path (recipe list page) instead of the recipe show page.

Run the test again, you should now have 0 failures and 0 errors.

```bash
bin/rails test test/integration/recipe_access_integration_test.rb -i test_non-owner_cannot_update_a_recipe
```

## Non-owner cannot modify the recipe

You already proved one PATCH deny for Bob in the custom authorization guard example in the hobby project. But editing also needs a deny for the edit form itself, the page a curious (or malicious) signed-in user can open by typing `/recipes/1/edit` directly in the address bar.

You do not need to add a test for `"owner can update a recipe"` in this file because it is already covered by `test "updates a recipe"` in `test/integration/recipes_integration_test.rb`. That test signs in as Alice, opens edit, PATCHes a title, and checks the show page. That test should still pass even after adding the `authorize!`. This chapter only adds the non-owner and guest edge cases not covered previously by the `recipes_integration_test.rb` or `recipe_access_integration_test.rb`.

You also don't need to add a guest edit test here. [Chapter 12](/guide/testing-authentication/#add-the-access-integration-tests) already proves an unsigned GET edit redirects to sign-in in `guest cannot edit a recipe`, and that path never reaches Action Policy.

### What counts as working?

| Flow | Expected |
| --- | --- |
| Non-owner opens the edit form | Bob GETs edit for Alice's Fluffy pancakes and lands on the recipes list (root path) |
| Non-owner PATCHes a title | Already in this file from the hobby step (`non-owner cannot update a recipe`), redirecting to the root path |
| Owner updates a title | Already in `recipes_integration_test.rb` as `updates a recipe` (Alice signed in). Leave that test alone. |
| Guest opens the edit form | Already in `recipe_access_integration_test.rb` as `guest cannot edit a recipe`. Leave that test alone. |

### Add scenarios to the test file

Update the existing test "non-owner cannot update a recipe" to also check for edit form restriction:

```ruby
# test/integration/recipe_access_integration_test.rb
  # Actor: Bob, signed in
  # Starting point: Alice owns recipes(:pancakes)
  # Action: GET the edit form for Alice's Fluffy pancakes; PATCH the title to "Hacked pancakes"
  # Expected outcome: redirect to the root path; title unchanged in the database
  # test "non-owner cannot update a recipe" do
  # end
```

### Add the integration test

Replace the body of the test with the following:

```ruby
# test/integration/recipe_access_integration_test.rb
  test "non-owner cannot update a recipe" do
    sign_in_as users(:bob)
    recipe = recipes(:pancakes)

    get edit_recipe_url(recipe)
    assert_redirected_to root_path

    patch recipe_url(recipe), params: { recipe: { title: "Hacked pancakes" } }
    assert_redirected_to root_path
    get recipe_url(recipe)
    assert_response :success
    assert_not_equal "Hacked pancakes", recipe.reload.title
  end
```

This is what's happening in the code above:

- After signing in to the app, Bob GETs the edit URL directly, the path a curious user could bookmark or guess.
- The redirect target is `root_path` because `user_not_authorized` calls `redirect_back fallback_location: root_path`. In the Cookbook app the root path is the recipes list, so Bob lands on a page they are allowed to see.
- Other assertions are the same from the previous test "non-owner cannot update a recipe": test PATCHes the title and asserts the recipe title is not changed.

Run the access file:

```bash
bin/rails test test/integration/recipe_access_integration_test.rb
```

While you are at it, spot-check the existing owner update test:

```bash
bin/rails test test/integration/recipes_integration_test.rb -i test_updates_a_recipe
```

You want 0 failures and 0 errors on both as the restrictions and permissions are already added to the recipe policy.

You already surfed through the browser to confirm the authorization message is visible on a recipe page for this feature after the authorization check for hobby project so you don't need to do it again here.

## Non-owner cannot destroy the recipe

Deleting is the action people worry about the most in any app and rightfully so because it can lead to a result that is unrecoverable. A wrong PATCH can be edited back but a wrong DELETE takes the recipe, its ingredients, and its steps with it with no hope of recovery. So, it's given that we add a strict authorization check for this action.

Similar to the update path, you do not need to add a test for `"owner can destroy a recipe"` in this file because it is already covered by `test "destroys a recipe"` in `test/integration/recipes_integration_test.rb`. That test signs in as Alice, DELETEs `recipes(:lentil_soup)`, asserts `Recipe.count` drops by one, follows to the list, and checks the remaining recipe rows.

The guest delete path is also already covered by `guest cannot destroy a recipe` in `test/integration/recipe_access_integration_test.rb`.

### What counts as working?

| Flow | Expected |
| --- | --- |
| Non-owner deletes a recipe | Bob DELETEs Alice's Fluffy pancakes, `Recipe.count` is unchanged and lands on the root path |
| Owner deletes a recipe | Already covered by `destroys a recipe` in `recipes_integration_test.rb` |
| Guest deletes a recipe | Already covered by `guest cannot destroy a recipe` in `recipe_access_integration_test.rb` |

### Add the scenario and test

Add the following scenario to the `recipe_access_integration_test.rb` file:

```ruby
# test/integration/recipe_access_integration_test.rb
  # Actor: Bob, signed in
  # Starting point: Alice owns recipes(:pancakes)
  # Action: DELETE pancakes
  # Expected outcome: recipe count unchanged; redirect to the root path
  # test "non-owner cannot destroy a recipe" do
  # end
```

Then replace the body of the test with the following:

```ruby
# test/integration/recipe_access_integration_test.rb
  test "non-owner cannot destroy a recipe" do
    sign_in_as users(:bob)
    recipe = recipes(:pancakes)

    assert_no_difference("Recipe.count") do
      delete recipe_url(recipe)
    end
    assert_redirected_to root_path
  end
```

This is what's happening in the code above:

- The deny wraps `delete` in `assert_no_difference("Recipe.count")` to ensure "the row is still there" after the delete action.
- Since Bob is not authorized to delete Alice's recipe, they are redirected back to the root path.

Run the test:

```bash
bin/rails test test/integration/recipe_access_integration_test.rb
```

You want 0 failures and 0 errors. The deny passes right away because `RecipePolicy#destroy?` asks `owner?` and Bob is not the owner of Alice's recipe.

While you are at it, ensure the existing test for owner deleting their recipe also passes:

```bash
bin/rails test test/integration/recipes_integration_test.rb -i test_destroys_a_recipe
```

You should see 0 failures and 0 errors.

<%= render Guide::LoadDevelopmentFixtures.new %>

Before you move on, quickly surf through the browser to ensure the authorization for non-owner not able to delete a recipe works as expected.

1. If you already have the server running, stop it and restart back again with `bin/dev`. This is required because you added ActionPolicy gem and without restarting the server, the changes will not be loaded and even may throw errors.
2. Sign in as Bob with the credentials `bob@example.com` / `password`.
3. Open the recipe show page for Alice's Fluffy pancakes
4. Click the Destroy button
5. You should be redirected with an authorization error message: "You are not authorized to perform this action."
6. Reload the page and you should see the recipe is still there.

## Non-owner cannot destroy ingredients and steps

Remove button for Ingredients and Steps you added in [Chapter 11](/guide/testing-turbo-frames-and-streams/#green-nested-destroy-routes-controllers-and-remove-on-show) still only look at if the user is signed in or not (similar to how recipe actions worked at the start of this chapter). So, any user that is signed in to the app can delete any ingredient or step on any recipe right now; the behavior you added in [Chapter 12](/guide/testing-authentication/#green-allow-guest-browse-and-hide-buttons-to-modify-a-recipe).

`RecipesController` is now locked with the proper authorization checks in the RecipePolicy file, but Bob can still sign in and send a DELETE to `/recipes/1/ingredients/2` to take Salt out of Alice's pancakes. This is what you will fix next.

Cookbook has two ways to delete a nested record for ingredients and steps from the recipe:

1. The show page Remove button hits `IngredientsController#destroy` / `StepsController#destroy`
2. The recipe edit form has Remove button that marks `_destroy` and goes through `RecipesController#update`.

You do not need to add happy path tests for owner being able to remove ingredients and steps, it has already been covered in `recipes_integration_test.rb` with `removes ingredients and steps from the detail page` (Turbo Stream DELETEs) and `removes existing ingredients and steps when updating a recipe` (PATCH with `_destroy`). Those should stay green even after the authorize checks land in IngredientsController and StepsController.

### What counts as working?

| Flow | Expected |
| --- | --- |
| Guest removes ingredient or step from the detail page | No session, so each DELETE redirects to sign-in and the child counts are unchanged |
| Guest removes ingredient or step when updating a recipe | No session, so PATCH redirects to sign-in and the child counts are unchanged |
| Non-owner removes ingredient or step from the detail page | Bob DELETEs Alice's Salt (ingredient) and preheat (step), lands on the root path; both child counts are unchanged |
| Non-owner removes ingredient or step when updating a recipe | Bob PATCHes Alice's pancakes with `_destroy` for Salt (ingredient) and preheat (step), lands on the root path; both child counts are unchanged |
| Owner removes ingredient or step from the detail page | Already covered by `removes ingredients and steps from the detail page` in `recipes_integration_test.rb` |
| Owner removes ingredient or step when updating a recipe | Already covered by `removes existing ingredients and steps when updating a recipe` in `recipes_integration_test.rb` |

### Use existing policy or create a new policy for ingredients and steps?

There are two options to approach authorization for nested resources:

1. Use the existing recipe policy for ingredients and steps.

    This is a very tempting shortcut, you can do this by calling `authorize! @recipe, to: :update?` in the ingredients and steps controllers. This will reuse the recipe rule you already have for the update action.
    
    It perfectly works today but can bite you bitterly in future. Removing an ingredient would be wired to a rule that exists for a different feature, the recipe edit form. The day you loosen `update?`, say you let a collaborator rename a shared recipe, you also hand that person the Remove button on every ingredient and step, and nothing in the recipe tests goes red to warn you. The same coupling shows up if the child policy calls `allowed_to?(:update?, record.recipe)` internally.

2. Create new policies each for ingredients and steps.

    This is the option I like the most and a better approach than just reusing existing policy of a different resource. It's additional work upfront but something that is worth it in the long run. With this option, you  give each resource its own policy file so every rule answers one question for one resource. This will ensure a change to one feature only touches that particular feature and authorization stays tight and narrow to restrict unauthorized access.

In this guide, you will use the second option and create a new policy for ingredients and steps.

### Add scenarios to the test file

Append the following scenarios to the `recipe_access_integration_test.rb` file:

```ruby
# test/integration/recipe_access_integration_test.rb
  # Actor: Guest, no session
  # Starting point: Alice owns recipes(:pancakes) with ingredients(:salt) and steps(:preheat)
  # Action: DELETE the salt ingredient; DELETE the preheat step
  # Expected outcome: ingredient count unchanged; step count unchanged; redirect to sign-in
  # test "guest cannot remove an ingredient or a step from the detail page" do
  # end

  # Actor: Guest, no session
  # Starting point: Alice owns recipes(:pancakes) with ingredients(:salt) and steps(:preheat)
  # Action: PATCH pancakes with _destroy for Salt and the preheat step
  # Expected outcome: ingredient count unchanged; step count unchanged; redirect to sign-in
  # test "guest cannot remove an ingredient or a step when updating a recipe" do
  # end

  # Actor: Bob, signed in
  # Starting point: Alice owns recipes(:pancakes) with ingredients(:salt) and steps(:preheat)
  # Action: DELETE the salt ingredient; DELETE the preheat step
  # Expected outcome: ingredient count unchanged; step count unchanged; redirect to the root path
  # test "non-owner cannot remove an ingredient or a step from the detail page" do
  # end

  # Actor: Bob, signed in
  # Starting point: Alice owns recipes(:pancakes) with ingredients(:salt) and steps(:preheat)
  # Action: PATCH pancakes with _destroy for Salt and the preheat step
  # Expected outcome: ingredient count unchanged; step count unchanged; redirect to the root path
  # test "non-owner cannot remove an ingredient or a step when updating a recipe" do
  # end
```

### Red: Add the integration tests

Replace the body of the new tests with the following:

```ruby
# test/integration/recipe_access_integration_test.rb
  test "guest cannot remove an ingredient or a step from the detail page" do
    recipe = recipes(:pancakes)
    salt = ingredients(:salt)
    preheat = steps(:preheat)

    assert_no_difference("Ingredient.count") do
      delete recipe_ingredient_url(recipe, salt), as: :turbo_stream
    end
    assert_redirected_to new_session_url

    assert_no_difference("Step.count") do
      delete recipe_step_url(recipe, preheat), as: :turbo_stream
    end
    assert_redirected_to new_session_url

    get recipe_url(recipe)
    assert_response :success
    assert_match salt.name, response.body
    assert_match preheat.instruction, response.body
  end

  test "guest cannot remove an ingredient or a step when updating a recipe" do
    recipe = recipes(:pancakes)
    salt = ingredients(:salt)
    preheat = steps(:preheat)

    assert_no_difference("Ingredient.count", "Step.count") do
      patch recipe_url(recipe),
            params: {
              recipe: {
                ingredients_attributes: {
                  "0" => {
                    id: salt.id,
                    _destroy: "1"
                  }
                },
                steps_attributes: {
                  "0" => {
                    id: preheat.id,
                    _destroy: "1"
                  }
                }
              }
            }
    end
    assert_redirected_to new_session_url

    get recipe_url(recipe)
    assert_response :success
    assert_match salt.name, response.body
    assert_match preheat.instruction, response.body
  end

  test "non-owner cannot remove an ingredient or a step from the detail page" do
    sign_in_as users(:bob)
    recipe = recipes(:pancakes)
    salt = ingredients(:salt)
    preheat = steps(:preheat)

    assert_no_difference("Ingredient.count") do
      delete recipe_ingredient_url(recipe, salt), as: :turbo_stream
    end
    assert_redirected_to root_path

    assert_no_difference("Step.count") do
      delete recipe_step_url(recipe, preheat), as: :turbo_stream
    end
    assert_redirected_to root_path

    get recipe_url(recipe)
    assert_response :success
    assert_match salt.name, response.body
    assert_match preheat.instruction, response.body
  end

  test "non-owner cannot remove an ingredient or a step when updating a recipe" do
    sign_in_as users(:bob)
    recipe = recipes(:pancakes)
    salt = ingredients(:salt)
    preheat = steps(:preheat)

    assert_no_difference %w[Ingredient.count Step.count] do
      patch recipe_url(recipe),
            params: {
              recipe: {
                title: recipe.title,
                ingredients_attributes: {
                  "0" => {
                    id: salt.id,
                    _destroy: "1"
                  }
                },
                steps_attributes: {
                  "0" => {
                    id: preheat.id,
                    _destroy: "1"
                  }
                }
              }
            }
    end
    assert_redirected_to root_path

    get recipe_url(recipe)
    assert_response :success
    assert_match salt.name, response.body
    assert_match preheat.instruction, response.body
  end
```

This is what's happening in the code above:

- `assert_no_difference("Ingredient.count")` and `assert_no_difference("Step.count")` wrap the detail-page deletes to ensure the rows are still there after each DELETE.
- The guest test signs nobody in, so `Authentication` from Chapter 12 stops the request before any policy runs. That is why it expects `new_session_url` and not the root path.
- `as: :turbo_stream` matches how the Remove button submits the request to the server, the same request format you used for these routes in [Chapter 11](/guide/testing-turbo-frames-and-streams/#green-nested-destroy-routes-controllers-and-remove-on-show). The controllers respond with a Turbo Stream that removes the list item.
- The update deny sends the same nested `_destroy` params as `removes existing ingredients and steps when updating a recipe` in `recipes_integration_test.rb`, but Bob is the actor. `RecipesController#update` hits `authorize! @recipe` before nested attributes run, so this path is denied by `RecipePolicy#update?`, not by `IngredientPolicy` or `StepPolicy`.
- The show-page assertions after the denies check that Salt and the preheat step are still visible.

Run the full access integration test:

```bash
bin/rails test test/integration/recipe_access_integration_test.rb
```

Guest related tests should pass because Authentication stops the request before any policy runs. The update deny should also pass because `RecipePolicy#update?` is in place from earlier in this chapter. The non-owner detail-page test should fail with the following message:

```bash
F

Failure:
RecipeAccessIntegrationTest#test_non-owner_cannot_remove_an_ingredient_or_a_step_from_the_detail_page [test/integration/recipe_access_integration_test.rb:135]:
`Ingredient.count` didn't change by 0, but by -1.
Expected: 2
  Actual: 1
```

The detail-page deny fails because Bob is not authorized on `IngredientsController#destroy` and `StepsController#destroy` yet. You need to fix that by creating policies for ingredients and steps and calling `authorize!` in those controllers.

### Green: generate a policy for ingredients and steps

Generate policy files each for ingredients and steps with the following command in the terminal:

```bash
bin/rails generate action_policy:policy Ingredient --no-test-framework
bin/rails generate action_policy:policy Step --no-test-framework
```

`--no-test-framework` skips tests files for ingredients and steps. This option needs to be passed because we don't want to write policy unit tests as we rely on the integration tests to prove authorization which ensures authorization is working in a full request/response cycle.

Replace the generated `app/policies/ingredient_policy.rb` with the following:

```ruby
# app/policies/ingredient_policy.rb
class IngredientPolicy < ApplicationPolicy
  def destroy?
    recipe_owner?
  end

  private

  def recipe_owner?
    owner?(another_record: record.recipe)
  end
end
```

Then do the same for `app/policies/step_policy.rb`:

```ruby
# app/policies/step_policy.rb
class StepPolicy < ApplicationPolicy
  def destroy?
    recipe_owner?
  end

  private

  def recipe_owner?
    owner?(another_record: record.recipe)
  end
end
```

This is what's happening in the code above:

- Both policies are only defining the `destroy?` rule as the Cookbook app only exposes destroy on these two resources, so `destroy?` is the only rule worth writing today. Everything else is denied by default because `ApplicationPolicy` returns false for every rule.
- `record` is the ingredient or the step, so ownership is one hop away through `record.recipe`.
- The private `recipe_owner?` method calls `owner?` with the recipe record as an argument, this is because the ingredient and step rows have no `user_id` column and need to derive the ownership from the parent recipe record.

IngredientPolicy and StepPolicy call `owner?` from `ApplicationPolicy` with the parent recipe but the parent recipe is not yet allowed to be passed as an argument. The `owner?` helper you wrote earlier only looked at `record.user_id`, so update it to accept an optional `another_record:` or you will get an error once you wire up `authorize!` in IngredientsController and StepsController:

```ruby
# app/policies/application_policy.rb
class ApplicationPolicy < ActionPolicy::Base
  # ... existing code

  private

  def owner?(another_record: nil)
    return record.user_id == user.id if another_record.nil?

    another_record.user_id == user.id
  end
end
```

This is what's happening in the code above:

- If the `another_record` is `nil`, it checks the record's `user_id` directly.
- If the `another_record` is passed as an argument, it derives the `user_id` from that another_record.

<%= render Shared::Tip.new(
  title: "Heads up!",
  markdown: <<~MD
    `user.id` inside a policy assumes somebody is signed in. That holds true in the Cookbook app because all the controllers that are using `authorize!` never call `allow_unauthenticated_access` which means these controllers already expect the user to be signed in to the app before accessing any actions inside them.
    
    If you ever open one of these actions to guests, `user` becomes `nil` and `user.id` will raise an error. You will need to add `return false if user.blank?` at the top of the method before any `user.id` check when that day comes.
  MD
) %>

Finally, update the controllers to call `authorize!` so they check authorization before the row is destroyed:

```ruby
# app/controllers/ingredients_controller.rb
def destroy
  authorize! @ingredient

  @ingredient.destroy!
  # ... existing turbo_stream remove
end
```

```ruby
# app/controllers/steps_controller.rb
def destroy
  authorize! @step

  @step.destroy!
  # ... existing turbo_stream remove
end
```

This is what's happening in the code above:

- `authorize! @ingredient` picks `IngredientPolicy` from the record's class and `destroy?` from the action name; same as what you did in `RecipesController`. And it's the same for `authorize! @step`.
- The failed check raises `ActionPolicy::Unauthorized`, which the `rescue_from` in `ApplicationController` turns into the flash message and the redirect. That is why Bob's deny tests expect the root path, exactly like the recipe denies.

Run the file again, you should have 0 failures and 0 errors.

```bash
bin/rails test test/integration/recipe_access_integration_test.rb
```

While you are here, spot-check happy path tests for owner being able to remove ingredients and steps, both of them should be green:

```bash
bin/rails test test/integration/recipes_integration_test.rb -i test_removes_ingredients_and_steps_from_the_detail_page
bin/rails test test/integration/recipes_integration_test.rb -i test_removes_existing_ingredients_and_steps_when_updating_a_recipe
```

<%= render Guide::LoadDevelopmentFixtures.new %>

Before you move on, quickly surf through the browser to ensure the authorization for owner being able to remove ingredients and steps works as expected.

1. Start the server with `bin/dev` (if not already running)
2. Sign in as Bob with the credentials `bob@example.com` / `password`.
3. Open the recipe show page for Alice's Fluffy pancakes
4. Click the Remove button for Salt
5. You should be redirected with an authorization error message: "You are not authorized to perform this action."
6. Reload the page and you should see the Salt ingredient is still there.
7. Click the Remove button for the preheat step
8. You should be redirected with an authorization error message: "You are not authorized to perform this action."
9. Reload the page and you should see the preheat step is still there.

<%= render Guide::SupportCta.new(
  variant: :mid_chapter,
  site_metadata: site.data.site_metadata,
  milestone_hook: "With the authorization checks in place, Bob is signed in but will not be able to rename Alice's pancakes, delete a recipe, or delete one ingredient or step from it. The access tests prove the deny edge cases over HTTP.",
  headline: "Owner-only writes are now a tested rule, not a hope.",
  body_text: "You moved ownership out of a controller comparison into RecipePolicy, then gave ingredients and steps their own policies instead of borrowing the recipe update rule. Owner happy paths stay in `recipes_integration_test.rb`. This access file owns the non-owner and nested guest denies. If that habit is sticking, consider supporting this guide by sponsoring it or buying me a drink."
) %>

## Hide change actions for non-owners and guests

Chapter 12 hid Edit, Destroy, and Remove behind `authenticated?`, which means any signed-in user can see those buttons on any recipe. That is why Bob still sees those controls on Alice's pancakes even though the rule says a non-owner should not see modify buttons.

It's another story that clicking on any one of them redirects the user to the root path since the controllers are already equipped with authorization checks. Next step is to hide these buttons completely for users that are not authorized to see them. You will do that by adding `allowed_to?` to the recipe partial along with existing `authenticated?` check so the UI matches the policy.

Index and show both render `app/views/recipes/_recipe.html.erb` for guests and signed-in users. Action Policy requires a `user` in the authorization context. Calling `allowed_to?` alone for a guest raises `ActionView::Template::Error: Missing policy authorization context: user`. Combine authorization calls with `authenticated?` so the policy only runs when someone is signed in.

Update the Destroy button in `app/views/recipes/_recipe.html.erb`:

```erb
<%%# app/views/recipes/_recipe.html.erb %>
<%% for_show = local_assigns.fetch(:for_show, false) %>
<%% destroy_url = for_show ? recipe_path(recipe, format: :html) : recipe %>

<div id="<%%= dom_id recipe %>">
  <!-- ... existing code ... -->

  <%% if authenticated? && allowed_to?(:destroy?, recipe) %>
    <%%= button_to "Destroy this recipe", destroy_url, method: :delete, data: { turbo_confirm: "Are you sure?" } %>
  <%% end %>
</div>
```

Replace the entire `app/views/recipes/show.html.erb` template so Edit and each Remove use the same two-part check:

```erb
<%%# app/views/recipes/show.html.erb %>
<%%= render @recipe, for_show: true %>

<%% if @recipe.ingredients.any? %>
  <h2>Ingredients</h2>
  <ul id="ingredients">
    <%% @recipe.ingredients.each do |ingredient| %>
      <li id="<%%= dom_id(ingredient) %>">
        <%%= ingredient.name %><%% if ingredient.quantity.present? %> (<%%= number_with_precision(ingredient.quantity, precision: 2, strip_insignificant_zeros: true) %><%% if ingredient.unit.present? %> <%%= ingredient.unit %><%% end %>)<%% end %>
        <%% if authenticated? && allowed_to?(:destroy?, ingredient) %>
          <%%= button_to "Remove",
                recipe_ingredient_path(@recipe, ingredient),
                method: :delete,
                data: { turbo_confirm: "Remove this ingredient?" } %>
        <%% end %>
      </li>
    <%% end %>
  </ul>
<%% end %>

<%% if @recipe.steps.any? %>
  <h2>Steps</h2>
  <ol id="steps">
    <%% @recipe.steps.order(:position).each do |step| %>
      <li id="<%%= dom_id(step) %>">
        <%%= step.instruction %>
        <%% if authenticated? && allowed_to?(:destroy?, step) %>
          <%%= button_to "Remove",
                recipe_step_path(@recipe, step),
                method: :delete,
                data: { turbo_confirm: "Remove this step?" } %>
        <%% end %>
      </li>
    <%% end %>
  </ol>
<%% end %>

<div>
  <%% if authenticated? && allowed_to?(:edit?, @recipe) %>
    <%%= link_to "Edit this recipe", edit_recipe_path(@recipe) %>
  <%% end %>

  <%%= link_to "Back to recipes", recipes_path %>
</div>
```

This is what's happening in the code above:

- `authenticated?` answers "is anyone signed in?" Guests stop here and never build a policy.
- `allowed_to?` answers "may this signed-in user change this record?" Bob fails. Alice passes.
- Destroy asks about the recipe. Each Remove asks about the ingredient or step. Edit asks `edit?` on the recipe. Same rules the controllers already call.

<%= render Guide::LoadDevelopmentFixtures.new %>

Next, surf in the browser:

1. Start the server with `bin/dev` (if not already running)
2. Sign in as Bob with the credentials `bob@example.com` / `password`. Visit `http://localhost:3000/recipes`, open Fluffy pancakes. Confirm the title appears but Edit this recipe, Destroy this recipe, and Remove buttons are not present.
3. On the list while signed in as Bob, confirm Destroy this recipe is not present on any recipe (every fixture recipe belongs to Alice).
4. Sign out and sign back in as Alice with the credentials `alice@example.com` / `password`. Open the Fluffy pancakes recipe. Confirm Edit this recipe, Destroy this recipe, and Remove buttons; all of them are visible.
5. On the list while signed in as Alice, confirm Destroy this recipe still appears on a row.
6. Sign out and open the list and a recipe show as a guest. Confirm no Destroy, Edit, or Remove buttons are visible and both pages render without any error.

## Non-owner does not see change buttons

You just tested manually in the browser that the buttons to modify a recipe are not visible to non-owner (Bob) while still being visible to owner (Alice). Next, you need to automate this behavior with tests.

System tests in this guide stay on the happiest path through the app, so you will not add a system file for button visibility. Instead, you will expand the existing access integration test for that.

Destroy this recipe lives in `app/views/recipes/_recipe.html.erb`, which renders on the list and on show. Edit and Remove live only on the show template and Bob needs deny tests for both pages. Alice's owner half is already covered: `signed-in user sees new recipe on the list` finds Destroy on the index, and `signed-in user sees edit and destroy on show` finds Edit, Destroy, and Remove on pancakes.

### What counts as working?

| Flow | Expected |
| --- | --- |
| Non-owner on the list | Bob GETs `/recipes` and does not see Destroy button on any recipe (every fixture recipe belongs to Alice) |
| Non-owner on show | Bob GETs Alice's pancakes show and does not see Edit this recipe, Destroy button, or Remove buttons |
| Owner UI | Already covered by the two signed-in Alice tests above |

### Add the scenario and test

Append the following scenario to the access integration test file at `test/integration/recipe_access_integration_test.rb`:

```ruby
# test/integration/recipe_access_integration_test.rb
  # Actor: Bob, signed in
  # Starting point: Alice owns recipes(:pancakes) and recipes(:lentil_soup)
  # Action: GET recipes list, then GET pancakes show
  # Expected outcome: no Destroy on the list; no Edit, Destroy, or Remove on show
  # test "non-owner does not see change buttons" do
  # end
```

Now, replace the test body with the following:

```ruby
# test/integration/recipe_access_integration_test.rb
  test "non-owner does not see change buttons" do
    sign_in_as users(:bob)

    get recipes_url
    assert_response :success
    assert_select "button", text: "Destroy this recipe", count: 0

    get recipe_url(recipes(:pancakes))
    assert_response :success
    assert_match recipes(:pancakes).title, response.body
    assert_select "a", text: "Edit this recipe", count: 0
    assert_select "button", text: "Destroy this recipe", count: 0
    assert_select "button", text: "Remove", count: 0
  end
```

This is what's happening in the code above:

- `assert_select ..., count: 0` ensures Bob does not see Destroy button on the list and no Edit, Destroy, or Remove buttons on show. Bob is signed in, so Authentication is not the reason the buttons are gone, the authorization check `allowed_to?` in the views is.
- Fixture recipes all belong to Alice today, so Bob's list has zero Destroy buttons.

Run the access integration test to ensure all of them are passing:

```bash
bin/rails test test/integration/recipe_access_integration_test.rb
```

## Commit your work

Run the full suite to ensure everything is green:

```bash
bin/rails test:all
```

You want 0 failures and 0 errors. Then:

```bash
git add .
git commit -m "Add recipe ownership and Action Policy authorization tests"
```

Small commits make it easier to roll back, bisect a regression, or open a PR with a clear story.

## What is next

You now have authentication (who is signed in) and authorization (who may change which recipe). The Cookbook app still allows everyone to see everyone's recipes (list and show), that will be unchanged for the rest of the guide. But you have locked all other ownership-based changes (create, update, destroy and remove) to only be visible to the owner backed by a comprehensive set of tests.

In next chapter, you will cover mailers (for example share this recipe) with thin mailer objects and focused assertions.

Continue to **[Chapter 14: Testing mailers](/guide/testing-mailers/)**.
