├── .env.development ├── .gitignore ├── .ruby-version ├── .travis.yml ├── CHANGELOG.markdown ├── Gemfile ├── Gemfile.lock ├── Procfile ├── README.markdown ├── Rakefile ├── app ├── controllers │ ├── application_controller.rb │ ├── gate_keeper_controller.rb │ ├── projects_controller.rb │ └── users_controller.rb ├── helpers │ ├── application_helper.rb │ ├── gate_keeper_helper.rb │ ├── projects_helper.rb │ └── users_helper.rb └── models │ ├── keymaster.rb │ ├── keymaster │ └── version.rb │ ├── membership.rb │ ├── project.rb │ ├── ssh_key.rb │ └── user.rb ├── config.ru ├── config ├── application.rb ├── boot.rb ├── database.ci.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── api_version.rb │ ├── backtrace_silencers.rb │ ├── inflections.rb │ ├── load_keymaster.rb │ ├── mime_types.rb │ ├── secret_token.rb │ ├── session_store.rb │ ├── wrap_parameters.rb │ └── yaml_renderer.rb ├── locales │ └── en.yml ├── routes.rb └── unicorn.rb ├── db ├── migrate │ ├── 20100222035824_create_users.rb │ ├── 20100222040924_create_projects.rb │ ├── 20100222042250_create_memberships.rb │ ├── 20100222044149_create_slugs.rb │ ├── 20100626141851_create_ssh_keys.rb │ ├── 20110404013217_add_cached_slug_to_projects.rb │ ├── 20110404014622_add_indices_to_memberships.rb │ └── 20110404015102_add_login_index_to_users.rb ├── schema.rb └── seeds.rb ├── doc └── README_FOR_APP ├── lib ├── gatekeeper.rb ├── rack │ └── api_version.rb └── tasks │ ├── .gitkeep │ └── test.rake ├── public ├── 404.html ├── 422.html ├── 500.html ├── favicon.ico ├── images │ └── envylabs.jpg └── robots.txt ├── script └── rails ├── test ├── factories │ ├── membership_factory.rb │ ├── project_factory.rb │ ├── ssh_key_factory.rb │ └── user_factory.rb ├── functional │ ├── gate_keeper_controller_test.rb │ ├── projects_controller_test.rb │ └── users_controller_test.rb ├── performance │ └── browsing_test.rb ├── private.key ├── public.key ├── test_helper.rb └── unit │ ├── helpers │ ├── gate_keeper_helper_test.rb │ ├── projects_helper_test.rb │ └── users_helper_test.rb │ ├── membership_test.rb │ ├── project_test.rb │ ├── ssh_key_test.rb │ └── user_test.rb └── vendor └── plugins └── .gitkeep /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 1.9.3-p448 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec unicorn -p $PORT -E $RACK_ENV -c ./config/unicorn.rb 2 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ## Envy Labs Key Master 2 | 3 | [![Build Status](https://travis-ci.org/envylabs/keymaster.png?branch=master)](https://travis-ci.org/envylabs/keymaster) 4 | [![Code Climate](https://codeclimate.com/github/envylabs/keymaster.png)](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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/gate_keeper_helper.rb: -------------------------------------------------------------------------------- 1 | module GateKeeperHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/projects_helper.rb: -------------------------------------------------------------------------------- 1 | module ProjectsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/users_helper.rb: -------------------------------------------------------------------------------- 1 | module UsersHelper 2 | end 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/models/keymaster/version.rb: -------------------------------------------------------------------------------- 1 | module Keymaster 2 | VERSION = '2.0.2' 3 | end 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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/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/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/database.ci.yml: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: postgresql 3 | database: keymaster_test 4 | username: postgres 5 | min_messages: WARNING 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/initializers/api_version.rb: -------------------------------------------------------------------------------- 1 | require 'keymaster' 2 | require 'rack/api_version' 3 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/envylabs/keymaster/6392966dac854d0c40fa648f0f5663c345976bc2/lib/tasks/.gitkeep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | Envy Labs
Envy Labs
3 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | Envy Labs
Envy Labs
3 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | Envy Labs
Envy Labs
3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/envylabs/keymaster/6392966dac854d0c40fa648f0f5663c345976bc2/public/favicon.ico -------------------------------------------------------------------------------- /public/images/envylabs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/envylabs/keymaster/6392966dac854d0c40fa648f0f5663c345976bc2/public/images/envylabs.jpg -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /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/membership_factory.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :membership do 3 | project 4 | user 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/public.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PUBLIC KEY----- 2 | MEgCQQC+mf8zhOYg1T18PBbdmU+acVagE9joD2nIW0/Rxxksx/t5mGULXmKV2myz 3 | ZqD0VGVW5Dw1swaRwPvOc57y7JbbAgMBAAE= 4 | -----END RSA PUBLIC KEY----- 5 | 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/unit/helpers/gate_keeper_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class GateKeeperHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/unit/helpers/projects_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ProjectsHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /test/unit/helpers/users_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class UsersHelperTest < ActionView::TestCase 4 | end 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /vendor/plugins/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/envylabs/keymaster/6392966dac854d0c40fa648f0f5663c345976bc2/vendor/plugins/.gitkeep --------------------------------------------------------------------------------