Giter VIP home page Giter VIP logo

stitches's Introduction

Create Microservices in Rails by pretty much just writing regular Rails code.

build status

This gem provides:

  • transparent API key authentication.
  • router-level API version based on headers.
  • a way to document your microservice endpoints via acceptance tests.
  • structured errors, buildable from invalid Active Records, Exceptions, or by hand.

This, plus much of what you get from Rails already, means you can create a microservice Rails application by just writing the same Rails code you write today. Instead of rendering web views, you render JSON (which is built into Rails).

To install

Add to your Gemfile:

gem 'stitches'

Then:

bundle install

Then, set it up:

> bin/rails generate stitches:api
> bundle exec rake db:migrate

Upgrading from an older version

  • When upgrading to version 4.0.0 and above you may now take advantage of an in-memory cache

You can enabled it like so

Stitches.configure do |config|
  config.max_cache_ttl = 5  # seconds
  config.max_cache_size = 100  # how many keys to cache
end

You can also set a leniency for disabled API keys, which will allow old API keys to continue to be used if they have a disabled_at field set as long as the leniency is not exceeded. Note that if the disabled_at field is not populated the behavior will remain the same as it always was, and the request will be denied when the enabled field is set to true. If Stitches allows a call due to leniency settings, a log message will be generated with a severity depending on how long ago the API key was disabled.

Stitches.configure do |config|
  config.disabled_key_leniency_in_seconds = 3 * 24 * 60 * 60 # Time in seconds, defaults to three days 
  config.disabled_key_leniency_error_log_threshold_in_seconds = 2 * 24 * 60 * 60 # Time in seconds, defaults to two days 
end

If a disabled key is used within the disabled_key_leniency_in_seconds, it will be allowed.

Anytime a disabled key is used a log will be generated. If it is before the disabled_key_leniency_error_log_threshold_in_seconds it will be a warning log message, if it is after that, it will be an error message. disabled_key_leniency_error_log_threshold_in_seconds should never be a greater number than disabled_key_leniency_in_seconds, as this provides an escallating series of warnings before finally disabling access.

  • If you are upgrading from a version older than 3.3.0 you need to run three generators, two of which create database migrations on your api_clients table:

    > bin/rails generate stitches:add_enabled_to_api_clients
    > bin/rails generate stitches:add_deprecation
    > bin/rails generate stitches:add_disabled_at_to_api_clients
    
  • If you are upgrading from a version between 3.3.0 and 3.5.0 you need to run two generators:

    > bin/rails generate stitches:add_deprecation
    > bin/rails generate stitches:add_disabled_at_to_api_clients
    
  • If you are upgrading from a version between 3.6.0 and 4.0.2 you need to run one generator:

    > bin/rails generate stitches:add_disabled_at_to_api_clients
    

Example Microservice Endpoint

Suppose we wish to allow our consumers to create Widgets

class Api::V1::WidgetsController < ApiController
  def create
    widget = Widget.create(widget_params)
    if widget.valid?
      head 201
    else
      render json: {
        errors: Stitches::Errors.from_active_record_object(widget)
      }, status: 422
    end
  end

private

  def widget_params
    params.require(:widget).permit(:name, :type, :sub_type)
  end
end

If you think there's nothing special about this—you are correct. This is the vanillaest of vanilla Rails controllers, with a few notable exceptions:

  • We aren't checking content type. A stitches-based microservice always uses JSON and refuses to route requests for non-JSON to you, so there's zero need to use respond_to and friends.
  • The error-building is structured and reliable.
  • This is an authenticated request. No request without proper authentication will be routed here, so you don't have to worry about it in your code.
  • This is a versioned request. While the URL will not contain v1 in it, the Accept header will require a version and get routed here. If you make a V2, it's just a new controller and this concern is handled at the routing layer.

All this means that the Rails skills of you and your team can be directly applied to building microservices. You don't have to make a bunch of boring decisions about auth, versioning, or content-types. It also means you can start deploying and creating microservices with little friction. No need to deal with a complex DSL or new programming language to get yourselves going with Microservices.

More Info

See the wiki for how to setup stitches.

  • Stitches Features include:
    • Authorization via API key
    • Versioned requests via HTTP content types
    • Structured Errors
    • ISO 8601-formatted dates
    • Deprecation using the Sunset header
    • An optional ApiKey cache to allow mostly DB free APIs
  • The Generator sets up some code in your app, so you can start writing APIs using vanilla Rails idioms:
    • a "ping" controller that can validate your app is working
    • version routing based on content-type (requests for V2 use the same URL, but are serviced by a different controller)
    • An ApiClient Active Record
    • Acceptance tests that can produce API documentation as they test your app.
  • Stitches provides testing support

API Key Caching

Since version 4.0.0, stitches now has the ability to cache API keys in memory for a configurable amount of time. This may be an improvement for some applications.

You must configure the API Cache for it be used.

Stitches.configure do |config|
  config.max_cache_ttl = 5  # seconds
  config.max_cache_size = 100  # how many keys to cache
