How To Give Engineers Filtered Database Access
Tags: ruby, rails, tutorial, database
Published: 2020-03-18 14:29:19

A common issue that many companies often face, including DEV, is how much data access to give their engineers. Allowing engineers access to production data has many benefits. It can help them make more informed decisions when they are working on projects because they have a clear picture of the data they will be working with. It is also helpful in debugging situations when production code breaks and an engineer needs to figure out why. 

On the flip side, allowing engineers access to production data is also a risk. You don't want an engineer to accidentally change something in the database they shouldn't. You also might have sensitive customer data in your database that you don't want engineers to see. We ran into this exact problem at DEV and found a solution we are really happy with which I wanted to share with everyone. 

# Hello Blazer

First, we tackled the database access issue. How do we give access to the database and all its information to our engineers? Some companies allow engineers access to a production console. This is not an option for us because we use Heroku and only a couple of senior engineers have full Heroku access which you need in order to start a console. 

With console access off the table, we started looking for other ways we could do it. That's when we found the [Blazer gem](https://github.com/ankane/blazer) written by Andrew Kane. When installed, Blazer allows users to query your database via a web UI like below. 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/7ern4ydrf5otuszgl7j1.png)

In addition to allowing users to query your database, Blazer has tons of other nice features such as:
* Saving your queries to reuse later
* Optionally track all queries that are run
* Make [charts](https://github.com/ankane/blazer#charts) from your queries
* Work with other [datasources](https://github.com/ankane/blazer#data-sources)
* [Smart Variables](https://github.com/ankane/blazer#smart-variables) and [Smart Columns](https://github.com/ankane/blazer#smart-columns)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/86frk7c7ycozqvjpk4dj.gif)

This was exactly what we were looking for so we went ahead and [added Blazer to DEV](https://github.com/thepracticaldev/dev.to/pull/6459/files) by following steps which are outlined on the gem's homepage. 

A couple of things to note. First off, we only gave `super_admins` access to Blazer because we still had to sort out the data privacy and read-only access issues. 
```ruby
authenticate :user, ->(user) { user.has_role?(:super_admin) } do
  mount Blazer::Engine, at: "blazer"
end
```
### Development Set Up

Notice that Blazer uses an ENV variable `BLAZER_DATABASE_URL` to know where to point to look at your database. In development, I found that if `BLAZER_DATABASE_URL` is nil then Blazer will use your ActiveRecord connection to talk to your database. This makes getting it set up and working in development super easy. 

### Production Set Up

The `BLAZER_DATABASE_URL` can also allow you to set up Blazer to use a separate user to talk to your database. One of the big things we want to prevent with Blazer is engineers accidentally changing production data. Per the Blazer README:

> Blazer tries to protect against queries that modify data (by running each query in a transaction and rolling it back), but a safer approach is to use a read-only user.

Even though Blazer uses a transaction we wanted to be extra careful and use it via a read-only user. Having a separate URL from our default ActiveRecord connection allows us to use a read-only user's credentials in the url to ensure no data is ever changed. 

```
ENV["BLAZER_DATABASE_URL"] = "postgres://read-only-user:read-only-user-password@hostname:5432/database-name"
```
### Setting Up a Read-only User
Blazer even offers [docs on how to set up a read-only user](https://github.com/ankane/blazer#permissions) for your given database. Since we use Postgres and Heroku I created the read-only user via our Heroku Postgres add-on following the [steps outlined in Heroku's Postgres guide](https://devcenter.heroku.com/articles/heroku-postgresql-credentials#creating-a-new-credential).

 ![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/d9gqj46pfeogd9z74c6t.png)

Once you have your new user in place then you want to take that user's name and password and your database name and create your `BLAZER_DATABASE_URL`. 

With the `BLAZER_DATABASE_URL` set, we now have a UI in place for querying our production database and the database is protected against being changed in any way. The only thing left to do is filter the data.

# Enter Hypershield

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/3akjg5674yirmvd7mqgn.gif)

No, not that kind of shield! The shield we were looking for was one that could hide sensitive user data for us. To accomplish this, once again we turned to another one of Andrew Kane's gems, [Hypershield](https://github.com/ankane/hypershield). How it works:

> Hypershield creates shielded views (in the hypershield schema by default) that hide sensitive tables and columns. The advantage of this approach over column-level privileges is you can use SELECT *.

The first step to getting this to work was to [install the gem](https://github.com/thepracticaldev/dev.to/pull/6628/files#diff-8b7db4d5cc4b8f6dc8feb7030baa2478R8) and create a list of columns we wanted to shield. We installed the gem in our production group since we only want this running in production.

```ruby
group :production do
  gem "hypershield", "~> 0.2.0" # Allow admins to query data via internal
  gem "nakayoshi_fork", "~> 0.0.4" # solves CoW friendly problem on MRI 2.2 and later
  gem "rack-host-redirect", "~> 1.3" # Lean and simple host redirection via Rack middleware
end
```

Next, we created a list of columns we wanted to shield in our `hypershield.rb` initializer file.

```ruby
if Rails.env.production?
  Hypershield.enabled = ENV["ENABLE_HYPERSHIELD"].present?

  # Validate that hypershield schema exists before trying to use it
  begin
    if ActiveRecord::Base.connection.schema_exists?("hypershield")
      # Specify the schema to use and columns to show and hide
      Hypershield.schemas = {
        hypershield: {
          # columns to hide
          # matches table.column
          hide: %w[
            auth_data_dump
            email
            encrypted
            encrypted_password
            message_html
            message_markdown
            password
            previous_refresh_token
            refresh_token
            secret
            token
            current_sign_in_ip
            last_sign_in_ip
            reset_password_token
            remember_token
            unconfirmed_email
          ]
        }
      }

      # Log SQL statements
      Hypershield.log_sql = false
    end
  rescue ActiveRecord::NoDatabaseError
    Rails.logger.error("Hypershield initializer failed to check schema due to NoDatabaseError")
  end
end
```

One thing you will notice is that we have a couple of extra lines doing different checks before we configure Hypershield. First, we have:
```ruby
Hypershield.enabled = ENV["ENABLE_HYPERSHIELD"].present?
```
This line will disable the Hypershield schema [`refresh` task](https://github.com/ankane/hypershield/blob/cef342cd9c9f6963105791bb427a8f55cc14804e/lib/tasks/hypershield.rake#L4) which happens when migrations are run if the ENV variable `ENABLE_HYPERSHIELD` is not present. 

Next, we have:
```ruby
if ActiveRecord::Base.connection.schema_exists?("hypershield")
```
This ensures that if the hypershield schema does not exist we won't try to set it up and fail with an error. These two lines are important because there are other communities using the DEV application source code and we didn't want them to break due to a missing, completely optional, feature we added to the application.

### Creating the Schema
Once the gem and initializer are in place, next we need to create the hypershield schema. Since we are working in Postgres, here are the steps we took to set it up. These set up steps and those for other database types are documented in the [gem's README](https://github.com/ankane/hypershield#database-setup). 

Create a new schema in your database
```sql
CREATE SCHEMA hypershield;
```

Grant privileges to the user you have setup with Blazer. In our case, we want to grant all of these privileges to our read-only Blazer user that we set up via Heroku. 
```sql
GRANT USAGE ON SCHEMA hypershield TO readonly-blazer-username;

-- replace migrations with the user who manages your schema
ALTER DEFAULT PRIVILEGES FOR ROLE heroku-default-user IN SCHEMA hypershield
    GRANT SELECT ON TABLES TO readonly-blazer-username;

-- keep public in search path for functions
ALTER ROLE readonly-blazer-username SET search_path TO hypershield, public;
```

If you are working on Heroku you can run these commands via a Rails console.
```ruby
sql = <<-SQL
  CREATE SCHEMA hypershield;
SQL
ActiveRecord::Base.connection.execute(sql)
```

Or you can connect directly to Postgres via the Heroku CLI using those Blazer user credentials you set up earlier. 
```
heroku pg:psql postgres-app-name --credential readonly-blazer-username --app your-app-name
```
 
I found that the last `ALTER ROLE` statement needed to be run by our read-only Blazer user so you might have to use the Heroku CLI with that user's credentials for that one. Another small Heroku gotcha is that when you set up the read-only Blazer user it is given full access to the public schema. You will want to revoke this with the following command. This ensures that your Blazer user can ONLY view the shielded views set up by your Hypershield gem.
```sql
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM readonly-blazer-username;
```


# Putting It Altogether

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/g2s5lzkilzskgz2owd4c.gif)

After you have your shielded schema in place with your read-only Blazer user it is time to test it out on Blazer to make sure everything works as expected. An easy way to do this is to run a `SELECT *` query on one of the tables you expect to be filtered. In our case, I tested it out with messages. 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/786mx0x3x9yc4a4gyd3e.png)

As expected, none of the message text or body fields are present. Another way to test it out is to request one of the fields you blocked from a table. In this case, I try to explicitly request `message_html` from messages and as you can see I get an error.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/0fdqf9bwxm9w05mtvmiv.png)

When you are satisfied with all of your testing, the last thing you need to do is expose Blazer to those you want to be able to use it. At DEV we choose to [allow all tech admins](https://github.com/thepracticaldev/dev.to/pull/6574)(basically engineers) access to the Blazer console.
```ruby
authenticate :user, ->(user) { user.has_role?(:tech_admin) } do
  mount Blazer::Engine, at: "blazer"
end
```

# DONE!

And with that, you are done! Now sit back and wait for the praises to roll in from your fellow devs who can now access the data they have so desperately needed. 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/l7k3mxth5v9q0ljwy0tn.gif)