Commit 76d83dd
2017-10-22 23:06:27
Changed files (22)
lib
bin/console
@@ -0,0 +1,14 @@
+#!/usr/bin/env ruby
+
+require "bundler/setup"
+require "saml/kit"
+
+# You can add fixtures and/or initialization code here to make experimenting
+# with your gem easier. You can also use a different console, if you like.
+
+# (If you use this, don't forget to add pry to your Gemfile!)
+# require "pry"
+# Pry.start
+
+require "irb"
+IRB.start(__FILE__)
bin/setup
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -euo pipefail
+IFS=$'\n\t'
+set -vx
+
+bundle install
+
+# Do any other automated setup that you need to do here
lib/saml/kit/authentication_request.rb
@@ -0,0 +1,58 @@
+module Saml
+ module Kit
+ class AuthenticationRequest
+ def initialize(xml, registry = ServiceProviderRegistry.new)
+ @xml = xml
+ @registry = registry
+ @hash = Hash.from_xml(@xml)
+ end
+
+ def id
+ @hash['AuthnRequest']['ID']
+ end
+
+ def acs_url
+ @hash['AuthnRequest']['AssertionConsumerServiceURL']
+ end
+
+ def issuer
+ @hash['AuthnRequest']['Issuer']
+ end
+
+ def valid?
+ @registry.registered?(issuer)
+ end
+
+ def to_xml
+ @xml
+ end
+
+ def response_for(user)
+ SamlResponse::Builder.new(user, self).build
+ end
+
+ class Builder
+ attr_accessor :id, :issued_at, :issuer, :acs_url
+
+ def to_xml(xml = ::Builder::XmlMarkup.new)
+ xml.tag!('samlp:AuthnRequest',
+ "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
+ "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
+ ID: id,
+ Version: "2.0",
+ IssueInstant: issued_at.strftime("%Y-%m-%dT%H:%M:%SZ"),
+ AssertionConsumerServiceURL: acs_url,
+ ) do
+ xml.tag!('saml:Issuer', issuer)
+ xml.tag!('samlp:NameIDPolicy', Format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress")
+ end
+ xml.target!
+ end
+
+ def build
+ AuthenticationRequest.new(to_xml)
+ end
+ end
+ end
+ end
+end
lib/saml/kit/configuration.rb
@@ -0,0 +1,7 @@
+module Saml
+ module Kit
+ class Configuration
+ attr_accessor :issuer
+ end
+ end
+end
lib/saml/kit/namespaces.rb
@@ -0,0 +1,42 @@
+module Saml
+ module Kit
+ module Namespaces
+ METADATA = "urn:oasis:names:tc:SAML:2.0:metadata"
+ ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
+ SIGNATURE = "http://www.w3.org/2000/09/xmldsig#"
+ PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
+
+ module Statuses
+ SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success"
+ end
+
+ module Consents
+ UNSPECIFIED = "urn:oasis:names:tc:SAML:2.0:consent:unspecified"
+ end
+
+ module AuthnContext
+ module ClassRef
+ PASSWORD = "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
+ PASSWORD_PROTECTED = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
+ end
+ end
+
+ module Methods
+ BEARER = "urn:oasis:names:tc:SAML:2.0:cm:bearer"
+ end
+
+ module Formats
+ module Attr
+ URI = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
+ BASIC = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
+ end
+
+ module NameId
+ EMAIL_ADDRESS = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
+ TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
+ PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
+ end
+ end
+ end
+ end
+end
lib/saml/kit/saml_request.rb
@@ -0,0 +1,9 @@
+module Saml
+ module Kit
+ class SamlRequest
+ def self.decode(raw_request)
+ AuthenticationRequest.new(Base64.decode64(raw_request))
+ end
+ end
+ end
+end
lib/saml/kit/saml_response.rb
@@ -0,0 +1,127 @@
+module Saml
+ module Kit
+ class SamlResponse
+ def initialize(xml)
+ @xml = xml
+ end
+
+ def acs_url
+ Hash.from_xml(@xml)['Response']['Destination']
+ end
+
+ def to_xml
+ @xml
+ end
+
+ def encode
+ Base64.strict_encode64(to_xml)
+ end
+
+ class Builder
+ attr_reader :user, :request, :id, :reference_id, :now
+
+ def initialize(user, request)
+ @user = user
+ @request = request
+ @id = SecureRandom.uuid
+ @reference_id = SecureRandom.uuid
+ @now = Time.now.utc
+ end
+
+ def to_xml
+ xml = ::Builder::XmlMarkup.new
+ xml.tag!("samlp:Response", response_options) do
+ xml.Issuer(configuration.issuer, xmlns: Namespaces::ASSERTION)
+ xml.tag!("samlp:Status") do
+ xml.tag!('samlp:StatusCode', Value: Namespaces::Statuses::SUCCESS)
+ end
+ xml.Assertion(assertion_options) do
+ xml.Issuer configuration.issuer
+ xml.Subject do
+ xml.NameID user.uuid, Format: name_id_format
+ xml.SubjectConfirmation Method: Namespaces::Methods::BEARER do
+ xml.SubjectConfirmationData "", subject_confirmation_data_options
+ end
+ end
+ xml.Conditions conditions_options do
+ xml.AudienceRestriction do
+ xml.Audience request.issuer
+ end
+ end
+ xml.AuthnStatement authn_statement_options do
+ xml.AuthnContext do
+ xml.AuthnContextClassRef Namespaces::AuthnContext::ClassRef::PASSWORD
+ end
+ end
+ xml.AttributeStatement do
+ user.assertion_attributes.each do |key, value|
+ xml.Attribute Name: key, NameFormat: Namespaces::Formats::Attr::URI, FriendlyName: key do
+ xml.AttributeValue value.to_s
+ end
+ end
+ end
+ end
+ end
+ xml.target!
+ end
+
+ def build
+ SamlResponse.new(to_xml)
+ end
+
+ private
+
+ def configuration
+ Saml::Kit.configuration
+ end
+
+ def response_options
+ {
+ ID: "_#{id}",
+ Version: "2.0",
+ IssueInstant: now.iso8601,
+ Destination: request.acs_url,
+ Consent: Namespaces::Consents::UNSPECIFIED,
+ InResponseTo: request.id,
+ "xmlns:samlp" => Namespaces::PROTOCOL,
+ }
+ end
+
+ def assertion_options
+ {
+ ID: "_#{reference_id}",
+ IssueInstant: now.iso8601,
+ Version: "2.0",
+ }
+ end
+
+ def subject_confirmation_data_options
+ {
+ InResponseTo: request.id,
+ NotOnOrAfter: 3.hours.from_now.utc.iso8601,
+ Recipient: request.acs_url,
+ }
+ end
+
+ def conditions_options
+ {
+ NotBefore: 5.seconds.ago.utc.iso8601,
+ NotOnOrAfter: 3.hours.from_now.utc.iso8601,
+ }
+ end
+
+ def authn_statement_options
+ {
+ AuthnInstant: now.iso8601,
+ SessionIndex: assertion_options[:ID],
+ SessionNotOnOrAfter: 3.hours.from_now.utc.iso8601,
+ }
+ end
+
+ def name_id_format
+ "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
+ end
+ end
+ end
+ end
+end
lib/saml/kit/service_provider_registry.rb
@@ -0,0 +1,9 @@
+module Saml
+ module Kit
+ class ServiceProviderRegistry
+ def registered?(issuer)
+ true
+ end
+ end
+ end
+end
lib/saml/kit/version.rb
@@ -0,0 +1,5 @@
+module Saml
+ module Kit
+ VERSION = "0.1.0"
+ end
+end
lib/saml/kit.rb
@@ -0,0 +1,25 @@
+require "saml/kit/version"
+
+require "builder"
+require "securerandom"
+require "active_support/duration"
+require "active_support/core_ext/numeric/time"
+require "active_support/core_ext/hash/conversions"
+require "saml/kit/authentication_request"
+require "saml/kit/configuration"
+require "saml/kit/namespaces"
+require "saml/kit/saml_request"
+require "saml/kit/saml_response"
+require "saml/kit/service_provider_registry"
+
+module Saml
+ module Kit
+ def self.configuration
+ @config ||= Saml::Kit::Configuration.new
+ end
+
+ def self.configure
+ yield configuration
+ end
+ end
+end
spec/saml/authentication_request_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::AuthenticationRequest do
+ subject { described_class.new(raw_xml, registry) }
+ let(:registry) { double }
+ let(:id) { SecureRandom.uuid }
+ let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
+ let(:issuer) { FFaker::Movie.title }
+ let(:raw_xml) do
+ builder = described_class::Builder.new
+ builder.id = id
+ builder.issued_at = Time.now.utc
+ builder.issuer = issuer
+ builder.acs_url = acs_url
+ builder.to_xml
+ end
+
+ it { expect(subject.issuer).to eql(issuer) }
+ it { expect(subject.id).to eql(id) }
+ it { expect(subject.acs_url).to eql(acs_url) }
+
+ describe "#valid?" do
+ it 'returns false when the service provider is not known' do
+ allow(registry).to receive(:registered?).with(issuer).and_return(false)
+ expect(subject).to_not be_valid
+ end
+
+ it 'returns true when the service provider is registered' do
+ allow(registry).to receive(:registered?).with(issuer).and_return(true)
+ expect(subject).to be_valid
+ end
+ end
+end
spec/saml/kit_spec.rb
@@ -0,0 +1,7 @@
+require "spec_helper"
+
+RSpec.describe Saml::Kit do
+ it "has a version number" do
+ expect(Saml::Kit::VERSION).not_to be nil
+ end
+end
spec/saml/saml_response_spec.rb
@@ -0,0 +1,115 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::SamlResponse do
+ describe "#acs_url" do
+ let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
+ let(:user) { double(:user, uuid: SecureRandom.uuid, assertion_attributes: { }) }
+ let(:request) { double(id: SecureRandom.uuid, acs_url: acs_url, issuer: FFaker::Movie.title) }
+ subject { described_class::Builder.new(user, request).build }
+
+ it 'returns the acs_url' do
+ expect(subject.acs_url).to eql(acs_url)
+ end
+ end
+
+ describe "#to_xml" do
+ subject { described_class::Builder.new(user, request) }
+ let(:user) { double(:user, uuid: SecureRandom.uuid, assertion_attributes: { email: email, created_at: Time.now.utc.iso8601 }) }
+ let(:request) { double(id: SecureRandom.uuid, acs_url: acs_url, issuer: FFaker::Movie.title) }
+ let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
+ let(:issuer) { FFaker::Movie.title }
+ let(:email) { FFaker::Internet.email }
+
+ <<-XML
+<samlp:Response
+ xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
+ xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+ ID="_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6"
+ Version="2.0"
+ IssueInstant="2014-07-17T01:01:48Z"
+ Destination="http://sp.example.com/demo1/index.php?acs"
+ InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685">
+ <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
+ <samlp:Status>
+ <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
+ </samlp:Status>
+ <saml:Assertion
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:xs="http://www.w3.org/2001/XMLSchema"
+ ID="_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75"
+ Version="2.0"
+ IssueInstant="2014-07-17T01:01:48Z">
+ <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
+ <saml:Subject>
+ <saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
+ <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
+ <saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
+ </saml:SubjectConfirmation>
+ </saml:Subject>
+ <saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
+ <saml:AudienceRestriction>
+ <saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
+ </saml:AudienceRestriction>
+ </saml:Conditions>
+ <saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
+ <saml:AuthnContext>
+ <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
+ </saml:AuthnContext>
+ </saml:AuthnStatement>
+ <saml:AttributeStatement>
+ <saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+ <saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+ <saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
+ </saml:Attribute>
+ </saml:AttributeStatement>
+ </saml:Assertion>
+</samlp:Response>
+ XML
+ it 'returns a proper response for the user' do
+ travel_to 1.second.from_now
+ allow(Saml::Kit.configuration).to receive(:issuer).and_return(issuer)
+ result = subject.to_xml
+ hash = Hash.from_xml(result)
+
+ expect(hash['Response']['ID']).to be_present
+ expect(hash['Response']['Version']).to eql('2.0')
+ expect(hash['Response']['IssueInstant']).to eql(Time.now.utc.iso8601)
+ expect(hash['Response']['Destination']).to eql(acs_url)
+ expect(hash['Response']['InResponseTo']).to eql(request.id)
+ expect(hash['Response']['Issuer']).to eql(issuer)
+ expect(hash['Response']['Status']['StatusCode']['Value']).to eql("urn:oasis:names:tc:SAML:2.0:status:Success")
+
+ expect(hash['Response']['Assertion']['ID']).to be_present
+ expect(hash['Response']['Assertion']['IssueInstant']).to eql(Time.now.utc.iso8601)
+ expect(hash['Response']['Assertion']['Version']).to eql("2.0")
+ expect(hash['Response']['Assertion']['Issuer']).to eql(issuer)
+
+ expect(hash['Response']['Assertion']['Subject']['NameID']).to eql(user.uuid)
+ expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['Method']).to eql("urn:oasis:names:tc:SAML:2.0:cm:bearer")
+ expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
+ expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['Recipient']).to eql(acs_url)
+ expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['InResponseTo']).to eql(request.id)
+
+ expect(hash['Response']['Assertion']['Conditions']['NotBefore']).to eql(5.seconds.ago.utc.iso8601)
+ expect(hash['Response']['Assertion']['Conditions']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
+ expect(hash['Response']['Assertion']['Conditions']['AudienceRestriction']['Audience']).to eql(request.issuer)
+
+ expect(hash['Response']['Assertion']['AuthnStatement']['AuthnInstant']).to eql(Time.now.utc.iso8601)
+ expect(hash['Response']['Assertion']['AuthnStatement']['SessionNotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
+ expect(hash['Response']['Assertion']['AuthnStatement']['SessionIndex']).to eql(hash['Response']['Assertion']['ID'])
+ expect(hash['Response']['Assertion']['AuthnStatement']['AuthnContext']['AuthnContextClassRef']).to eql('urn:oasis:names:tc:SAML:2.0:ac:classes:Password')
+
+ expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['Name']).to eql('email')
+ expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['FriendlyName']).to eql('email')
+ expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['NameFormat']).to eql('urn:oasis:names:tc:SAML:2.0:attrname-format:uri')
+ expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['AttributeValue']).to eql(email)
+
+ expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['Name']).to eql('created_at')
+ expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['FriendlyName']).to eql('created_at')
+ expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['NameFormat']).to eql('urn:oasis:names:tc:SAML:2.0:attrname-format:uri')
+ expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['AttributeValue']).to be_present
+ end
+ end
+end
spec/spec_helper.rb
@@ -0,0 +1,17 @@
+require "bundler/setup"
+require "saml/kit"
+require "ffaker"
+require "active_support/testing/time_helpers"
+
+RSpec.configure do |config|
+ config.include ActiveSupport::Testing::TimeHelpers
+ # Enable flags like --only-failures and --next-failure
+ config.example_status_persistence_file_path = ".rspec_status"
+
+ # Disable RSpec exposing methods globally on `Module` and `main`
+ config.disable_monkey_patching!
+
+ config.expect_with :rspec do |c|
+ c.syntax = :expect
+ end
+end
.gitignore
@@ -0,0 +1,12 @@
+/.bundle/
+/.yardoc
+/Gemfile.lock
+/_yardoc/
+/coverage/
+/doc/
+/pkg/
+/spec/reports/
+/tmp/
+
+# rspec failure tracking
+.rspec_status
.rspec
@@ -0,0 +1,2 @@
+--format documentation
+--color
.travis.yml
@@ -0,0 +1,5 @@
+sudo: false
+language: ruby
+rvm:
+ - 2.4.2
+before_install: gem install bundler -v 1.15.4
Gemfile
@@ -0,0 +1,6 @@
+source "https://rubygems.org"
+
+git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
+
+# Specify your gem's dependencies in saml-kit.gemspec
+gemspec
LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 mo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
Rakefile
@@ -0,0 +1,6 @@
+require "bundler/gem_tasks"
+require "rspec/core/rake_task"
+
+RSpec::Core::RakeTask.new(:spec)
+
+task :default => :spec
README.md
@@ -0,0 +1,39 @@
+# Saml::Kit
+
+Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/saml/kit`. To experiment with that code, run `bin/console` for an interactive prompt.
+
+TODO: Delete this and the text above, and describe your gem
+
+## Installation
+
+Add this line to your application's Gemfile:
+
+```ruby
+gem 'saml-kit'
+```
+
+And then execute:
+
+ $ bundle
+
+Or install it yourself as:
+
+ $ gem install saml-kit
+
+## Usage
+
+TODO: Write usage instructions here
+
+## Development
+
+After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
+
+To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
+
+## Contributing
+
+Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/saml-kit.
+
+## License
+
+The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
saml-kit.gemspec
@@ -0,0 +1,39 @@
+# coding: utf-8
+lib = File.expand_path("../lib", __FILE__)
+$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
+require "saml/kit/version"
+
+Gem::Specification.new do |spec|
+ spec.name = "saml-kit"
+ spec.version = Saml::Kit::VERSION
+ spec.authors = ["mo"]
+ spec.email = ["mo@mokhan.ca"]
+
+ spec.summary = %q{A simple toolkit for working with SAML.}
+ spec.description = %q{A simple toolkit for working with SAML.}
+ spec.homepage = "http://www.mokhan.ca"
+ spec.license = "MIT"
+
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
+ if spec.respond_to?(:metadata)
+ spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
+ else
+ raise "RubyGems 2.0 or newer is required to protect against " \
+ "public gem pushes."
+ end
+
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
+ f.match(%r{^(test|spec|features)/})
+ end
+ spec.bindir = "exe"
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
+ spec.require_paths = ["lib"]
+
+ spec.add_dependency "builder", "~> 3.2"
+ spec.add_dependency "activesupport", "~> 5.1"
+ spec.add_development_dependency "bundler", "~> 1.15"
+ spec.add_development_dependency "rake", "~> 10.0"
+ spec.add_development_dependency "rspec", "~> 3.0"
+ spec.add_development_dependency "ffaker", "~> 2.7"
+end