end

Your cache size should be larger then the number of consumer keys your service has.

Developing

Although Stitches.configuration is global, do not depend directly on that in your logic. Instead, allow all classes to receive a configuration object in their constructor. This makes the classes easier to deal with and change, without incurring much of a real cost to development. Global symbols suck, but are convenient. This is how you make the most of it.

Also, the integration test does a lot of "testing the implementation", but since Rails generators are notorious for silently failing with a successful result, we have to make sure that the various inject_into_file calls are actually working. Do not do any fancy refactors here, just keep it up to date.

Releases

See the release process for open source gems in the Stitch Fix engineering wiki under technical topics.


Provided with love by your friends at Stitch Fix Engineering

stitches's People

Contributors

alexcameron89 avatar alimac avatar bf4 avatar brettfishman avatar bwebster avatar cameronjacoby avatar carmensea avatar danshultz avatar davetron5000 avatar davidcpell avatar ebarendt avatar faun avatar fixbot avatar greggroth avatar halloffame avatar jamescook avatar jlambert121 avatar joecannatti avatar jonathandean avatar mboeh avatar micahhainlinestitchfix avatar ndp avatar nshemonsky avatar pisaacs avatar samsm avatar simeonwillbanks avatar stevenharman avatar stitchfixsam avatar webdestroya avatar zackse avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

stitches's Issues

Root route always unauthorized

how can I allow the root route? Or what am I setting incorrectly?

My stitches.rb:

require 'stitches'

Stitches.configure do |configuration|
  # Regexp of urls that do not require ApiKeys or valid, versioned mime types
  configuration.allowlist_regexp = %r{
    admin|dashboard|root|password_resets|login|logout|activate|sessions|
    sms_events|sidekiq|resque|docs|assets|temp_athletes|temp_commit_offers|
    commit-offers|co_email_recipients|co_email_groupings|conferences|
    rails\/mailers|forest
  }

  # Name of the custom Authorization scheme.  See http://www.ietf.org/rfc/rfc2617.txt for details,
  # but generally should be a string with no spaces or special characters.
  configuration.custom_http_auth_scheme = 'My_Key'
end

My routes.rb:

Rails.application.routes.draw do
 ...
  root to: 'user_sessions#new'
 ...
end

My application_controller.rb:

class ApplicationController < ActionController::Base
  include Sorcery::Controller

  def authenticate
    current_user || not_authenticated
  end

  def not_authenticated
    respond_to do |type|
      type.html { redirect_to login_path, alert: 'Please login first' }
      type.json { render json: { errors: 'Unable to authenticate' }, status: :unauthorized }
    end
  end

  def unable_to_activate
    render json: { errors: 'User already activated or not found' }, status: :unprocessable_entity
  end
end

My user_sessions_controller.rb:

class UserSessionsController < ApplicationController
  skip_before_action :require_login # , except: %i[destroy]

  def new
    if logged_in?
      if current_user.is_admin?
        redirect_to(admin_path) && return
      else
        redirect_to(athletes_path) && return
      end
    end
    @user = User.new
  end
 ...
end

When I start my server locally and attempt to go to the root all I get rendered is:
Unauthorized - no authorization header
and the only request I see come through is for:
Started GET "/" for 127.0.0.1 at 2018-12-10 10:16:15 -0600

Version is missing in the response headers

Problem:
ApiVersionConstraint requires clients to send Accept request headers that specify the API version, for example: Accept: "application/json; version=2". However, the API response does not match this expected behavior.

Solution:
Add a default header to Api::ApiController that responds with the correct version.

For example: Content-Type: "application/json; version=2"

Header override

Is it possible to use an alternative header for authentication?
I use stitches to protect the API routes from unauthorized requests based on the API key, but I use devise-jwt to authenticate/authorize user access.
Devise-jwt uses a Bearer token in the Authorization header, so being able to override the header used by stitches to almost anything else would let these two gems play nice together.

Uninitialized constant Api::V1 in debug

Problem:
I get ActionController::RoutingError (uninitialized constant Api::V1) in debug mode using Fast Debugger in IntelliJ IDEA (ruby-debug-ide 0.8.0.beta8 with debase 0.3.0.beta8).

Steps to reproduce:

Create new Rails 6 API app and setup stitches.

Create a Controller, e.g. RestaurantsController with Rails scaffold:

My example rails app has following routes then:

namespace :api do
    scope module: :v1, constraints: Stitches::ApiVersionConstraint.new(1) do
      resource :ping, only: [:create]
      # Add your V1 resources here
      resources :restaurants
    end
  end
end
...

Controller looks like:

class Api::V1::RestaurantsController < Api::ApiController
...
  # POST /restaurants
  def create
    @restaurant = Restaurant.new(restaurant_params)

    if @restaurant.save
      render json: { restaurant: @restaurant }, status: :created,
             location: @restaurant
    else
      render json: Stitches::Errors.from_active_record(@restaurant),
             status: :unprocessable_entity
    end
  end
...
end

