├── lib
├── tasks
│ ├── .gitkeep
│ └── test.rake
├── rack
│ └── api_version.rb
└── gatekeeper.rb
├── public
├── favicon.ico
├── robots.txt
├── images
│ └── envylabs.jpg
├── 404.html
├── 422.html
└── 500.html
├── vendor
└── plugins
│ └── .gitkeep
├── .ruby-version
├── app
├── helpers
│ ├── users_helper.rb
│ ├── projects_helper.rb
│ ├── application_helper.rb
│ └── gate_keeper_helper.rb
├── models
│ ├── keymaster
│ │ └── version.rb
│ ├── ssh_key.rb
│ ├── membership.rb
│ ├── project.rb
│ ├── keymaster.rb
│ └── user.rb
└── controllers
│ ├── application_controller.rb
│ ├── projects_controller.rb
│ ├── gate_keeper_controller.rb
│ └── users_controller.rb
├── Procfile
├── config
├── initializers
│ ├── api_version.rb
│ ├── load_keymaster.rb
│ ├── yaml_renderer.rb
│ ├── mime_types.rb
│ ├── secret_token.rb
│ ├── backtrace_silencers.rb
│ ├── session_store.rb
│ ├── wrap_parameters.rb
│ └── inflections.rb
├── database.ci.yml
├── environment.rb
├── boot.rb
├── locales
│ └── en.yml
├── routes.rb
├── unicorn.rb
├── application.rb
└── environments
│ ├── test.rb
│ ├── development.rb
│ └── production.rb
├── test
├── unit
│ ├── helpers
│ │ ├── users_helper_test.rb
│ │ ├── projects_helper_test.rb
│ │ └── gate_keeper_helper_test.rb
│ ├── membership_test.rb
│ ├── project_test.rb
│ ├── ssh_key_test.rb
│ └── user_test.rb
├── factories
│ ├── membership_factory.rb
│ ├── project_factory.rb
│ ├── ssh_key_factory.rb
│ └── user_factory.rb
├── public.key
├── performance
│ └── browsing_test.rb
├── test_helper.rb
├── functional
│ ├── gate_keeper_controller_test.rb
│ ├── projects_controller_test.rb
│ └── users_controller_test.rb
└── private.key
├── config.ru
├── doc
└── README_FOR_APP
├── db
├── migrate
│ ├── 20110404015102_add_login_index_to_users.rb
│ ├── 20100222040924_create_projects.rb
│ ├── 20100222042250_create_memberships.rb
│ ├── 20110404014622_add_indices_to_memberships.rb
│ ├── 20100222035824_create_users.rb
│ ├── 20110404013217_add_cached_slug_to_projects.rb
│ ├── 20100222044149_create_slugs.rb
│ └── 20100626141851_create_ssh_keys.rb
├── seeds.rb
└── schema.rb
├── Rakefile
├── script
└── rails
├── .travis.yml
├── CHANGELOG.markdown
├── Gemfile
├── .gitignore
├── .env.development
├── README.markdown
└── Gemfile.lock
/lib/tasks/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/plugins/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 1.9.3-p448
2 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-Agent: *
2 | Disallow: /
3 |
--------------------------------------------------------------------------------
/app/helpers/users_helper.rb:
--------------------------------------------------------------------------------
1 | module UsersHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/projects_helper.rb:
--------------------------------------------------------------------------------
1 | module ProjectsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/gate_keeper_helper.rb:
--------------------------------------------------------------------------------
1 | module GateKeeperHelper
2 | end
3 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: bundle exec unicorn -p $PORT -E $RACK_ENV -c ./config/unicorn.rb
2 |
--------------------------------------------------------------------------------
/app/models/keymaster/version.rb:
--------------------------------------------------------------------------------
1 | module Keymaster
2 | VERSION = '2.0.2'
3 | end
4 |
--------------------------------------------------------------------------------
/config/initializers/api_version.rb:
--------------------------------------------------------------------------------
1 | require 'keymaster'
2 | require 'rack/api_version'
3 |
--------------------------------------------------------------------------------
/public/images/envylabs.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/envylabs/keymaster/HEAD/public/images/envylabs.jpg
--------------------------------------------------------------------------------
/test/unit/helpers/users_helper_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class UsersHelperTest < ActionView::TestCase
4 | end
5 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | protect_from_forgery
3 | end
4 |
--------------------------------------------------------------------------------
/test/unit/helpers/projects_helper_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class ProjectsHelperTest < ActionView::TestCase
4 | end
5 |
--------------------------------------------------------------------------------
/test/factories/membership_factory.rb:
--------------------------------------------------------------------------------
1 | FactoryGirl.define do
2 | factory :membership do
3 | project
4 | user
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/test/unit/helpers/gate_keeper_helper_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class GateKeeperHelperTest < ActionView::TestCase
4 | end
5 |
--------------------------------------------------------------------------------
/config/database.ci.yml:
--------------------------------------------------------------------------------
1 | test:
2 | adapter: postgresql
3 | database: keymaster_test
4 | username: postgres
5 | min_messages: WARNING
6 |
--------------------------------------------------------------------------------
/test/factories/project_factory.rb:
--------------------------------------------------------------------------------
1 | FactoryGirl.define do
2 | factory :project do
3 | sequence(:name) { |n| "Factory Project #{n}" }
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/test/factories/ssh_key_factory.rb:
--------------------------------------------------------------------------------
1 | FactoryGirl.define do
2 | factory :ssh_key do
3 | sequence(:public_key) { |n| "ssh-dss #{n}AAAAB3NzaC1kc3MAAACBAIcq==" }
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run Keymaster::Application
5 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the rails application
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the rails application
5 | Keymaster::Application.initialize!
6 |
--------------------------------------------------------------------------------
/test/public.key:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PUBLIC KEY-----
2 | MEgCQQC+mf8zhOYg1T18PBbdmU+acVagE9joD2nIW0/Rxxksx/t5mGULXmKV2myz
3 | ZqD0VGVW5Dw1swaRwPvOc57y7JbbAgMBAAE=
4 | -----END RSA PUBLIC KEY-----
5 |
6 |
--------------------------------------------------------------------------------
/config/initializers/load_keymaster.rb:
--------------------------------------------------------------------------------
1 | # Not autoloaded by rails unless cache_classes is true because Keymaster is defined by application.rb
2 | require 'keymaster' unless Keymaster::Application.config.cache_classes
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 |
3 | # Set up gems listed in the Gemfile.
4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
5 |
6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
7 |
--------------------------------------------------------------------------------
/config/initializers/yaml_renderer.rb:
--------------------------------------------------------------------------------
1 | require 'yaml'
2 |
3 | ActionController::Renderers.add(:yaml) do |yaml, options|
4 | self.content_type ||= Mime::YAML
5 | yaml.respond_to?(:to_yaml) ? yaml.to_yaml(options) : yaml
6 | end
7 |
--------------------------------------------------------------------------------
/doc/README_FOR_APP:
--------------------------------------------------------------------------------
1 | Use this README file to introduce your application and point to useful places in the API for learning more.
2 | Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries.
3 |
--------------------------------------------------------------------------------
/app/controllers/projects_controller.rb:
--------------------------------------------------------------------------------
1 | class ProjectsController < ApplicationController
2 | respond_to :yaml
3 |
4 | def show
5 | @project = Project.find(params[:id])
6 | respond_with(@project.attributes)
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Sample localization file for English. Add more files in this directory for other locales.
2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3 |
4 | en:
5 | hello: "Hello world"
6 |
--------------------------------------------------------------------------------
/app/models/ssh_key.rb:
--------------------------------------------------------------------------------
1 | class SshKey < ActiveRecord::Base
2 | belongs_to :user
3 | validates_format_of :public_key, :with => %r{^ssh-(rsa|dss)\b}
4 | validates_uniqueness_of :public_key
5 | validates_presence_of :public_key
6 | end
7 |
--------------------------------------------------------------------------------
/db/migrate/20110404015102_add_login_index_to_users.rb:
--------------------------------------------------------------------------------
1 | class AddLoginIndexToUsers < ActiveRecord::Migration
2 | def self.up
3 | add_index :users, :login, :unique => true
4 | end
5 |
6 | def self.down
7 | remove_index :users, :login
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/app/controllers/gate_keeper_controller.rb:
--------------------------------------------------------------------------------
1 | class GateKeeperController < ApplicationController
2 |
3 | def index
4 | respond_to do |format|
5 | format.rb { render :text => Keymaster.gatekeeper_data.gsub('%CURRENT_KEYMASTER_HOST%', request.host) }
6 | end
7 | end
8 |
9 | end
10 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 | # Mime::Type.register_alias "text/html", :iphone
6 | Mime::Type.register 'application/x-ruby', :rb
7 |
--------------------------------------------------------------------------------
/test/performance/browsing_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'rails/performance_test_help'
3 |
4 | # Profiling results for each test method are written to tmp/performance.
5 | class BrowsingTest < ActionDispatch::PerformanceTest
6 | def test_homepage
7 | get '/'
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | # Add your own tasks in files placed in lib/tasks ending in .rake,
3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
4 |
5 | require File.expand_path('../config/application', __FILE__)
6 |
7 | Keymaster::Application.load_tasks
8 |
--------------------------------------------------------------------------------
/db/migrate/20100222040924_create_projects.rb:
--------------------------------------------------------------------------------
1 | class CreateProjects < ActiveRecord::Migration
2 | def self.up
3 | create_table :projects do |t|
4 | t.string :name
5 |
6 | t.timestamps
7 | end
8 | end
9 |
10 | def self.down
11 | drop_table :projects
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Keymaster::Application.routes.draw do
2 | resources :projects, :only => [:show] do
3 | resources :users, :only => [:index, :show]
4 | end
5 |
6 | resources :users, :only => :show
7 |
8 | get '/gatekeeper(.:format)', :to => 'gate_keeper#index', :as => :gatekeeper
9 | end
10 |
--------------------------------------------------------------------------------
/script/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3 |
4 | APP_PATH = File.expand_path('../../config/application', __FILE__)
5 | require File.expand_path('../../config/boot', __FILE__)
6 | require 'rails/commands'
7 |
--------------------------------------------------------------------------------
/test/factories/user_factory.rb:
--------------------------------------------------------------------------------
1 | FactoryGirl.define do
2 | factory :user do
3 | sequence(:login) { |n| "login#{n}"}
4 | sequence(:uid) { |n| 5000 + n }
5 | full_name 'Factory User'
6 |
7 | after(:build) do |user|
8 | FactoryGirl.build(:ssh_key, :user => user)
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/20100222042250_create_memberships.rb:
--------------------------------------------------------------------------------
1 | class CreateMemberships < ActiveRecord::Migration
2 | def self.up
3 | create_table :memberships do |t|
4 | t.integer :project_id
5 | t.integer :user_id
6 |
7 | t.timestamps
8 | end
9 | end
10 |
11 | def self.down
12 | drop_table :memberships
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/rack/api_version.rb:
--------------------------------------------------------------------------------
1 | module Rack
2 | class ApiVersion
3 | def initialize(app, version = Keymaster.version)
4 | @app = app
5 | @version = version
6 | end
7 |
8 | def call(env)
9 | @app.call(env).tap do |response|
10 | response[1]['X-API-Version'] = @version
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/tasks/test.rake:
--------------------------------------------------------------------------------
1 | namespace :test do
2 | namespace :ci do
3 | desc "Configure the CI test server"
4 | task :configure do
5 | require 'pathname'
6 | root = Pathname.new File.expand_path('../../../', __FILE__)
7 | cp root.join('config/database.ci.yml'), root.join('config/database.yml')
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 1.9.3
4 | services:
5 | - postgresql
6 | before_script:
7 | - psql -c 'create database keymaster_test;' -U postgres
8 | - cp config/database.ci.yml config/database.yml
9 | - RAILS_ENV=test bundle exec rake db:setup
10 | bundler_args: --without=development production
11 | notifications:
12 | email: false
13 |
--------------------------------------------------------------------------------
/db/migrate/20110404014622_add_indices_to_memberships.rb:
--------------------------------------------------------------------------------
1 | class AddIndicesToMemberships < ActiveRecord::Migration
2 | def self.up
3 | add_index :memberships, :project_id
4 | add_index :memberships, :user_id
5 | end
6 |
7 | def self.down
8 | remove_index :memberships, :user_id
9 | remove_index :memberships, :project_id
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/20100222035824_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration
2 | def self.up
3 | create_table :users do |t|
4 | t.string :login
5 | t.string :full_name
6 | t.text :public_ssh_key
7 | t.integer :uid
8 |
9 | t.timestamps
10 | end
11 | end
12 |
13 | def self.down
14 | drop_table :users
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }])
7 | # Mayor.create(:name => 'Daley', :city => cities.first)
8 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV["RAILS_ENV"] = "test"
2 | ENV["PUBLIC_SIGNING_KEY"] = File.read(File.expand_path('../public.key', __FILE__))
3 | ENV["PRIVATE_SIGNING_KEY"] = File.read(File.expand_path('../private.key', __FILE__))
4 |
5 | require File.expand_path('../../config/environment', __FILE__)
6 | require 'rails/test_help'
7 |
8 | class ActiveSupport::TestCase
9 | fixtures :all
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20110404013217_add_cached_slug_to_projects.rb:
--------------------------------------------------------------------------------
1 | class AddCachedSlugToProjects < ActiveRecord::Migration
2 | def self.up
3 | add_column :projects, :cached_slug, :string, :limit => 255
4 | add_index :projects, :cached_slug, :unique => true
5 | end
6 |
7 | def self.down
8 | remove_index :projects, :cached_slug
9 | remove_column :projects, :cached_slug
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/models/membership.rb:
--------------------------------------------------------------------------------
1 | class Membership < ActiveRecord::Base
2 |
3 | belongs_to :user
4 | belongs_to :project
5 |
6 | validates_presence_of :user_id,
7 | :project_id
8 |
9 | validates_uniqueness_of :user_id,
10 | :scope => :project_id,
11 | :case_sensitive => false
12 |
13 | end
14 |
--------------------------------------------------------------------------------
/config/initializers/secret_token.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 | # Make sure the secret is at least 30 characters and all random,
6 | # no regular words or you'll be exposed to dictionary attacks.
7 | Keymaster::Application.config.secret_token = ENV["SECRET_TOKEN"]
8 |
--------------------------------------------------------------------------------
/CHANGELOG.markdown:
--------------------------------------------------------------------------------
1 | ### 2.0.1 / 2011-06-17
2 |
3 | * Bug Fixes
4 | * Removed Rack::ResponseSignatureRepeater
5 | * This removes the Response-Signature header from the application, leaving X-Response-Signature intact.
6 |
7 | ### 2.0.0 / 2011-06-17
8 |
9 | * Enhancements
10 | * Upgrade to Rails 3.0.9
11 | * Remove VERSION file in favor of Keymaster::VERSION
12 | * Use postgresql in all environments (dropping sqlite3 support)
13 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Keymaster::Application.config.session_store :cookie_store, :key => '_keymaster_session'
4 |
5 | # Use the database for sessions instead of the cookie-based default,
6 | # which shouldn't be used to store highly confidential information
7 | # (create the session table with "rails generate session_migration")
8 | # Keymaster::Application.config.session_store :active_record_store
9 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | ruby "1.9.3"
4 |
5 | gem 'rails', '3.2.14'
6 | gem 'dotenv-rails', '~> 0.8.0'
7 | gem 'pg', '~> 0.16.0'
8 | gem 'friendly_id', '~> 4.0'
9 | gem 'rack-response-signature', '~> 0.3.1', :require => 'rack/response_signature'
10 |
11 | group :production do
12 | gem 'unicorn', '~> 4.6'
13 | end
14 |
15 | group :test do
16 | gem 'shoulda', '~> 3.5'
17 | gem 'mocha', :require => 'mocha/setup'
18 | gem 'factory_girl_rails', '~> 4.2'
19 | end
20 |
--------------------------------------------------------------------------------
/app/models/project.rb:
--------------------------------------------------------------------------------
1 | class Project < ActiveRecord::Base
2 | extend FriendlyId
3 |
4 | has_many :memberships,
5 | :dependent => :destroy
6 | has_many :users,
7 | :through => :memberships
8 |
9 | validates_presence_of :name
10 | validates_uniqueness_of :name
11 |
12 | friendly_id :name,
13 | :use => :slugged,
14 | :slug_column => :cached_slug
15 |
16 | end
17 |
--------------------------------------------------------------------------------
/test/functional/gate_keeper_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class GateKeeperControllerTest < ActionController::TestCase
4 |
5 | context 'The GateKeeperController' do
6 |
7 | context 'using GET to index' do
8 |
9 | setup do
10 | get :index, :format => 'rb'
11 | end
12 |
13 | should respond_with(:success)
14 |
15 | should "respond with Ruby" do
16 | assert_equal(response.content_type, Mime::RB)
17 | end
18 |
19 | end
20 |
21 | end
22 |
23 | end
24 |
--------------------------------------------------------------------------------
/test/private.key:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIBOwIBAAJBAL6Z/zOE5iDVPXw8Ft2ZT5pxVqAT2OgPachbT9HHGSzH+3mYZQte
3 | YpXabLNmoPRUZVbkPDWzBpHA+85znvLsltsCAwEAAQJAYR9xplv7NBHU8eBgumyr
4 | 3oQQYyOZ7K4l9h1pb/jnQCSKRLeKTXDMuhjW72EeO7jGusMtlOeXzuxaNveC7WHm
5 | qQIhAN6S22Esd76Fxd2NAGzP65y4Z4FahiOeVlgb+eG8DgylAiEA2znt5GkLvf1J
6 | e9aPMCg/87o+8NaVCVpGeJb+5StdPX8CIQC1M6RtAVHfh3MmQwQEkmXEapDBy9wH
7 | JYIwK16Ne5eIjQIgVWZZr9LkChzzVVSd7wqe7xksj7Fn2X7bWPqpTSj5Z40CIQCK
8 | 9HsbsQJG7I6+MoGlKPbw5/tKpBjt7AFgkoRPfx1TDg==
9 | -----END RSA PRIVATE KEY-----
10 |
11 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 | #
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 | # Disable root element in JSON by default.
12 | ActiveSupport.on_load(:active_record) do
13 | self.include_root_in_json = false
14 | end
15 |
--------------------------------------------------------------------------------
/app/models/keymaster.rb:
--------------------------------------------------------------------------------
1 | module Keymaster
2 |
3 | ##
4 | # Returns the current version number of the application.
5 | #
6 | def self.version
7 | Keymaster::VERSION
8 | end
9 |
10 | ##
11 | # Returns the data (content) for the gatekeeper.rb file to be executed by
12 | # the servers.
13 | #
14 | def self.gatekeeper_data
15 | @@gatekeeper_data ||= Rails.root.join('lib/gatekeeper.rb').read.
16 | gsub('%CURRENT_KEYMASTER_VERSION%', self.version).
17 | gsub('%CURRENT_PUBLIC_KEY%', ENV['PUBLIC_SIGNING_KEY'])
18 | end
19 |
20 | end
21 |
--------------------------------------------------------------------------------
/test/unit/membership_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class MembershipTest < ActiveSupport::TestCase
4 |
5 | context 'A Membership' do
6 |
7 | setup do
8 | @membership = FactoryGirl.create(:membership)
9 | end
10 |
11 | subject { @membership }
12 |
13 | should belong_to(:user)
14 | should belong_to(:project)
15 |
16 | should validate_presence_of(:user_id)
17 | should validate_presence_of(:project_id)
18 |
19 | should validate_uniqueness_of(:user_id).scoped_to(:project_id).case_insensitive
20 |
21 | end
22 |
23 | end
24 |
--------------------------------------------------------------------------------
/app/controllers/users_controller.rb:
--------------------------------------------------------------------------------
1 | class UsersController < ApplicationController
2 | respond_to :yaml
3 |
4 | def index
5 | @project = Project.find(params[:project_id])
6 | @users = @project.users.includes(:ssh_keys)
7 |
8 | respond_with(@users.collect { |u| u.keymaster_data })
9 | end
10 |
11 | def show
12 | @parent = params.has_key?(:project_id) ? Project.find(params[:project_id]).users : User
13 | @user = @parent.where(:login => params[:id]).includes(:ssh_keys).first!
14 |
15 | respond_with(@user.keymaster_data)
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile ~/.gitignore_global
6 |
7 | # Ignore bundler config
8 | /.bundle
9 |
10 | # Ignore the default SQLite database.
11 | /db/*.sqlite3
12 |
13 | # Ignore all logfiles and tempfiles.
14 | /log/*.log
15 | /tmp
16 |
17 | /.env
18 | /.env.*
19 | !/.env.development
20 | /config/database.yml
21 | /.ruby-gemset
22 |
--------------------------------------------------------------------------------
/test/functional/projects_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class ProjectsControllerTest < ActionController::TestCase
4 |
5 | context 'The ProjectsController' do
6 |
7 | context 'using GET to show with YAML' do
8 |
9 | setup do
10 | @project = FactoryGirl.create(:project)
11 | get :show, :id => @project.to_param, :format => 'yaml'
12 | end
13 |
14 | should respond_with(:success)
15 |
16 | should "respond with YAML" do
17 | assert_equal(response.content_type, Mime::YAML)
18 | end
19 |
20 | end
21 |
22 | end
23 |
24 | end
25 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format
4 | # (all these examples are active by default):
5 | # ActiveSupport::Inflector.inflections do |inflect|
6 | # inflect.plural /^(ox)$/i, '\1en'
7 | # inflect.singular /^(ox)en/i, '\1'
8 | # inflect.irregular 'person', 'people'
9 | # inflect.uncountable %w( fish sheep )
10 | # end
11 | #
12 | # These inflection rules are supported but not enabled by default:
13 | # ActiveSupport::Inflector.inflections do |inflect|
14 | # inflect.acronym 'RESTful'
15 | # end
16 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
Envy LabsEnvy Labs
3 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 | Envy LabsEnvy Labs
3 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 | Envy LabsEnvy Labs
3 |
--------------------------------------------------------------------------------
/db/migrate/20100222044149_create_slugs.rb:
--------------------------------------------------------------------------------
1 | class CreateSlugs < ActiveRecord::Migration
2 | def self.up
3 | create_table :slugs do |t|
4 | t.string :name
5 | t.integer :sluggable_id
6 | t.integer :sequence, :null => false, :default => 1
7 | t.string :sluggable_type, :limit => 40
8 | t.string :scope, :limit => 40
9 | t.datetime :created_at
10 | end
11 | add_index :slugs, [:name, :sluggable_type, :scope, :sequence], :name => "index_slugs_on_n_s_s_and_s", :unique => true
12 | add_index :slugs, :sluggable_id
13 | end
14 |
15 | def self.down
16 | drop_table :slugs
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test/unit/project_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class ProjectTest < ActiveSupport::TestCase
4 |
5 | context 'A Project' do
6 |
7 | setup do
8 | @project = FactoryGirl.create(:project)
9 | end
10 |
11 | subject { @project }
12 |
13 | should have_many(:memberships).dependent(:destroy)
14 | should have_many(:users)
15 |
16 | should validate_presence_of(:name)
17 |
18 | should validate_uniqueness_of(:name)
19 |
20 | should 'use the name for the slug' do
21 | assert_equal('this-is-a-test', FactoryGirl.create(:project, :name => 'This is a Test').to_param)
22 | end
23 |
24 | end
25 |
26 | end
27 |
--------------------------------------------------------------------------------
/test/unit/ssh_key_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class SshKeyTest < ActiveSupport::TestCase
4 | context 'An SshKey' do
5 | should belong_to(:user)
6 |
7 | should validate_presence_of(:public_key)
8 |
9 | should allow_value('ssh-dss foo').for(:public_key)
10 | should allow_value('ssh-rsa foo').for(:public_key)
11 |
12 | should_not allow_value('foo').for(:public_key)
13 | should_not allow_value('123').for(:public_key)
14 |
15 | context '' do
16 | setup { FactoryGirl.create(:user).tap { |user| FactoryGirl.create(:ssh_key, :user => user) } }
17 | should validate_uniqueness_of(:public_key)
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/config/unicorn.rb:
--------------------------------------------------------------------------------
1 | worker_processes Integer(ENV["WEB_CONCURRENCY"] || 3)
2 | timeout 15
3 | preload_app true
4 |
5 | before_fork do |server, worker|
6 | Signal.trap 'TERM' do
7 | puts 'Unicorn master intercepting TERM and sending myself QUIT instead'
8 | Process.kill 'QUIT', Process.pid
9 | end
10 |
11 | defined?(ActiveRecord::Base) and
12 | ActiveRecord::Base.connection.disconnect!
13 | end
14 |
15 | after_fork do |server, worker|
16 | Signal.trap 'TERM' do
17 | puts 'Unicorn worker intercepting TERM and doing nothing. Wait for master to send QUIT'
18 | end
19 |
20 | defined?(ActiveRecord::Base) and
21 | ActiveRecord::Base.establish_connection
22 | end
23 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | PRIVATE_SIGNING_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIBOgIBAAJBAMzP+TVDELKOkCXO27VAsstU80cfW3X03MR10jcmofNGJSkea19z\nCpEM/3AzFbuFcYEuuctACnQVfzLd0/DeC18CAwEAAQJAaNOdYnRj7G/pOWCptRhb\nKpTdOy7CehoMkIUZRd8BDuiSV+ty9iw7s2V6JEP45BD6YiV5GKjJPTANhZq62LYu\n0QIhAPCS74e/pkid+tstQaoknX47r73xFW4wPo4w54Xz7UNJAiEA2fIBWZZTuQfQ\nMnV/G2hOXE9GEfYKFWho76LzEqdPMWcCIAM3M5Rw71wRIIVFeZc4nhJN4e98BXlP\nk8Z6yN11gTphAiBpHQb5pj8K5nHLZE/BcDUa4EDzOK70VD8IFJcXUAop0QIhAL5i\nqbPjjCDEpXIN5J5VCgw89nJ4N1OLcXp24mVOKdMx\n-----END RSA PRIVATE KEY-----"
2 | PUBLIC_SIGNING_KEY="-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAMzP+TVDELKOkCXO27VAsstU80cfW3X0\n3MR10jcmofNGJSkea19zCpEM/3AzFbuFcYEuuctACnQVfzLd0/DeC18CAwEAAQ==\n-----END PUBLIC KEY-----"
3 | SECRET_TOKEN="acf66925ec0a264de266251075d52a2e2174394ac6659e831b8a7cc66ed0bc93f02ba7fbae093a2391c055e6a525e4dd1f034043723bdf42ec657242647cfc0b"
4 |
--------------------------------------------------------------------------------
/db/migrate/20100626141851_create_ssh_keys.rb:
--------------------------------------------------------------------------------
1 | class CreateSshKeys < ActiveRecord::Migration
2 | def self.up
3 | create_table :ssh_keys do |t|
4 | t.integer :user_id, :null => false
5 | t.text :public_key, :null => false
6 | t.timestamps
7 | end
8 | add_index :ssh_keys, :user_id
9 |
10 | say_with_time("Migrating SSH keys") do
11 | User.find_each do |user|
12 | user.ssh_keys.create!(:public_key => user.public_ssh_key)
13 | end
14 | end
15 |
16 | remove_column :users, :public_ssh_key
17 | end
18 |
19 | def self.down
20 | add_column :users, :public_ssh_key, :text
21 | User.reset_column_information
22 |
23 | say_with_time("Migrating SSH keys") do
24 | User.find_each do |user|
25 | say "[Warning] Only keeping first key for #{user.login}" if user.ssh_keys.count > 1
26 | user.update_attributes!(:public_ssh_key => user.ssh_keys.first.public_key)
27 | end
28 | end
29 |
30 | remove_index :ssh_keys, :user_id
31 | drop_table :ssh_keys
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ActiveRecord::Base
2 |
3 | has_many :memberships,
4 | :dependent => :destroy
5 | has_many :projects,
6 | :through => :memberships
7 | has_many :ssh_keys,
8 | :dependent => :destroy
9 |
10 | validates_presence_of :login,
11 | :full_name,
12 | :uid
13 |
14 | validates_uniqueness_of :login,
15 | :uid
16 |
17 | validates_length_of :login, :within => 3..50
18 |
19 | validates_format_of :login, :with => %r{^[a-z][a-z0-9]*$}i, :allow_blank => true
20 |
21 | validates_numericality_of :uid, :greater_than_or_equal_to => 5000
22 |
23 | attr_readonly :login,
24 | :uid
25 |
26 | def to_param #:nodoc:
27 | login.parameterize
28 | end
29 |
30 | def keymaster_data
31 | {
32 | :uid => uid,
33 | :login => login,
34 | :full_name => full_name,
35 | :public_ssh_key => ssh_keys.collect { |k| k.public_key }.join("\n")
36 | }
37 | end
38 |
39 | end
40 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require 'rails/all'
4 |
5 | if defined?(Bundler)
6 | # If you precompile assets before deploying to production, use this line
7 | Bundler.require(*Rails.groups(:assets => %w(development test)))
8 | # If you want your assets lazily compiled in production, use this line
9 | # Bundler.require(:default, :assets, Rails.env)
10 | end
11 |
12 | module Keymaster
13 | class Application < Rails::Application
14 | config.encoding = 'utf-8'
15 |
16 | # Enable escaping HTML in JSON.
17 | config.active_support.escape_html_entities_in_json = true
18 |
19 | # Enforce whitelist mode for mass assignment.
20 | # This will create an empty whitelist of attributes available for mass-assignment for all models
21 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible
22 | # parameters by using an attr_accessible or attr_protected declaration.
23 | config.active_record.whitelist_attributes = true
24 |
25 | # Enable the asset pipeline
26 | config.assets.enabled = true
27 |
28 | # Version of your assets, change this if you want to expire all your assets
29 | config.assets.version = '1.0'
30 |
31 | config.middleware.use 'Rack::ApiVersion'
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Keymaster::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Configure static asset server for tests with Cache-Control for performance
11 | config.serve_static_assets = true
12 | config.static_cache_control = "public, max-age=3600"
13 |
14 | # Log error messages when you accidentally call methods on nil
15 | config.whiny_nils = true
16 |
17 | # Show full error reports and disable caching
18 | config.consider_all_requests_local = true
19 | config.action_controller.perform_caching = false
20 |
21 | # Raise exceptions instead of rendering exception templates
22 | config.action_dispatch.show_exceptions = false
23 |
24 | # Disable request forgery protection in test environment
25 | config.action_controller.allow_forgery_protection = false
26 |
27 | # Tell Action Mailer not to deliver emails to the real world.
28 | # The :test delivery method accumulates sent emails in the
29 | # ActionMailer::Base.deliveries array.
30 | config.action_mailer.delivery_method = :test
31 |
32 | # Raise exception on mass assignment protection for Active Record models
33 | config.active_record.mass_assignment_sanitizer = :strict
34 |
35 | # Print deprecation notices to the stderr
36 | config.active_support.deprecation = :stderr
37 | end
38 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Keymaster::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Log error messages when you accidentally call methods on nil.
10 | config.whiny_nils = true
11 |
12 | # Show full error reports and disable caching
13 | config.consider_all_requests_local = true
14 | config.action_controller.perform_caching = false
15 |
16 | # Don't care if the mailer can't send
17 | config.action_mailer.raise_delivery_errors = false
18 |
19 | # Print deprecation notices to the Rails logger
20 | config.active_support.deprecation = :log
21 |
22 | # Only use best-standards-support built into browsers
23 | config.action_dispatch.best_standards_support = :builtin
24 |
25 | # Raise exception on mass assignment protection for Active Record models
26 | config.active_record.mass_assignment_sanitizer = :strict
27 |
28 | # Log the query plan for queries taking more than this (works
29 | # with SQLite, MySQL, and PostgreSQL)
30 | config.active_record.auto_explain_threshold_in_seconds = 0.5
31 |
32 | # Do not compress assets
33 | config.assets.compress = false
34 |
35 | # Expands the lines which load the assets
36 | config.assets.debug = true
37 |
38 | # Exercise the response signing in development mode.
39 | config.middleware.use 'Rack::ResponseSignature', ENV['PRIVATE_SIGNING_KEY']
40 | end
41 |
--------------------------------------------------------------------------------
/test/functional/users_controller_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class UsersControllerTest < ActionController::TestCase
4 |
5 | context 'The UsersController' do
6 |
7 | context 'using GET to index with YAML' do
8 |
9 | setup do
10 | @project = FactoryGirl.create(:project)
11 | FactoryGirl.create(:membership, :project => @project)
12 | FactoryGirl.create(:membership, :project => @project)
13 | get :index, :project_id => @project.to_param, :format => 'yaml'
14 | end
15 |
16 | should respond_with(:success)
17 |
18 | should "respond with YAML" do
19 | assert_equal(response.content_type, Mime::YAML)
20 | end
21 |
22 | end
23 |
24 | context 'using GET to show with YAML' do
25 |
26 | context 'directly' do
27 |
28 | setup do
29 | @user = FactoryGirl.create(:user)
30 | get :show, :id => @user.to_param, :format => 'yaml'
31 | end
32 |
33 | should respond_with(:success)
34 |
35 | should "respond with YAML" do
36 | assert_equal(response.content_type, Mime::YAML)
37 | end
38 |
39 | end
40 |
41 | context 'for a project' do
42 |
43 | context 'for a user of the project' do
44 |
45 | setup do
46 | @project = FactoryGirl.create(:project)
47 | @user = FactoryGirl.create(:user)
48 | @membership = FactoryGirl.create(:membership, :project => @project, :user => @user)
49 | get :show, :project_id => @project.to_param, :id => @user.to_param, :format => 'yaml'
50 | end
51 |
52 | should respond_with(:success)
53 |
54 | should "respond with YAML" do
55 | assert_equal(response.content_type, Mime::YAML)
56 | end
57 |
58 | end
59 |
60 | context 'for a user not in the project' do
61 |
62 | setup do
63 | @project = FactoryGirl.create(:project)
64 | @user = FactoryGirl.create(:user)
65 | @membership = FactoryGirl.create(:membership, :project => @project)
66 | end
67 |
68 | should 'raise RecordNotFound' do
69 | assert_raise(ActiveRecord::RecordNotFound) do
70 | get :show, :project_id => @project.to_param, :id => @user.to_param, :format => 'yaml'
71 | end
72 | end
73 |
74 | end
75 |
76 | end
77 |
78 | end
79 |
80 | end
81 |
82 | end
83 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # Note that this schema.rb definition is the authoritative source for your
6 | # database schema. If you need to create the application database on another
7 | # system, you should be using db:schema:load, not running all the migrations
8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9 | # you'll amass, the slower it'll run and the greater likelihood for issues).
10 | #
11 | # It's strongly recommended to check this file into your version control system.
12 |
13 | ActiveRecord::Schema.define(:version => 20110404015102) do
14 |
15 | create_table "memberships", :force => true do |t|
16 | t.integer "project_id"
17 | t.integer "user_id"
18 | t.datetime "created_at"
19 | t.datetime "updated_at"
20 | end
21 |
22 | add_index "memberships", ["project_id"], :name => "index_memberships_on_project_id"
23 | add_index "memberships", ["user_id"], :name => "index_memberships_on_user_id"
24 |
25 | create_table "projects", :force => true do |t|
26 | t.string "name"
27 | t.datetime "created_at"
28 | t.datetime "updated_at"
29 | t.string "cached_slug"
30 | end
31 |
32 | add_index "projects", ["cached_slug"], :name => "index_projects_on_cached_slug", :unique => true
33 |
34 | create_table "slugs", :force => true do |t|
35 | t.string "name"
36 | t.integer "sluggable_id"
37 | t.integer "sequence", :default => 1, :null => false
38 | t.string "sluggable_type", :limit => 40
39 | t.string "scope", :limit => 40
40 | t.datetime "created_at"
41 | end
42 |
43 | add_index "slugs", ["name", "sluggable_type", "scope", "sequence"], :name => "index_slugs_on_n_s_s_and_s", :unique => true
44 | add_index "slugs", ["sluggable_id"], :name => "index_slugs_on_sluggable_id"
45 |
46 | create_table "ssh_keys", :force => true do |t|
47 | t.integer "user_id", :null => false
48 | t.text "public_key", :null => false
49 | t.datetime "created_at"
50 | t.datetime "updated_at"
51 | end
52 |
53 | add_index "ssh_keys", ["user_id"], :name => "index_ssh_keys_on_user_id"
54 |
55 | create_table "users", :force => true do |t|
56 | t.string "login"
57 | t.string "full_name"
58 | t.integer "uid"
59 | t.datetime "created_at"
60 | t.datetime "updated_at"
61 | end
62 |
63 | add_index "users", ["login"], :name => "index_users_on_login", :unique => true
64 |
65 | end
66 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | ## Envy Labs Key Master
2 |
3 | [](https://travis-ci.org/envylabs/keymaster)
4 | [](https://codeclimate.com/github/envylabs/keymaster)
5 |
6 | http://keymaster.envylabs.com
7 |
8 | The key master, in cooperation with the gate keeper, manages all public key and user access to project servers.
9 |
10 | Users may be added to the system (thereby storing their public SSH key) and then assigned to zero or more projects in the system. The physical project servers then run the gate keeper - via cron - and continuously update their local users and key pairs for their project.
11 |
12 | The actual data transferred is very small, being only YAML-ized Ruby hashes.
13 |
14 | ### Sample cron task
15 |
16 | The following is a sample cron task, assuming that the gate keeper file is stored as "gatekeeper.rb" in root's home directory.
17 |
18 | ```bash
19 | */5 * * * * PROJECT="envylabs" /root/gatekeeper.rb &> /dev/null
20 | ```
21 |
22 | This will automatically sync the system users and their SSH key pairs with the key master every 5 minutes of every hour of every day.
23 |
24 | ### Sample Workflow
25 |
26 | The following is a sample workflow of adding a Project, and assigning users:
27 |
28 | ```ruby
29 | project = Project.create!(:name => 'New Project')
30 | #
31 |
32 | user = User.first
33 | #
34 | user2 = User.create!(:login => 'newlogin', :full_name => 'New User', :public_ssh_key => File.read("id_dsa.pub"), :uid => 5001)
35 | #
36 |
37 | project.users << user
38 | project.users << user2
39 | ```
40 |
41 | Then, for the gatekeeper.rb client, you execute it with:
42 |
43 | ```bash
44 | $ PROJECT="new-project" ./gatekeeper.rb
45 | ```
46 |
47 | This will contact the central server, inquiring for the authorized users for the given project. Those users will then be synchronized to have sudo access with their stored public SSH keys, as well as add their SSH keys to the deploy user on the system (who does not have sudo access).
48 |
49 | ### Environment Variables
50 |
51 | This application expects certain environment variables to be set:
52 |
53 | * PUBLIC_SIGNING_KEY -- Your public key to verify the signature on the client side (gatekeeper.rb)
54 | * PRIVATE_SIGNING_KEY -- Your private key for signing the server responses.
55 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Keymaster::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # Code is not reloaded between requests
5 | config.cache_classes = true
6 |
7 | # Full error reports are disabled and caching is turned on
8 | config.consider_all_requests_local = false
9 | config.action_controller.perform_caching = true
10 |
11 | # Disable Rails's static asset server (Apache or nginx will already do this)
12 | config.serve_static_assets = false
13 |
14 | # Compress JavaScripts and CSS
15 | config.assets.compress = true
16 |
17 | # Don't fallback to assets pipeline if a precompiled asset is missed
18 | config.assets.compile = false
19 |
20 | # Generate digests for assets URLs
21 | config.assets.digest = true
22 |
23 | # Defaults to nil and saved in location specified by config.assets.prefix
24 | # config.assets.manifest = YOUR_PATH
25 |
26 | # Specifies the header that your server uses for sending files
27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
29 |
30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
31 | # config.force_ssl = true
32 |
33 | # See everything in the log (default is :info)
34 | # config.log_level = :debug
35 |
36 | # Prepend all log lines with the following tags
37 | # config.log_tags = [ :subdomain, :uuid ]
38 |
39 | # Use a different logger for distributed setups
40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
41 |
42 | # Use a different cache store in production
43 | # config.cache_store = :mem_cache_store
44 |
45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server
46 | # config.action_controller.asset_host = "http://assets.example.com"
47 |
48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
49 | # config.assets.precompile += %w( search.js )
50 |
51 | # Disable delivery errors, bad email addresses will be ignored
52 | # config.action_mailer.raise_delivery_errors = false
53 |
54 | # Enable threaded mode
55 | # config.threadsafe!
56 |
57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
58 | # the I18n.default_locale when a translation can not be found)
59 | config.i18n.fallbacks = true
60 |
61 | # Send deprecation notices to registered listeners
62 | config.active_support.deprecation = :notify
63 |
64 | # Log the query plan for queries taking more than this (works
65 | # with SQLite, MySQL, and PostgreSQL)
66 | # config.active_record.auto_explain_threshold_in_seconds = 0.5
67 |
68 | config.middleware.use 'Rack::ResponseSignature', ENV['PRIVATE_SIGNING_KEY']
69 | end
70 |
--------------------------------------------------------------------------------
/test/unit/user_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class UserTest < ActiveSupport::TestCase
4 |
5 | context 'A User' do
6 |
7 | setup do
8 | @user = FactoryGirl.create(:user)
9 | end
10 |
11 | subject { @user }
12 |
13 | should have_many(:memberships).dependent(:destroy)
14 | should have_many(:projects)
15 | should have_many(:ssh_keys)
16 |
17 | should validate_presence_of(:login)
18 | should validate_presence_of(:full_name)
19 | should validate_presence_of(:uid)
20 |
21 | should validate_uniqueness_of(:uid)
22 | should validate_uniqueness_of(:login)
23 |
24 | should have_readonly_attribute(:login)
25 | should have_readonly_attribute(:uid)
26 |
27 | should('ensure length of login') { ensure_length_of(:login).is_at_least(3).is_at_most(50) }
28 |
29 | should allow_value(5000).for(:uid)
30 | should allow_value(6000).for(:uid)
31 | should allow_value(10000).for(:uid)
32 |
33 | should_not allow_value(0).for(:uid)
34 | should_not allow_value(1).for(:uid)
35 | should_not allow_value(-100).for(:uid)
36 | should_not allow_value(4999).for(:uid)
37 |
38 | should allow_value('thomas').for(:login)
39 | should allow_value('foo').for(:login)
40 | should allow_value('baz123').for(:login)
41 | should allow_value('HappyBunnies').for(:login)
42 |
43 | should_not allow_value('Happy Bunnies').for(:login)
44 | should_not allow_value('Thomas Meeks').for(:login)
45 | should_not allow_value('1bunny').for(:login)
46 | should_not allow_value('foo@bar.com').for(:login)
47 | should_not allow_value('foo-bar').for(:login)
48 | should_not allow_value('foo.bar').for(:login)
49 | should_not allow_value('.foo').for(:login)
50 |
51 | should 'use the login for the slug' do
52 | assert_equal('fubar', FactoryGirl.build(:user, :login => 'fubar').to_param)
53 | end
54 |
55 | context 'keymaster_data' do
56 | setup { @result = @user.keymaster_data }
57 | should("be a hash") { assert @result.kind_of?(Hash) }
58 | [:login, :uid, :full_name].each do |key|
59 | should "include the User's #{key}" do
60 | assert @result.has_key?(key), "Missing #{key}"
61 | assert_equal @user.send(key), @result[key]
62 | end
63 | end
64 | should 'include the public_ssh_key' do
65 | assert @result.has_key?(:public_ssh_key)
66 | end
67 |
68 | context 'with multiple ssh keys' do
69 | should "concatenate them in public_ssh_key" do
70 | FactoryGirl.create_list(:ssh_key, 2, :user => @user)
71 | assert_equal 2, @user.ssh_keys.count
72 | @result = @user.keymaster_data
73 | @user.ssh_keys.each do |key|
74 | assert @result[:public_ssh_key].include?(key.public_key), "#{key.public_key} not found in #{@result[:public_ssh_key].inspect}"
75 | end
76 | end
77 | end
78 | end
79 |
80 | end
81 |
82 | end
83 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | actionmailer (3.2.14)
5 | actionpack (= 3.2.14)
6 | mail (~> 2.5.4)
7 | actionpack (3.2.14)
8 | activemodel (= 3.2.14)
9 | activesupport (= 3.2.14)
10 | builder (~> 3.0.0)
11 | erubis (~> 2.7.0)
12 | journey (~> 1.0.4)
13 | rack (~> 1.4.5)
14 | rack-cache (~> 1.2)
15 | rack-test (~> 0.6.1)
16 | sprockets (~> 2.2.1)
17 | activemodel (3.2.14)
18 | activesupport (= 3.2.14)
19 | builder (~> 3.0.0)
20 | activerecord (3.2.14)
21 | activemodel (= 3.2.14)
22 | activesupport (= 3.2.14)
23 | arel (~> 3.0.2)
24 | tzinfo (~> 0.3.29)
25 | activeresource (3.2.14)
26 | activemodel (= 3.2.14)
27 | activesupport (= 3.2.14)
28 | activesupport (3.2.14)
29 | i18n (~> 0.6, >= 0.6.4)
30 | multi_json (~> 1.0)
31 | arel (3.0.2)
32 | builder (3.0.4)
33 | dotenv (0.8.0)
34 | dotenv-rails (0.8.0)
35 | dotenv (= 0.8.0)
36 | erubis (2.7.0)
37 | factory_girl (4.2.0)
38 | activesupport (>= 3.0.0)
39 | factory_girl_rails (4.2.1)
40 | factory_girl (~> 4.2.0)
41 | railties (>= 3.0.0)
42 | friendly_id (4.0.9)
43 | hike (1.2.3)
44 | i18n (0.6.4)
45 | journey (1.0.4)
46 | json (1.8.0)
47 | kgio (2.8.0)
48 | mail (2.5.4)
49 | mime-types (~> 1.16)
50 | treetop (~> 1.4.8)
51 | metaclass (0.0.1)
52 | mime-types (1.23)
53 | mocha (0.14.0)
54 | metaclass (~> 0.0.1)
55 | multi_json (1.7.8)
56 | pg (0.16.0)
57 | polyglot (0.3.3)
58 | rack (1.4.5)
59 | rack-cache (1.2)
60 | rack (>= 0.4)
61 | rack-response-signature (0.3.1)
62 | rack (~> 1.0)
63 | rack-ssl (1.3.3)
64 | rack
65 | rack-test (0.6.2)
66 | rack (>= 1.0)
67 | rails (3.2.14)
68 | actionmailer (= 3.2.14)
69 | actionpack (= 3.2.14)
70 | activerecord (= 3.2.14)
71 | activeresource (= 3.2.14)
72 | activesupport (= 3.2.14)
73 | bundler (~> 1.0)
74 | railties (= 3.2.14)
75 | railties (3.2.14)
76 | actionpack (= 3.2.14)
77 | activesupport (= 3.2.14)
78 | rack-ssl (~> 1.3.2)
79 | rake (>= 0.8.7)
80 | rdoc (~> 3.4)
81 | thor (>= 0.14.6, < 2.0)
82 | raindrops (0.11.0)
83 | rake (10.1.0)
84 | rdoc (3.12.2)
85 | json (~> 1.4)
86 | shoulda (3.5.0)
87 | shoulda-context (~> 1.0, >= 1.0.1)
88 | shoulda-matchers (>= 1.4.1, < 3.0)
89 | shoulda-context (1.1.4)
90 | shoulda-matchers (2.2.0)
91 | activesupport (>= 3.0.0)
92 | sprockets (2.2.2)
93 | hike (~> 1.2)
94 | multi_json (~> 1.0)
95 | rack (~> 1.0)
96 | tilt (~> 1.1, != 1.3.0)
97 | thor (0.18.1)
98 | tilt (1.4.1)
99 | treetop (1.4.14)
100 | polyglot
101 | polyglot (>= 0.3.1)
102 | tzinfo (0.3.37)
103 | unicorn (4.6.3)
104 | kgio (~> 2.6)
105 | rack
106 | raindrops (~> 0.7)
107 |
108 | PLATFORMS
109 | ruby
110 |
111 | DEPENDENCIES
112 | dotenv-rails (~> 0.8.0)
113 | factory_girl_rails (~> 4.2)
114 | friendly_id (~> 4.0)
115 | mocha
116 | pg (~> 0.16.0)
117 | rack-response-signature (~> 0.3.1)
118 | rails (= 3.2.14)
119 | shoulda (~> 3.5)
120 | unicorn (~> 4.6)
121 |
--------------------------------------------------------------------------------
/lib/gatekeeper.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # ==Usage
4 | # Load and maintain all users for the 'envy-labs' project:
5 | # PROJECT=envy-labs ruby gatekeeper.rb
6 | # Normally, this would be done from a cron task:
7 | # */5 * * * * PROJECT=envy-labs /root/gatekeeper.rb &>/dev/null
8 | #
9 |
10 | ENV['PATH'] = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
11 |
12 | require 'openssl'
13 | require 'base64'
14 | require 'net/http'
15 | require 'uri'
16 | require 'yaml'
17 | require 'cgi'
18 |
19 | HTTP_ERRORS = [ Timeout::Error,
20 | Errno::EINVAL,
21 | Errno::ECONNRESET,
22 | EOFError,
23 | Net::HTTPBadResponse,
24 | Net::HTTPHeaderSyntaxError,
25 | Net::ProtocolError ] unless defined?(HTTP_ERRORS)
26 |
27 | module Keymaster
28 |
29 | def self.host
30 | @_host ||= '%CURRENT_KEYMASTER_HOST%'
31 | end
32 |
33 | ##
34 | # Returns the version of the Keymaster in which this Gatekeeper instance
35 | # is compatible against.
36 | #
37 | def self.version
38 | @_version ||= '%CURRENT_KEYMASTER_VERSION%'
39 | end
40 |
41 | ##
42 | # Returns a collection of Users allowed to access the requested project.
43 | #
44 | def self.users_for_project(project)
45 | yaml = query("http://#{self.host}/projects/#{project}/users.yaml")
46 | YAML.load(yaml).collect { |user_data| ShellUser.new(user_data) }
47 | end
48 |
49 | ##
50 | # Returns +true+ if the given content (with it's associated signature) came
51 | # from the Keymaster. This method performs an RSA signature verification
52 | # against the pre-shared public key.
53 | #
54 | def self.valid?(content, signature)
55 | rsa.verify(digest, signature, content)
56 | end
57 |
58 |
59 | private
60 |
61 |
62 | def self.rsa
63 | @_rsa ||= OpenSSL::PKey::RSA.new(public_key)
64 | rescue OpenSSL::PKey::RSAError
65 | log("Invalid pre-shared RSA public key. Aborting.")
66 | exit(1)
67 | end
68 |
69 | def self.digest
70 | @_rsa_digest ||= OpenSSL::Digest::SHA256.new
71 | end
72 |
73 | ##
74 | # Returns the RSA Public Key for the Keymaster. All data collected from
75 | # the Keymaster should be signed with the companion RSA Private Key.
76 | #
77 | def self.public_key
78 | @_public_key ||= "%CURRENT_PUBLIC_KEY%"
79 | end
80 |
81 | ##
82 | # Returns the data (body) from GET requesting the given URL.
83 | #
84 | def self.query(url, options = {})
85 | uri = URI.parse(url)
86 | response = Net::HTTP.start(uri.host, uri.port) do |http|
87 | yield(http) if block_given?
88 | http.get(uri.path)
89 | end
90 |
91 | unless response.kind_of?(Net::HTTPSuccess)
92 | log("Non-200 Server Response - Code #{response.code}.", :fail => true)
93 | end
94 |
95 | unless valid?(response.body.strip, Base64.decode64(CGI.unescape(response['X-Response-Signature']))) || options[:ignore_signature]
96 | log("Invalid signature received. Aborting.", :fail => true)
97 | end
98 |
99 | unless current?(response['X-API-Version']) || options[:ignore_version]
100 | log("Local version out-of-date, downloading and aborting.")
101 | update!
102 | exit(0)
103 | end
104 |
105 | response.body
106 | rescue *HTTP_ERRORS
107 | log("HTTP Error occurred: #{$!.class.name} - #{$!.message}", :fail => true)
108 | end
109 |
110 | ##
111 | # Returns +true+ if the given version matches the locally compatible
112 | # Keymaster.version.
113 | #
114 | def self.current?(version)
115 | Keymaster.version == version
116 | end
117 |
118 | ##
119 | # Downloads the newest version of the Gatekeeper from the server and
120 | # overwrites the local installation.
121 | #
122 | def self.update!
123 | data = query("http://#{self.host}/gatekeeper.rb", :ignore_version => true)
124 | File.open(File.expand_path(__FILE__), 'w') { |f| f.write data }
125 | log("Gatekeeper updated.")
126 | end
127 |
128 | end
129 |
130 | module LocalMachine
131 |
132 | module ExecutionResult
133 |
134 | def success?
135 | @success || false
136 | end
137 |
138 | def success=(value)
139 | @success = value
140 | end
141 |
142 | end
143 |
144 | ##
145 | # Execute a file on the server with optional parameters.
146 | #
147 | def self.execute(executable, options = {})
148 | executable_path = `/usr/bin/env which "#{executable}"`.strip
149 | if $?.success?
150 | result = `\"#{executable_path}\" #{options[:parameters]}`.strip
151 | result.extend(ExecutionResult)
152 | result.success = $?.success?
153 | log(%|Execution of "#{executable_path}" #{options[:parameters]} failed|, :fail => options[:fail]) unless result.success? || options[:log_fail] == false
154 | result
155 | else
156 | log(%|Could not locate "#{executable}" in the user's environment|, :fail => true)
157 | end
158 | end
159 |
160 | ##
161 | # Log a message to syslog.
162 | #
163 | def self.log(message, options = {})
164 | puts message
165 | execute("logger", :parameters => %|-i -t "Gatekeeper" "#{message}"|)
166 | exit(1) if options[:fail]
167 | end
168 |
169 | def self.setup!
170 | add_group("sudo")
171 | set_sudo_nopasswd
172 | add_group("webapps")
173 | add_group("envylabs_accounts")
174 | end
175 |
176 |
177 | private
178 |
179 |
180 | ##
181 | # Check for correct sudoer line, add if it doesn't exist.
182 | #
183 | def self.set_sudo_nopasswd
184 | unless execute("grep", :parameters => %|-q "%sudo ALL=NOPASSWD: ALL" /etc/sudoers|, :log_fail => false).success?
185 | execute("echo", :parameters => %|"%sudo ALL=NOPASSWD: ALL" >> /etc/sudoers|, :fail => true)
186 | end
187 | end
188 |
189 | ##
190 | # Add the given group unless it's already present on the system.
191 | #
192 | def self.add_group(group)
193 | unless execute("egrep", :parameters => %|-q ^#{group} /etc/group|, :log_fail => false).success?
194 | execute("groupadd", :parameters => group, :fail => true)
195 | end
196 | end
197 | end
198 |
199 | ##
200 | # Envy Labs ShellUsers
201 | #
202 | class ShellUser
203 |
204 | attr_accessor :login, :full_name, :public_key, :uid
205 |
206 |
207 | ##
208 | # Synchronizes shell users on the system with those dictated by the
209 | # Keymaster server for the requested project.
210 | #
211 | # This also synchronizes the deploy user's authorized keys to match those
212 | # users who have access to the server.
213 | #
214 | def self.synchronize(project)
215 | users = Keymaster.users_for_project(project)
216 | add_users(users)
217 | remove_unlisted_users(users)
218 |
219 | new({
220 | :login => 'deploy',
221 | :full_name => 'Application Deployment User',
222 | :public_ssh_key => users.collect { |u| u.public_key }
223 | }, {
224 | :groups => 'webapps'
225 | }).setup!
226 | end
227 |
228 |
229 | def initialize(attributes = {}, options = {})
230 | self.login = attributes['login'] || attributes[:login]
231 | self.full_name = attributes['full_name'] || attributes[:full_name]
232 | self.public_key = attributes['public_ssh_key'] || attributes[:public_ssh_key]
233 | self.uid = attributes['uid'] || attributes[:uid]
234 | @groups = options[:groups]
235 | end
236 |
237 | def setup!
238 | create! unless exists?
239 | synchronize_authorized_keys!
240 | end
241 |
242 | def destroy!
243 | log(%|Destroying "#{self.login}" user|)
244 | execute("killall", :parameters => %|-u "#{self.login}"|)
245 | sleep(2) # allow the user to be logged out
246 | execute("userdel", :parameters => %|-rf "#{self.login}"|, :fail => true)
247 | end
248 |
249 | def synchronize_authorized_keys!
250 | keys = [public_key].flatten
251 |
252 | make_authorized_keys_file
253 |
254 | data = File.read(authorized_keys_path).strip
255 | data = data.gsub(%r{#{Regexp.escape(comment_open)}.*?#{Regexp.escape(comment_close)}}m, '').strip
256 | keys.each { |key| data = data.gsub(%r{#{Regexp.escape(key)}}, '') } # temporarily, explicitly remove keys left over after stripping header block. This is because existing envylabs users will have authorized keys outside of the block (set before this was well managed).
257 | data << "\n#{comment_open}"
258 | keys.each { |key| data << "\n#{key}" }
259 | data << "\n#{comment_close}"
260 | data << "\n"
261 | File.open(authorized_keys_path, 'w') { |file| file.write data }
262 | end
263 |
264 |
265 | private
266 |
267 |
268 | def self.add_users(users)
269 | users.each { |user| user.setup! }
270 | end
271 |
272 | def self.remove_unlisted_users(users)
273 | local_logins = execute("cat", :parameters => %|/etc/group \| grep "^envylabs_accounts"|, :fail => true).split(':').last
274 | return unless local_logins
275 | local_logins = local_logins.strip.split(',')
276 | local_logins = local_logins - users.collect { |u| u.login }
277 | local_logins.each { |login| new('login' => login).destroy! }
278 | end
279 |
280 |
281 | def exists?
282 | execute("egrep", :parameters => "-q ^#{self.login} /etc/passwd", :log_fail => false).success?
283 | end
284 |
285 | def create!
286 | log(%|Creating "#{self.login}" user|)
287 | execute("useradd", :parameters => "--groups #{@groups || 'sudo,envylabs_accounts'} --create-home --shell /bin/bash #{uid ? "--uid #{self.uid}" : ''} --comment \"#{self.full_name}\" --password \`dd if=/dev/urandom count=1 2> /dev/null | sha512sum | cut -c-128\` #{self.login}", :fail => true)
288 | end
289 |
290 | def chown_home
291 | execute("chown", :parameters => %|-R #{login}:#{login} "#{home_path}"|, :fail => true)
292 | end
293 |
294 | def home_path
295 | "/home/#{login}"
296 | end
297 |
298 | def make_home
299 | return if File.exist?(home_path)
300 | execute("mkdir", :parameters => %|-p "#{home_path}"|, :fail => true)
301 | chown_home
302 | end
303 |
304 | def authorized_keys_path
305 | "#{home_path}/.ssh/authorized_keys"
306 | end
307 |
308 | def make_authorized_keys_file
309 | return if File.exist?(authorized_keys_path)
310 | make_home
311 | execute("mkdir", :parameters => %|-p "#{home_path}/.ssh"|, :fail => true)
312 | execute("touch", :parameters => %|"#{authorized_keys_path}"|, :fail => true)
313 | execute("chown", :parameters => %|-R #{login}:#{login} "#{home_path}/.ssh"|, :fail => true)
314 | execute("chmod", :parameters => %|700 "#{home_path}/.ssh"|, :fail => true)
315 | execute("chmod", :parameters => %|600 "#{authorized_keys_path}"|, :fail => true)
316 | end
317 |
318 | def comment_open
319 | '# Begin Gatekeeper generated keys'
320 | end
321 |
322 | def comment_close
323 | '# End Gatekeeper generated keys'
324 | end
325 |
326 | end
327 |
328 |
329 | def log(message, options = {})
330 | LocalMachine.log(message, options)
331 | end
332 |
333 | def execute(executable, options = {})
334 | LocalMachine.execute(executable, options)
335 | end
336 |
337 |
338 | ##
339 | # Need to run as root/sudo
340 | #
341 | unless execute("id", :parameters => %|-u|) == '0'
342 | puts "Please run as root/sudo"
343 | log("Please run as root/sudo", :fail => true)
344 | end
345 |
346 | if ENV['PROJECT'].nil? || ENV['PROJECT'] == ''
347 | puts "Please specify the project"
348 | log("Please specify the project", :fail => true)
349 | end
350 |
351 | LocalMachine.setup!
352 | ShellUser.synchronize(ENV['PROJECT'].to_s.strip.downcase)
353 |
--------------------------------------------------------------------------------