Now start Server with rails s and send proper POST to localhost:3000/api/restaurants/1.
It works just fine for now.

Now start Server with debug and you will see the error: ActionController::RoutingError (uninitialized constant Api::V1) with the same POST request.

Deprecation warning in Rails 6

Getting a deprecation warning when running this gem in Rails 6.

DEPRECATION WARNING: `Module#parent` has been renamed to `module_parent`. `parent` is deprecated and will be removed in Rails 6.1. (called from <top (required)> at /Users/chris/galley/api_playground/config/environment.rb:5)

Looks like it is the result of rails/rails#34051. Not critical at all as it is just a warning, but wanted to flag!

White-list "complex" route

I have various routes whitelisted, docs for example, but I need to whitelist a route such as users/abc12379xyz_asdf/activate

I have tried this regex but it is still throwing an error:

configuration.whitelist_regexp = %r{\A/(sidekiq|resque|docs|assets)(users\/.*\/activate)(\Z|/.*\Z)}

error:
Unauthorized - no authorization header

Failure message on fresh install and rspec run.

rails new APP_NAME

  • added gem 'stitches'
    rails g rspec:install
    rails g apitome:install
    rails g stitches:api
  • create database
  • run migrations
  • run rspec to validate setup

[error]
uninitialized constant RSpec::Rails (NameError)

Rails engine?

Having a generator option alone makes it hard to take advantage of future updates without squashing any changes you may have made to the generated files in the meantime. A Rails engine pattern could help give this and still allow for a generator for non-Rails apps. It would also mean that we could set up a dummy app for CI to actually run the bundled tests.

Another option or baby step could be to vendor the generated files to discourage changes being made to them directly.

Gem release tasks

Need tasks to bump to a new version of the gem and release it to RubyGems.org

Alternative white lisiting method because `configuration.allowlist_regexp` is error prone

The pain of #61 highlights that this regex is cumbersome to maintain. In referrals, we have an entry that looks like this:

  configuration.allowlist_regexp = %r{\A/(referrals\/api\/contacts|referrals\/api\/referrals|assets|client|referrals\/api\/docs|referrals\/proof_of_life|referrals\/resque|referrals\/teaspoon)}

It turns out that there was an overly permissive entry in there which essentially white listed all of the referrals API - not desirable and tests are being back filled.

I'd like to add something that's a bit easier to read/maintain. I spent some time trying to replace the middleware by creating a Rails custom constraint. Constraints have a handle on the request, but not on the response and so I ran into trouble when trying to replicate these lines:

env[@configuration.env_var_to_hold_api_client_primary_key] = client.id
env[@configuration.env_var_to_hold_api_client] = client

Is there a) still a need for these lines, and b) a way to move them out of the middleware? Or maybe some third way to make it more obvious in routes.rb what does/does not require the api_key? A less desirable option would be to add a before_filter in the controllers but I'm guessing that choice was avoided on purpose?

ApiClients should be de-activatable

When rotating keys, you can't necessarily delete old api keys, for example if you have foreign key constraints to them from your domain objects.

The default api_clients table should have an active field, defaulting to true, or maybe an active_til field? It would probably be handy to have some rake tasks to help with rotation as well.

Issue with model validation and controller valid? check

I was reviewing the details from the Error Handling page, but it does seem to indicate the best way to implement validations in the model.

I have a model that contains a file attachment called :object. I have a validation that checks the file size based on file type:

def ojbect_size_valid
   errors.add(:object, 'should be less than or equal to 5MB for images') if object_file_size > 5.megabytes && image_type?
   errors.add(:object, 'should be less than or equal to 15MB for gifs and videos') if object_file_size > 15.megabytes && (gif_type? || video_type?)
end

This validation is catching files correctly but in the create method of the controller it is throwing an error:

def create
    mp = medium_params
    @medium = Medium.create(mp)

    if @medium.valid?
       ...
    else
      render json: {
        errors: Stitches::Errors.from_active_record_object(@medium)
      }, status: :unprocessable_entity
    end
  end

undefined method `full_messages' for {}:Hash

app/controllers/api/v1/media_controller.rb, line 108

  103         )
  104       else
  105   
  106   
  107         render json: {
> 108           errors: Stitches::Errors.from_active_record_object(@medium)
  109         }, status: :unprocessable_entity
  110       end
  111     end
  112   

What am I doing wrong here to get the errors to render?

Find a way to run the generator in a test

This will be brittle, but might be worth doing, if we had a test that:

  1. rails new
  2. bundle install
  3. rails g stitches:api
  4. rails rspec:install
  5. rails apitome:install
  6. bundle exec rake

And fails the build if any of that fails.

Remove Unused Middleware

Since this is an API, we don't want sessions or cookie support. Let's remove the middleware for these.

# No sessions. This removes the need for CSRF protection.
config.middleware.delete "ActionDispatch::Cookies"
config.middleware.delete "ActionDispatch::Session::CookieStore"
config.middleware.delete "ActionDispatch::Flash"

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.