├── .gitignore ├── .rspec ├── .ruby-gemset ├── .ruby-version ├── .travis.yml ├── .yardopts ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Procfile ├── README.md ├── Rakefile ├── app ├── assets │ ├── images │ │ └── .keep │ ├── javascripts │ │ ├── application.js.erb │ │ └── time.js.coffee │ └── stylesheets │ │ ├── application.css.scss.erb │ │ ├── connect.css.scss │ │ ├── devise │ │ └── sessions.css.scss │ │ └── users.css.scss ├── controllers │ ├── application_controller.rb │ ├── concerns │ │ ├── .keep │ │ ├── github_oauth_concern.rb │ │ ├── github_settings_mixin.rb │ │ ├── settings_mixin.rb │ │ └── setup_mixin.rb │ ├── connect_controller.rb │ ├── dashboard_controller.rb │ ├── github_users_controller.rb │ ├── settings_controller.rb │ ├── setup │ │ ├── admin_user_controller.rb │ │ ├── company_controller.rb │ │ ├── email_controller.rb │ │ ├── github_controller.rb │ │ ├── ldap_controller.rb │ │ └── rules_controller.rb │ └── users_controller.rb ├── helpers │ ├── application_helper.rb │ └── github_users_helper.rb ├── jobs │ └── connect_github_user_job.rb ├── mailers │ ├── .keep │ └── user_mailer.rb ├── models │ ├── .keep │ ├── concerns │ │ ├── .keep │ │ └── encryptable.rb │ ├── connect_github_user_status.rb │ ├── github_email.rb │ ├── github_organization_membership.rb │ ├── github_team.rb │ ├── github_user.rb │ ├── setting.rb │ └── user.rb └── views │ ├── connect │ ├── _connect_step.html.erb │ └── index.html.erb │ ├── dashboard │ └── index.html.erb │ ├── devise │ ├── sessions │ │ └── new.html.erb │ └── shared │ │ └── _links.erb │ ├── github_users │ ├── index.html.erb │ └── show.html.erb │ ├── layouts │ └── application.html.erb │ ├── settings │ ├── _active_directory.html.erb │ ├── _company.html.erb │ ├── _email.html.erb │ ├── _github.html.erb │ ├── _rules.html.erb │ └── edit.html.erb │ ├── setup │ ├── admin_user │ │ └── new.html.erb │ ├── company │ │ └── edit.html.erb │ ├── email │ │ └── edit.html.erb │ ├── github │ │ └── edit.html.erb │ ├── ldap │ │ └── edit.html.erb │ └── rules │ │ └── edit.html.erb │ ├── user_mailer │ ├── access_revoked.html.erb │ └── access_revoked.text.erb │ └── users │ ├── _github_user.html.erb │ ├── _github_users.html.erb │ ├── _ldap_user.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ └── show.html.erb ├── bin ├── bundle ├── delayed_job ├── rails ├── rake ├── setup └── spring ├── config.ru ├── config ├── application.rb ├── boot.rb ├── database.yml.example ├── database.yml.travis ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── action_mailer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── cookies_serializer.rb │ ├── devise.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── session_store.rb │ ├── state_machine_patch.rb │ └── wrap_parameters.rb ├── locales │ ├── devise.en.yml │ └── en.yml ├── routes.rb ├── secrets.yml.example └── secrets.yml.travis ├── cookbook ├── .kitchen.yml ├── Berksfile ├── Berksfile.lock ├── Gemfile ├── Gemfile.lock ├── README.md ├── attributes │ ├── database.rb │ ├── default.rb │ ├── engines.rb │ ├── nginx.rb │ ├── ruby.rb │ └── ssh.rb ├── libraries │ └── github_connector_helpers.rb ├── metadata.rb ├── recipes │ ├── cron.rb │ ├── database.rb │ ├── default.rb │ ├── nginx.rb │ ├── ruby.rb │ ├── server.rb │ ├── ssh.rb │ ├── upstart.rb │ └── user.rb ├── templates │ └── default │ │ ├── database.yml.erb │ │ ├── nginx-github-connector.conf.erb │ │ ├── secrets.yml.erb │ │ ├── upstart-github-connector-web.conf.erb │ │ └── upstart-github-connector-worker.conf.erb └── test_data_bags │ └── github_connector │ ├── secrets.json │ ├── ssh.json │ └── ssl_cert.json ├── cortex.yaml ├── db ├── migrate │ ├── 20140619160007_devise_create_users.rb │ ├── 20140624041139_add_github_attrs_to_user.rb │ ├── 20140626181353_create_settings.rb │ ├── 20140708224056_create_emails.rb │ ├── 20140709045852_add_last_sync_to_user.rb │ ├── 20140709191104_add_state_attrs_to_user.rb │ ├── 20140714210644_add_sync_errors_to_user.rb │ ├── 20140722192112_add_github_teams.rb │ ├── 20140724141457_refactor_github_tables.rb │ ├── 20140726214806_move_state_to_github_user.rb │ ├── 20140811194159_add_github_urls.rb │ ├── 20140818012538_add_admin_flag_to_user.rb │ ├── 20140915164525_convert_settings_value_to_text.rb │ ├── 20140917184213_create_delayed_jobs.rb │ ├── 20140917184236_add_connect_github_user_statuses.rb │ ├── 20140920200517_add_remember_token_to_user.rb │ ├── 20141018212156_add_github_user_disabled_teams.rb │ ├── 20160215025445_add_github_organization_memberships.rb │ └── 20210311145806_add_user_department.rb ├── schema.rb └── seeds.rb ├── ldap ├── README.md ├── base.ldif ├── clear.ldif ├── local.schema ├── run-server └── slapd-test.conf.erb ├── lib ├── assets │ └── .keep ├── base_executor.rb ├── github_admin.rb ├── github_connector │ ├── navbar.rb │ └── settings.rb ├── github_synchronizer.rb ├── ldap_synchronizer.rb ├── rules.rb ├── rules │ ├── active_ldap.rb │ ├── base.rb │ ├── email.rb │ ├── github_mfa.rb │ ├── github_oauth.rb │ ├── last_github_sync.rb │ └── last_ldap_sync.rb ├── settings │ ├── base.rb │ └── definition.rb ├── tasks │ ├── .keep │ ├── github.rake │ └── sync.rake └── transition_github_users.rb ├── log └── .keep ├── public ├── 404.html ├── 422.html ├── 500.html └── robots.txt ├── spec ├── controllers │ ├── connect_controller_spec.rb │ ├── dashboard_controller_spec.rb │ ├── github_users_controller_spec.rb │ ├── settings_controller_spec.rb │ ├── setup │ │ ├── admin_user_controller_spec.rb │ │ ├── company_controller_spec.rb │ │ ├── email_controller_spec.rb │ │ ├── github_controller_spec.rb │ │ ├── ldap_controller_spec.rb │ │ └── rules_controller_spec.rb │ └── users_controller_spec.rb ├── factories │ ├── github_email.rb │ ├── github_organization_membership.rb │ ├── github_team.rb │ ├── github_user.rb │ └── user.rb ├── helpers │ ├── application_helper_spec.rb │ └── github_users_helper_spec.rb ├── jobs │ └── connect_github_user_job_spec.rb ├── lib │ ├── github_admin_spec.rb │ ├── github_connector │ │ └── settings_spec.rb │ ├── github_synchronizer_spec.rb │ ├── ldap_synchronizer_spec.rb │ ├── rules │ │ ├── active_ldap_spec.rb │ │ ├── base_spec.rb │ │ ├── email_spec.rb │ │ ├── github_mfa_spec.rb │ │ ├── github_oauth_spec.rb │ │ ├── last_github_sync_spec.rb │ │ └── last_ldap_sync_spec.rb │ ├── rules_spec.rb │ ├── settings │ │ └── base_spec.rb │ └── transition_github_users_spec.rb ├── mailers │ └── user_mailer_spec.rb ├── models │ ├── connect_github_user_status_spec.rb │ ├── github_team_spec.rb │ ├── github_user_spec.rb │ ├── setting_spec.rb │ └── user_spec.rb ├── rails_helper.rb ├── spec_helper.rb ├── support │ └── controller_helpers.rb └── views │ ├── connect │ └── index.html.erb_spec.rb │ ├── layouts │ └── application.html.erb_spec.rb │ └── settings │ └── edit.html.erb_spec.rb └── vendor └── assets ├── javascripts └── .keep └── stylesheets └── .keep /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle 2 | /cookbook/.kitchen 3 | /config/database.yml 4 | /config/secrets.yml 5 | /coverage 6 | /doc 7 | /ldap/slapd-test.conf 8 | /ldap/openldap-data 9 | /log/*.log* 10 | /public/assets 11 | /public/favicon.ico 12 | /tmp 13 | /vendor/bundle 14 | /vendor/engines 15 | /.yardoc 16 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | github-connector 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.3.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | cache: bundler 4 | rvm: 5 | - 2.3.0 6 | before_script: 7 | - cp config/database.yml.travis config/database.yml 8 | - cp config/secrets.yml.travis config/secrets.yml 9 | - bundle exec rake db:create db:migrate 10 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --markup=markdown 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | GitHub Connector CHANGELOG 2 | ========================== 3 | 4 | v0.1.5 5 | ------ 6 | - Upgrade Octokit to latest version [#13] 7 | 8 | v0.1.4 9 | ------ 10 | - Add simple JSON API [#12] 11 | 12 | v0.1.3 13 | ------ 14 | - Update to Ruby 2.3.0 15 | 16 | v0.1.2 17 | ------ 18 | - Update chef cookbook metadata 19 | 20 | v0.1.1 21 | ------ 22 | - Update to Rails 4.2.1 23 | - Update cookbook to Ruby 2.2.1 24 | - Add a call to FileUtils.mkdir_p before running slapd [#2] 25 | - Check ConnectionStatus#error_message before rule failures [#4] 26 | 27 | v0.1.0 28 | ------ 29 | - Initial open source release 30 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '~> 4.2.11.1' 4 | 5 | gem 'autoprefixer-rails' 6 | gem 'bootstrap-sass' 7 | gem 'coffee-rails', '~> 4.0.0' 8 | gem 'compass-rails' 9 | gem 'daemons' 10 | gem 'delayed_job_active_record' 11 | gem 'devise', '>= 3.4.0' 12 | gem 'devise_ldap_authenticatable', '> 0.8.1' 13 | gem 'friendly_id' 14 | gem 'font-awesome-rails' 15 | gem 'jquery-rails' 16 | gem 'oauth2' 17 | gem 'octokit', '> 3.3.1' 18 | gem 'pg' 19 | gem 'puma' 20 | gem 'sanitize' 21 | gem 'sass-rails' 22 | gem 'state_machine' 23 | gem 'turbolinks' 24 | gem 'uglifier', '>= 1.3.0' 25 | 26 | # Add local customizations via rails engines 27 | require 'pathname' 28 | engines_path = Pathname.new(__FILE__).parent.join('vendor', 'engines') 29 | if engines_path.exist? 30 | engines_path.each_child(false) do |engine_name| 31 | gem engine_name.to_s, path: File.join('vendor', 'engines', engine_name) 32 | end 33 | end 34 | 35 | group :development do 36 | gem 'foreman' 37 | gem 'spring' 38 | gem 'therubyracer' 39 | gem 'yard' 40 | end 41 | 42 | group :development, :test do 43 | gem 'database_cleaner' 44 | gem 'rspec-rails' 45 | end 46 | 47 | group :test do 48 | gem 'simplecov', :require => false 49 | gem 'factory_girl_rails' 50 | end 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Rapid7, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec rails s -p $PORT 2 | ldap: ldap/run-server 3 | worker: rake jobs:work 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/github-connector/0de9978ad30d02cea43f986d248c39967d453896/app/assets/images/.keep -------------------------------------------------------------------------------- /app/assets/javascripts/application.js.erb: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require jquery 14 | //= require jquery_ujs 15 | //= require bootstrap-sprockets 16 | //= require turbolinks 17 | //= require_tree . 18 | <% 19 | # Require JS assets from custom engines 20 | engines_path = Rails.root.join('vendor', 'engines') 21 | if engines_path.exist? 22 | Rails.root.join('vendor', 'engines').each_child(false) do |engine_name| 23 | require_asset(engine_name.to_s) 24 | end 25 | end 26 | %> 27 | -------------------------------------------------------------------------------- /app/assets/javascripts/time.js.coffee: -------------------------------------------------------------------------------- 1 | ready = -> 2 | $("span[data-time]").each (i, element) -> 3 | data = $(element).data() 4 | if data.time 5 | date = new Date(data.time) 6 | timezone = /\((.*)\)/.exec(date.toString()) 7 | if timezone 8 | formatted_date = date.toLocaleString() + " " + timezone[1] 9 | else 10 | formatted_date = date.toString() 11 | $(element).html(formatted_date) 12 | 13 | 14 | $(document).ready(ready) 15 | $(document).on('page:load', ready) 16 | -------------------------------------------------------------------------------- /app/assets/stylesheets/application.css.scss.erb: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *=require_self 14 | *=require_tree 15 | <% 16 | # Require CSS assets from custom engines 17 | engines_path = Rails.root.join('vendor', 'engines') 18 | if engines_path.exist? 19 | Rails.root.join('vendor', 'engines').each_child(false) do |engine_name| 20 | require_asset(engine_name.to_s) 21 | end 22 | end 23 | %> 24 | */ 25 | $container-large-desktop: 970px; 26 | @import "compass/css3"; 27 | @import "bootstrap-sprockets"; 28 | @import "bootstrap"; 29 | @import "font-awesome"; 30 | 31 | .gh-main-nav { 32 | margin-bottom: 0; 33 | } 34 | 35 | .gh-main-content { 36 | margin-top: 20px; 37 | margin-bottom: 20px; 38 | &::before { 39 | display: block; 40 | content: ""; 41 | } 42 | } 43 | 44 | .max-col-xs { max-width: 360px; } 45 | .max-col-sm { max-width: $container-sm; } 46 | .max-col-md { max-width: $container-md; } 47 | .max-col-lg { max-width: $container-lg; } 48 | 49 | .jumbotron { 50 | $jumbotron-color: darken($brand-primary, 15%); 51 | 52 | padding: 20px 0; 53 | margin-bottom: 0; 54 | color: lighten($brand-primary, 40%); 55 | @include single-text-shadow(0, 1px, 0, false, rgba(black, 0.1)); 56 | @include background-image(linear-gradient(to top, $jumbotron-color, darken($jumbotron-color, 5%))); 57 | 58 | h1 { 59 | color: white; 60 | font-size: 50px; 61 | margin-top: 0; 62 | &:last-child { margin-bottom: 0; } 63 | } 64 | 65 | .btn-default { 66 | color: white; 67 | background-color: transparent; 68 | &:hover { 69 | color: $jumbotron-color; 70 | background-color: white; 71 | } 72 | } 73 | } 74 | 75 | @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { 76 | .nav-sm-hide { 77 | display: none; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/assets/stylesheets/connect.css.scss: -------------------------------------------------------------------------------- 1 | @import "bootstrap/variables"; 2 | 3 | .connect-steps { 4 | .list-group-item { 5 | padding-left: 32px + $grid-gutter-width; 6 | 7 | &.active { 8 | color: inherit; 9 | background-color: $list-group-bg; 10 | border-color: $list-group-border; 11 | } 12 | 13 | .step-icon { 14 | display: none; 15 | float: left; 16 | font-size: 30px; 17 | margin-left: -(32px + ($grid-gutter-width / 2)); 18 | &.step-icon-complete { color: green; } 19 | &.step-icon-error { color: red; } 20 | } 21 | span.step-icon { 22 | width: 32px; 23 | } 24 | 25 | &.active .step-icon-active { display: inline-block; } 26 | &.complete .step-icon-complete { display: inline-block; } 27 | &.complete .step-icon-active { display: none; } 28 | &.error .step-icon-error { display: inline-block; } 29 | } 30 | 31 | &.inprogress { 32 | .list-group-item.active { 33 | .step-icon-active { display: none; } 34 | .step-icon-loading { display: inline-block; } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/assets/stylesheets/devise/sessions.css.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the devise/sessions controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | body.sessions { 5 | background-color: #eee; 6 | } 7 | 8 | .form-signin { 9 | max-width: 330px; 10 | padding: 15px; 11 | margin: 0 auto; 12 | &.left { 13 | margin: 0; 14 | padding-left: 0; 15 | } 16 | } 17 | .form-signin .form-signin-heading, 18 | .form-signin .checkbox { 19 | margin-bottom: 10px; 20 | } 21 | .form-signin .checkbox { 22 | font-weight: normal; 23 | } 24 | .form-signin .form-control { 25 | position: relative; 26 | height: auto; 27 | -webkit-box-sizing: border-box; 28 | -moz-box-sizing: border-box; 29 | box-sizing: border-box; 30 | padding: 10px; 31 | font-size: 16px; 32 | } 33 | .form-signin .form-control:focus { 34 | z-index: 2; 35 | } 36 | .form-signin input[type="email"] { 37 | margin-bottom: -1px; 38 | border-bottom-right-radius: 0; 39 | border-bottom-left-radius: 0; 40 | } 41 | .form-signin input[type="password"] { 42 | margin-bottom: 10px; 43 | border-top-left-radius: 0; 44 | border-top-right-radius: 0; 45 | } 46 | -------------------------------------------------------------------------------- /app/assets/stylesheets/users.css.scss: -------------------------------------------------------------------------------- 1 | // Place all the styles related to the users controller here. 2 | // They will automatically be included in application.css. 3 | // You can use Sass (SCSS) here: http://sass-lang.com/ 4 | .ldap-account, .github-account { 5 | h1, h2, h3, h4, h5, h6 { 6 | a { color: inherit; } 7 | a:hover { text-decoration: none; } 8 | } 9 | 10 | table .list-unstyled { 11 | margin-bottom: 0; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | rescue_from DeviseLdapAuthenticatable::LdapException do |exception| 3 | render :text => exception, :status => 500 4 | end 5 | # Prevent CSRF attacks by raising an exception. 6 | # For APIs, you may want to use :null_session instead. 7 | protect_from_forgery with: :exception 8 | 9 | before_action :check_configured 10 | before_action :authenticate_user! 11 | before_action :load_navbar 12 | 13 | private 14 | def check_configured 15 | unless Rails.application.settings.configured? 16 | redirect_to setup_url 17 | end 18 | end 19 | 20 | def require_admin 21 | return true if current_user.admin? 22 | render :status => :forbidden, :text => 'Forbidden' 23 | false 24 | end 25 | 26 | def load_navbar 27 | @navbar = GithubConnector::Navbar.new 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/github-connector/0de9978ad30d02cea43f986d248c39967d453896/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/controllers/concerns/github_oauth_concern.rb: -------------------------------------------------------------------------------- 1 | module GithubOauthConcern 2 | extend ActiveSupport::Concern 3 | 4 | protected 5 | 6 | def oauth_authenticity_token 7 | session[:_oauth_state] ||= SecureRandom.base64(32) 8 | end 9 | 10 | def oauth_client 11 | settings = Rails.application.settings 12 | @oauth_client ||= OAuth2::Client.new(settings.github_client_id, settings.github_client_secret, 13 | site: 'https://github.com/', 14 | authorize_url: '/login/oauth/authorize', 15 | token_url: '/login/oauth/access_token' 16 | ) 17 | end 18 | 19 | def oauth_process_auth_code 20 | octokit = Octokit::Client.new(access_token: oauth_auth_code.token) 21 | ghuser = octokit.user 22 | 23 | github_user = GithubUser.find_or_initialize_by(id: ghuser.id) 24 | github_user.login = ghuser.login 25 | github_user.token = oauth_auth_code.token 26 | github_user.user = current_user 27 | github_user.sync! 28 | github_user 29 | end 30 | 31 | def oauth_scope 32 | settings = Rails.application.settings 33 | settings.github_user_oauth_scope 34 | end 35 | 36 | def oauth_validate_authenticity_token 37 | if oauth_state != oauth_authenticity_token 38 | raise ActionController::InvalidAuthenticityToken 39 | end 40 | end 41 | 42 | private 43 | 44 | def oauth_auth_code 45 | @oauth_auth_code ||= oauth_client.auth_code.get_token(oauth_code) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/controllers/concerns/github_settings_mixin.rb: -------------------------------------------------------------------------------- 1 | module GithubSettingsMixin 2 | extend ActiveSupport::Concern 3 | 4 | def github_admin 5 | redirect_to oauth_client.auth_code.authorize_url( 6 | state: oauth_authenticity_token, 7 | scope: admin_oauth_scope, 8 | redirect_uri: url_for(action: 'github_auth_code') 9 | ) 10 | end 11 | 12 | def github_auth_code 13 | oauth_validate_authenticity_token 14 | @github_user = oauth_process_auth_code 15 | Rails.application.settings.github_admin_token = oauth_auth_code.token 16 | flash.notice = "GitHub admin token updated successfully." 17 | redirect_to action: 'edit' 18 | end 19 | 20 | protected 21 | 22 | def admin_oauth_scope 23 | settings = Rails.application.settings 24 | settings.github_admin_oauth_scope 25 | end 26 | 27 | private 28 | 29 | def oauth_code 30 | params[:code] 31 | end 32 | 33 | def oauth_state 34 | params[:state] 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/controllers/concerns/settings_mixin.rb: -------------------------------------------------------------------------------- 1 | module SettingsMixin 2 | extend ActiveSupport::Concern 3 | 4 | PASSWORD_PLACEHOLDER = '|||PWPLACEHOLDER|||' 5 | 6 | included do 7 | before_filter :load_settings 8 | end 9 | 10 | def scrub_password(key) 11 | if @settings.dirty?(key) 12 | @settings.send(key) 13 | else 14 | PASSWORD_PLACEHOLDER 15 | end 16 | end 17 | 18 | private 19 | def keys 20 | Rails.application.settings.keys 21 | end 22 | 23 | def load_settings 24 | @settings = Rails.application.settings.load(keys).disconnect 25 | params = self.params[:settings] || {} 26 | keys.each do |key| 27 | if params.has_key?(key) 28 | next if params[key] == PASSWORD_PLACEHOLDER 29 | if @settings.definition(key).type == :array 30 | params[key] = params[key].split(/\r?\n/).map(&:strip).compact 31 | end 32 | @settings.send("#{key}=", params[key]) 33 | end 34 | end 35 | end 36 | 37 | def test_ldap_connection 38 | ldap = Net::LDAP.new 39 | ldap.host = @settings.ldap_host 40 | ldap.port = @settings.ldap_port 41 | ldap.encryption :simple_tls if @settings.ldap_ssl 42 | ldap.auth @settings.ldap_admin_user, @settings.ldap_admin_password 43 | begin 44 | ldap.bind.tap do |result| 45 | @error = "Invalid admin user or password." unless result 46 | end 47 | rescue => e 48 | @error = e.message 49 | Rails.logger.warn "Cannot LDAP bind: #{e.class} - #{e.message}" 50 | false 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /app/controllers/concerns/setup_mixin.rb: -------------------------------------------------------------------------------- 1 | module SetupMixin 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | skip_before_filter :authenticate_user! 6 | before_filter :check_configured 7 | end 8 | 9 | private 10 | def apply_defaults 11 | default_settings.each do |key, val| 12 | @settings.send("#{key}=", val) unless @settings.send("#{key}") 13 | end 14 | end 15 | 16 | def check_configured 17 | if Rails.application.settings.configured? 18 | redirect_to settings_url 19 | end 20 | end 21 | 22 | # Attempts to figure out the domain name based on the 23 | # URL or company name 24 | # 25 | # @return [String] 26 | def default_domain 27 | if request.host == 'localhost' && !Rails.application.settings.company.blank? 28 | "#{Rails.application.settings.company.downcase.gsub(' ', '_')}.com" 29 | else 30 | request.host 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/controllers/connect_controller.rb: -------------------------------------------------------------------------------- 1 | require 'oauth2' 2 | 3 | class ConnectController < ApplicationController 4 | include GithubOauthConcern 5 | before_filter :load_connect_status, only: [:status] 6 | 7 | def index 8 | @connect_status = ConnectGithubUserStatus.new( 9 | step: :request 10 | ) 11 | end 12 | 13 | def status 14 | render :index 15 | end 16 | 17 | def start 18 | redirect_to oauth_client.auth_code.authorize_url( 19 | state: oauth_authenticity_token, 20 | scope: oauth_scope, 21 | redirect_uri: oauth_redirect_uri 22 | ) 23 | end 24 | 25 | def auth_code 26 | if params[:state] != oauth_authenticity_token 27 | raise ActionController::InvalidAuthenticityToken 28 | end 29 | 30 | connect_job_status = ConnectGithubUserStatus.create!( 31 | user: current_user, 32 | oauth_code: params[:code], 33 | status: :queued, 34 | step: :grant 35 | ) 36 | ConnectGithubUserJob.perform_later(connect_job_status) 37 | redirect_to connect_status_path(connect_job_status) 38 | end 39 | 40 | protected 41 | 42 | def oauth_redirect_uri 43 | url_for action: 'auth_code' 44 | end 45 | 46 | private 47 | 48 | def load_connect_status 49 | @connect_status = ConnectGithubUserStatus.find(params[:id]) 50 | 51 | if @connect_status.user_id != current_user.id 52 | render :status => :forbidden, :text => 'Forbidden' 53 | return false 54 | end 55 | 56 | true 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /app/controllers/dashboard_controller.rb: -------------------------------------------------------------------------------- 1 | class DashboardController < ApplicationController 2 | def index 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/github_users_controller.rb: -------------------------------------------------------------------------------- 1 | class GithubUsersController < ApplicationController 2 | before_filter :load_github_user, except: [:index] 3 | before_filter :require_admin 4 | 5 | def index 6 | # TODO: Pagination 7 | @github_users = GithubUser.includes(:user).order(:login) 8 | respond_to do |format| 9 | format.html 10 | format.json { render json: @github_users } 11 | end 12 | end 13 | 14 | def show 15 | respond_to do |format| 16 | format.html 17 | format.json { render json: @github_user } 18 | end 19 | end 20 | 21 | private 22 | 23 | def load_github_user 24 | @github_user = GithubUser.friendly.find(params[:id]) 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /app/controllers/settings_controller.rb: -------------------------------------------------------------------------------- 1 | class SettingsController < ApplicationController 2 | include SettingsMixin 3 | include GithubOauthConcern 4 | include GithubSettingsMixin 5 | before_filter :require_admin 6 | before_filter :set_section_partials 7 | 8 | def edit 9 | end 10 | 11 | def update 12 | unless test_ldap_connection 13 | render :edit 14 | return 15 | end 16 | @settings.save 17 | 18 | if params[:connect_github] 19 | github_admin 20 | else 21 | flash.notice = "Settings saved successfully." 22 | redirect_to action: :edit 23 | end 24 | end 25 | 26 | def section_partials 27 | { 28 | 'Active Directory' => 'active_directory', 29 | 'GitHub' => 'github', 30 | 'Rules' => 'rules', 31 | 'Email' => 'email', 32 | } 33 | end 34 | private :section_partials 35 | 36 | def set_section_partials 37 | @section_partials = section_partials 38 | end 39 | private :set_section_partials 40 | end 41 | -------------------------------------------------------------------------------- /app/controllers/setup/admin_user_controller.rb: -------------------------------------------------------------------------------- 1 | class Setup::AdminUserController < Devise::SessionsController 2 | include SetupMixin 3 | prepend_before_filter :sign_out_if_signed_in, only: [:new] 4 | 5 | def create 6 | super do |resource| 7 | resource.admin = true 8 | resource.save! 9 | flash.notice = '' 10 | end 11 | end 12 | 13 | protected 14 | 15 | def after_sign_in_path_for(resource) 16 | setup_github_path 17 | end 18 | 19 | def sign_out_if_signed_in 20 | if signed_in? 21 | Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/controllers/setup/company_controller.rb: -------------------------------------------------------------------------------- 1 | class Setup::CompanyController < ApplicationController 2 | include SetupMixin 3 | include SettingsMixin 4 | 5 | def edit 6 | apply_defaults unless @settings.company 7 | end 8 | 9 | def update 10 | @settings.save 11 | 12 | redirect_to setup_ldap_url 13 | end 14 | 15 | private 16 | 17 | def default_settings 18 | { 19 | } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/controllers/setup/email_controller.rb: -------------------------------------------------------------------------------- 1 | class Setup::EmailController < ApplicationController 2 | include SetupMixin 3 | include SettingsMixin 4 | 5 | def edit 6 | apply_defaults unless @settings.smtp_address 7 | end 8 | 9 | def update 10 | @settings.save 11 | 12 | redirect_to setup_rules_url 13 | end 14 | 15 | private 16 | 17 | def default_settings 18 | { 19 | email_from: "github@#{default_domain}", 20 | email_base_url: root_url, 21 | smtp_address: "smtp.#{default_domain}", 22 | smtp_port: '25', 23 | smtp_enable_starttls_auto: true, 24 | } 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/controllers/setup/github_controller.rb: -------------------------------------------------------------------------------- 1 | class Setup::GithubController < ApplicationController 2 | include SetupMixin 3 | include SettingsMixin 4 | include GithubOauthConcern 5 | include GithubSettingsMixin 6 | 7 | def edit 8 | apply_defaults unless @settings.github_orgs 9 | end 10 | 11 | def update 12 | @settings.save 13 | 14 | if params[:connect_github] 15 | github_admin 16 | else 17 | redirect_to setup_email_url 18 | end 19 | end 20 | 21 | private 22 | 23 | def default_settings 24 | s = { 25 | github_check_mfa_team: 'github-connector-2fa-check', 26 | } 27 | unless Rails.application.settings.company.blank? 28 | s[:github_orgs] = [Rails.application.settings.company.downcase.gsub(' ', '-')] 29 | s[:github_default_teams] = ["#{Rails.application.settings.company.downcase.gsub(' ', '-')}-employees"] 30 | end 31 | 32 | s 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/controllers/setup/ldap_controller.rb: -------------------------------------------------------------------------------- 1 | class Setup::LdapController < ApplicationController 2 | include SetupMixin 3 | include SettingsMixin 4 | 5 | def edit 6 | apply_defaults unless @settings.ldap_host 7 | end 8 | 9 | def update 10 | unless test_ldap_connection 11 | render :edit 12 | return 13 | end 14 | @settings.save 15 | 16 | redirect_to setup_admin_url 17 | end 18 | 19 | 20 | private 21 | 22 | def default_settings 23 | { 24 | ldap_host: 'localhost', 25 | ldap_port: 3268, 26 | ldap_ssl: false, 27 | ldap_admin_user: "cn=admin,#{default_base}", 28 | ldap_admin_password: 'secret', 29 | ldap_base: default_base, 30 | ldap_attribute: 'sAMAccountName', 31 | } 32 | end 33 | 34 | def keys 35 | Rails.application.settings.ldap_keys 36 | end 37 | 38 | def default_base 39 | if request.host == 'localhost' 40 | 'dc=example,dc=com' 41 | else 42 | default_domain.split('.').map {|s| "dc=#{s}"}.join(',') 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/controllers/setup/rules_controller.rb: -------------------------------------------------------------------------------- 1 | class Setup::RulesController < ApplicationController 2 | include SetupMixin 3 | include SettingsMixin 4 | 5 | def edit 6 | apply_defaults unless @settings.rule_max_sync_age 7 | end 8 | 9 | def update 10 | @settings.save 11 | 12 | Rails.application.settings.configured = true 13 | flash.notice = "Setup Wizard completed successfully. You may verify settings below." 14 | 15 | redirect_to settings_url 16 | end 17 | 18 | 19 | private 20 | 21 | def default_settings 22 | { 23 | rule_max_sync_age: 86400, 24 | rule_email_regex: "@(#{default_domain.gsub('.', '\.')}|users\\.noreply\\.github\\.com)$", 25 | github_user_requirements: [ 26 | 'Must enable two factor authentication', 27 | 'Must only associate your company email address', 28 | ] 29 | } 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | before_filter :load_user, except: [:index] 3 | before_filter :require_admin, except: [:show] 4 | before_filter :require_admin_or_user, only: [:show] 5 | 6 | def index 7 | # TODO: Pagination 8 | @users = User.includes(:github_users).order(:name) 9 | respond_to do |format| 10 | format.html 11 | format.json { render json: @users } 12 | end 13 | end 14 | 15 | def show 16 | respond_to do |format| 17 | format.html 18 | format.json { render json: @user } 19 | end 20 | end 21 | 22 | def edit 23 | end 24 | 25 | def update 26 | @user.update!(user_params) 27 | redirect_to @user 28 | end 29 | 30 | private 31 | 32 | def load_user 33 | @user = User.friendly.find(params[:id]) 34 | end 35 | 36 | def require_admin_or_user 37 | return true if @user == current_user 38 | require_admin 39 | end 40 | 41 | def user_params 42 | params.require(:user).permit(:admin) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def current_user_path 3 | url_for controller: :users, action: :show, id: current_user ? current_user.username : nil 4 | end 5 | 6 | def jumbotron(&block) 7 | content_for(:jumbotron, &block) 8 | end 9 | 10 | def settings 11 | Rails.application.settings 12 | end 13 | 14 | def title(page_title) 15 | content_for(:title, page_title.to_s) 16 | end 17 | 18 | def nav_section(nav_section) 19 | content_for(:nav_section, nav_section) 20 | end 21 | 22 | def format_time(time) 23 | return nil unless time 24 | content_tag(:span, time.to_s, data: { time: time.utc.iso8601 }) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/helpers/github_users_helper.rb: -------------------------------------------------------------------------------- 1 | module GithubUsersHelper 2 | def github_user_state_label(github_user) 3 | state_class = case github_user.state 4 | when 'disabled' then 'label-danger' 5 | when 'unknown' then 'label-warning' 6 | when 'enabled' then 'label-success' 7 | when 'excluded' then 'label-info' 8 | when 'external' then 'label-info' 9 | end 10 | 11 | content_tag :span, github_user.human_state_name.capitalize, class: ['label', state_class].compact 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/jobs/connect_github_user_job.rb: -------------------------------------------------------------------------------- 1 | class ConnectGithubUserJob < ActiveJob::Base 2 | include GithubOauthConcern 3 | queue_as :default 4 | 5 | def perform(connect_status) 6 | @connect_status = connect_status 7 | 8 | @connect_status.update_attributes!( 9 | status: :running, 10 | step: :grant 11 | ) 12 | 13 | # Process the user's token 14 | begin 15 | @github_user = oauth_process_auth_code 16 | rescue OAuth2::Error => e 17 | Rails.logger.warn "Cannot establish OAuth token: #{e.message}" 18 | @connect_status.update_attributes!( 19 | status: :error, 20 | error_message: e.description 21 | ) 22 | return 23 | end 24 | 25 | @connect_status.update_attributes!( 26 | step: :add, 27 | github_user: @github_user 28 | ) 29 | 30 | # Add to organizations 31 | unless @github_user.add_to_organizations 32 | @connect_status.update_attributes!( 33 | status: :error 34 | ) 35 | return 36 | end 37 | 38 | # Enable user 39 | @github_user.enable if @github_user.can_enable? 40 | 41 | # Mark complete 42 | @connect_status.update_attributes!( 43 | status: :complete, 44 | step: :teams 45 | ) 46 | 47 | rescue => e 48 | Rails.logger.error "Error running ConnectGithubUserJob: #{e}\n\t#{e.backtrace.join("\n\t")}" 49 | @connect_status.update_attributes!( 50 | status: :error, 51 | error_message: e.message 52 | ) 53 | end 54 | 55 | private 56 | 57 | def current_user 58 | @connect_status.user 59 | end 60 | 61 | def oauth_code 62 | @connect_status.oauth_code 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/github-connector/0de9978ad30d02cea43f986d248c39967d453896/app/mailers/.keep -------------------------------------------------------------------------------- /app/mailers/user_mailer.rb: -------------------------------------------------------------------------------- 1 | class UserMailer < ActionMailer::Base 2 | def access_revoked(user, github_user) 3 | @user = user 4 | @github_user = github_user 5 | mail(to: @user.email, subject: 'GitHub Access Revoked') 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/github-connector/0de9978ad30d02cea43f986d248c39967d453896/app/models/.keep -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/github-connector/0de9978ad30d02cea43f986d248c39967d453896/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/concerns/encryptable.rb: -------------------------------------------------------------------------------- 1 | module Encryptable 2 | extend ActiveSupport::Concern 3 | 4 | # The encrypted database salt environment variable. 5 | ENCRYPTED_DATABASE_SALT = 'encryptable.encrypted_database_salt'.freeze 6 | 7 | module ClassMethods 8 | def attr_encryptor(attr) 9 | field = "encrypted_#{attr}" 10 | define_method("#{attr}=") { |val| 11 | unless val == self.send("#{attr}") 12 | self.send("#{field}=", encrypt(val)) 13 | end 14 | } 15 | define_method("#{attr}") { decrypt(self.send(field)) } 16 | end 17 | 18 | def crypt 19 | @crypt ||= begin 20 | salt = ENV[ENCRYPTED_DATABASE_SALT] || '' 21 | key_generator = ActiveSupport::KeyGenerator.new(Rails.application.secrets.database_key, iterations: 2000) 22 | key = key_generator.generate_key(salt) 23 | ActiveSupport::MessageEncryptor.new(key) 24 | end 25 | end 26 | end 27 | 28 | def encrypt(data) 29 | return nil if data.nil? 30 | self.class.crypt.encrypt_and_sign(data) 31 | end 32 | 33 | def decrypt(data) 34 | return nil if data.nil? 35 | self.class.crypt.decrypt_and_verify(data) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/models/connect_github_user_status.rb: -------------------------------------------------------------------------------- 1 | class ConnectGithubUserStatus < ActiveRecord::Base 2 | belongs_to :user 3 | belongs_to :github_user 4 | 5 | def step_complete?(step) 6 | steps_completed.include?(step) 7 | end 8 | 9 | def step_disabled?(step) 10 | steps_disabled.include?(step) 11 | end 12 | 13 | def step_error?(step) 14 | self.step == step && status == :error 15 | end 16 | 17 | def in_progress? 18 | %i(queued running).include?(status) 19 | end 20 | 21 | def complete? 22 | %i(complete).include?(status) 23 | end 24 | 25 | def status 26 | status = read_attribute(:status) 27 | status ? status.to_sym : nil 28 | end 29 | 30 | def step 31 | step = read_attribute(:step) 32 | step ? step.to_sym : nil 33 | end 34 | 35 | def steps 36 | %i(create request grant add teams) 37 | end 38 | 39 | def steps_completed 40 | if step == :request && !github_user_id 41 | [] 42 | elsif status == :complete 43 | steps 44 | else 45 | steps.first(step_index) 46 | end 47 | end 48 | 49 | def steps_disabled 50 | steps.last(steps.count - step_index - 1) 51 | end 52 | 53 | private 54 | 55 | def step_index 56 | steps.index(step) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /app/models/github_email.rb: -------------------------------------------------------------------------------- 1 | class GithubEmail < ActiveRecord::Base 2 | belongs_to :github_user 3 | 4 | default_scope { order(:created_at) } 5 | end 6 | -------------------------------------------------------------------------------- /app/models/github_organization_membership.rb: -------------------------------------------------------------------------------- 1 | class GithubOrganizationMembership < ActiveRecord::Base 2 | belongs_to :github_user 3 | 4 | default_scope { order(:created_at) } 5 | 6 | def admin? 7 | role == 'admin' 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/models/github_team.rb: -------------------------------------------------------------------------------- 1 | class GithubTeam < ActiveRecord::Base 2 | has_and_belongs_to_many :github_users, join_table: :github_user_teams 3 | 4 | attr_accessor :github_admin 5 | 6 | # Finds a GithubTeam using a "full" slug. A full slug 7 | # is the organization and team slug combined with a slash, for example: 8 | # org1/myteam 9 | # 10 | # @param full_slug [String] an organization and team slug separated with a slash 11 | # @return [GithubTeam] 12 | def self.find_by_full_slug(full_slug) 13 | (org, slug) = full_slug.split('/', 2) 14 | where(organization: org, slug: slug).first 15 | end 16 | 17 | # Does this team allow external users? 18 | # 19 | # @return [Boolean] 20 | # @see {GithubConnector::Settings#github_external_users} 21 | def external? 22 | external_teams = Rails.application.settings.github_external_teams 23 | external_teams && (external_teams.include?(slug) || external_teams.include?(full_slug)) 24 | end 25 | 26 | # Returns the "full" slug for this team. A full slug 27 | # is the organization and team slug combined with a slash, for example: 28 | # org1/myteam 29 | # 30 | # @return [String] an organization and team slug separated with a slash 31 | def full_slug 32 | "#{organization}/#{slug}" 33 | end 34 | 35 | def github_admin 36 | @github_admin ||= GithubAdmin.new 37 | end 38 | 39 | # Synchronizes {GithubTeam} attributes and members from Github. 40 | # 41 | # @return [Boolean] true if saved successfully. NOTE: This method returns 42 | # true even if GitHub API errors occur, as long as the error is successfully 43 | # saved to the `sync_error` attribute. 44 | def sync 45 | # TODO: Handle errors 46 | sync_github_team & sync_github_members 47 | end 48 | 49 | # Synchronizes {GithubTeam} attributes and members from GitHub. 50 | # An `ActiveRecord::RecordNotSaved` error is raised if the save 51 | # fails. 52 | # 53 | # @return [void] 54 | def sync! 55 | sync || raise(ActiveRecord::RecordNotSaved) 56 | end 57 | 58 | protected 59 | 60 | def sync_github_team 61 | data = github_admin.team(id) 62 | self.id = data[:id] 63 | self.name = data[:name] 64 | self.organization = data[:organization] 65 | self.slug = data[:slug] 66 | if changed? 67 | save 68 | else 69 | true 70 | end 71 | end 72 | 73 | def sync_github_members 74 | members = github_admin.team_members(id) 75 | added_members = [] 76 | removed_users = [] 77 | 78 | github_users.each do |user| 79 | next if members.has_key?(user.login) 80 | # TODO: Don't remove disabled users??? 81 | removed_users << user 82 | end 83 | github_users.delete(*removed_users) unless removed_users.empty? 84 | 85 | members.each do |login, member| 86 | next if github_users.any? { |user| user.login == login } 87 | added_members << login 88 | end 89 | github_users << GithubUser.where(login: added_members) unless added_members.empty? 90 | 91 | true 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /app/models/setting.rb: -------------------------------------------------------------------------------- 1 | class Setting < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/views/connect/_connect_step.html.erb: -------------------------------------------------------------------------------- 1 | <% 2 | classes = %w(list-group-item step-request) 3 | classes << 'disabled' if @connect_status.step_disabled?(step) 4 | if @connect_status.step_error?(step) 5 | classes << 'error' 6 | else 7 | classes << 'complete' if @connect_status.step_complete?(step) 8 | classes << 'active' if @connect_status.step == step 9 | end 10 | %> 11 | 12 |
  • 13 | 14 | 15 | 16 | 17 | <%= yield %> 18 |
  • 19 | -------------------------------------------------------------------------------- /app/views/dashboard/index.html.erb: -------------------------------------------------------------------------------- 1 | <% jumbotron do %> 2 |

    GitHub Connector

    3 |

    Connect your GitHub.com account(s) to <%= settings.company.present? ? settings.company : "Active Directory" %>

    4 | <% if current_user.github_users.empty? %> 5 | <%= link_to("Connect your GitHub.com account", connect_path, class: "btn btn-default btn-lg") %> 6 | <% else %> 7 | <%= link_to("Add another GitHub.com account", connect_path, class: "btn btn-default btn-lg") %> 8 | <% end %> 9 | <% end %> 10 | 11 | 12 | <%= render partial: 'users/github_users', locals: {user: current_user} %> 13 | 14 | <% if current_user.github_users.empty? %> 15 | <%= link_to('Connect your GitHub Account', connect_path, class: 'btn btn-default') %> 16 | <% else %> 17 | <%= link_to('Add another GitHub Account', connect_path, class: 'btn btn-default') %> 18 | <% end %> 19 | -------------------------------------------------------------------------------- /app/views/devise/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Sign In" %> 2 | <%= form_for(resource, as: resource_name, url: session_path(resource_name), html: {class: 'form-signin'}) do |f| %> 3 |

    Sign in

    4 |

    Sign in using your active directory credentials.

    5 | 6 | <%= f.text_field :username, 7 | autofocus: true, 8 | placeholder: "Username or email address", 9 | required: true, 10 | class: 'form-control' %> 11 | <%= f.password_field :password, 12 | autocomplete: 'off', 13 | placeholder: "Password", 14 | class: 'form-control' %> 15 | 16 | <% if devise_mapping.rememberable? -%> 17 |
    18 | <%= f.label :remember_me do %> 19 | <%= f.check_box :remember_me %>Remember me? 20 | <% end %> 21 |
    22 | <% end -%> 23 | 24 | <%= f.submit "Sign in", class: 'btn btn-lg btn-primary btn-block' %> 25 | <% end %> 26 | 27 | <%= render "devise/shared/links" %> 28 | -------------------------------------------------------------------------------- /app/views/devise/shared/_links.erb: -------------------------------------------------------------------------------- 1 | <%- if controller_name != 'sessions' %> 2 | <%= link_to "Sign in", new_session_path(resource_name) %>
    3 | <% end -%> 4 | 5 | <%- if devise_mapping.registerable? && controller_name != 'registrations' %> 6 | <%= link_to "Sign up", new_registration_path(resource_name) %>
    7 | <% end -%> 8 | 9 | <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> 10 | <%= link_to "Forgot your password?", new_password_path(resource_name) %>
    11 | <% end -%> 12 | 13 | <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> 14 | <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
    15 | <% end -%> 16 | 17 | <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> 18 | <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
    19 | <% end -%> 20 | 21 | <%- if devise_mapping.omniauthable? %> 22 | <%- resource_class.omniauth_providers.each do |provider| %> 23 | <%= link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider) %>
    24 | <% end -%> 25 | <% end -%> 26 | -------------------------------------------------------------------------------- /app/views/github_users/index.html.erb: -------------------------------------------------------------------------------- 1 | <% title "GitHub Users" %> 2 | <% nav_section :github_users %> 3 | <% jumbotron do %> 4 |

    GitHub Users

    5 |

    GitHub users associated with <%= settings.company.present? ? settings.company : 'our' %> GitHub organizations

    6 | <% end %> 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% @github_users.each do |ghuser| %> 18 | 19 | 20 | 25 | 27 | <% end %> 28 | 29 |
    GitHub AccountActive Directory UserState
    <%= link_to(ghuser.login, ghuser) %> 21 | <% if ghuser.user %> 22 | <%= link_to(ghuser.user.name, ghuser.user) %> 23 | <% end %> 24 | <%= github_user_state_label(ghuser) %> 26 |
    30 | -------------------------------------------------------------------------------- /app/views/github_users/show.html.erb: -------------------------------------------------------------------------------- 1 | <% title @github_user.login %> 2 | <% nav_section :github_users %> 3 | <% jumbotron do %> 4 |

    <%= @github_user.login %>

    5 | <% end %> 6 | 7 | <%= render partial: 'users/github_user', locals: {github_user: @github_user} %> 8 | -------------------------------------------------------------------------------- /app/views/settings/_active_directory.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | <%= label_tag(:settings_ldap_host, 'Host', class: 'control-label col-sm-2') %> 3 |
    4 | <%= text_field(:settings, :ldap_host, class: 'form-control') %> 5 | Hostname of the server running LDAP. 6 |
    7 |
    8 | 9 |
    10 | <%= label_tag(:settings_ldap_port, 'Port', class: 'control-label col-sm-2') %> 11 |
    12 | <%= text_field(:settings, :ldap_port, class: 'form-control') %> 13 |
    14 | <%= label_tag(:settings_ldap_ssl) do %> 15 | <%= check_box(:settings, :ldap_ssl) %> Use SSL? 16 | <% end %> 17 |
    18 |
    19 |
    20 | 21 |
    22 | <%= label_tag(:settings_ldap_admin_user, 'User', class: 'control-label col-sm-2') %> 23 |
    24 | <%= text_field(:settings, :ldap_admin_user, class: 'form-control') %> 25 | User to log in to LDAP. Example: cn=foouser,dc=example,dc=com. 26 |
    27 |
    28 | 29 |
    30 | <%= label_tag(:settings_ldap_admin_password, 'Password', class: 'control-label col-sm-2') %> 31 |
    32 | <%= password_field(:settings, 33 | :ldap_admin_password, 34 | value: controller.scrub_password(:ldap_admin_password), 35 | class: 'form-control') %> 36 |
    37 |
    38 | 39 |
    40 | <%= label_tag(:settings_ldap_base, 'Base', class: 'control-label col-sm-2') %> 41 |
    42 | <%= text_field(:settings, :ldap_base, class: 'form-control') %> 43 | Root node in LDAP from which to search for users and groups. Example: cn=users,dc=example,dc=com. 44 |
    45 |
    46 | 47 |
    48 | <%= label_tag(:settings_ldap_attribute, 'Attribute', class: 'control-label col-sm-2') %> 49 |
    50 | <%= text_field(:settings, :ldap_attribute, class: 'form-control') %> 51 | LDAP username attribute. 52 |
    53 |
    54 | 55 | -------------------------------------------------------------------------------- /app/views/settings/_company.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | <%= label_tag(:settings_company, 'Company', class: 'control-label col-sm-2') %> 3 |
    4 | <%= text_field(:settings, :company, class: 'form-control') %> 5 | The company name to display. 6 |
    7 |
    8 | -------------------------------------------------------------------------------- /app/views/settings/_github.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | <%= label_tag(:settings_github_client_id, 'Client ID', class: 'control-label col-sm-2') %> 3 |
    4 | <%= text_field(:settings, :github_client_id, class: 'form-control') %> 5 | GitHub OAuth application client ID. 6 |
    7 |
    8 | 9 |
    10 | <%= label_tag(:settings_github_client_secret, 'Client secret', class: 'control-label col-sm-2') %> 11 |
    12 | <%= text_field(:settings, :github_client_secret, class: 'form-control') %> 13 | GitHub OAuth application client secret. 14 |
    15 |
    16 | 17 |
    18 | <%= label_tag(:settings_github_admin_token, 'Admin token', class: 'control-label col-sm-2') %> 19 |
    20 |
    21 | <%= password_field(:settings, 22 | :github_admin_token, 23 | value: @settings.github_admin_token ? controller.scrub_password(:github_admin_token) : nil, 24 | class: 'form-control') %> 25 | 26 | <%= button_tag "Connect to my GitHub", class: 'btn btn-default', name: 'connect_github', value: 'connect' %> 27 | 28 |
    29 | A GitHub user OAuth token used for synchronizing organizations. The connect button will authorize your GitHub account. 30 |
    31 |
    32 | 33 |
    34 | <%= label_tag(:settings_github_orgs, 'Organizations', class: 'control-label col-sm-2') %> 35 |
    36 | <%= text_area(:settings, 37 | :github_orgs, 38 | value: @settings.github_orgs ? @settings.github_orgs.join("\r\n") : nil, 39 | class: 'form-control') %> 40 | GitHub organizations, one slug per line, to manage with this connector. 41 |
    42 |
    43 | 44 |
    45 | <%= label_tag(:settings_github_check_mfa_team, '2FA team', class: 'control-label col-sm-2') %> 46 |
    47 | <%= text_field(:settings, :github_check_mfa_team, class: 'form-control') %> 48 | A GitHub team with no privileges used to check 2FA for new users. When a GitHub user is first added, they will be temporarily added to this team so we can check 2FA status. 49 |
    50 |
    51 | 52 |
    53 | <%= label_tag(:settings_github_default_teams, 'Default teams', class: 'control-label col-sm-2') %> 54 |
    55 | <%= text_area(:settings, 56 | :github_default_teams, 57 | value: @settings.github_default_teams ? @settings.github_default_teams.join("\r\n") : nil, 58 | class: 'form-control') %> 59 | GitHub teams, one slug per line, all users should belong to. 60 |
    61 |
    -------------------------------------------------------------------------------- /app/views/settings/_rules.html.erb: -------------------------------------------------------------------------------- 1 |
    2 | <%= label_tag(:settings_enforce_rules, 'Enforce', class: 'control-label col-sm-2') %> 3 |
    4 |
    5 | <%= label_tag(:settings_enforce_rules) do %> 6 | <%= check_box(:settings, :enforce_rules) %> Enforce rules? 7 | <% end %> 8 |
    9 | If checked, GitHub users will be removed or restricted to external teams depending on their state. If unchecked, the GitHub connector runs in dry-run mode - no changes to GitHub users will be made. 10 |
    11 |
    12 | 13 |
    14 | <%= label_tag(:settings_github_user_requirements, 'User requirements', class: 'control-label col-sm-2') %> 15 |
    16 | <%= text_area(:settings, 17 | :github_user_requirements, 18 | value: @settings.github_user_requirements ? @settings.github_user_requirements.join("\r\n") : nil, 19 | class: 'form-control') %> 20 | A list of user requirements, one per line. These will be displayed to users when adding new GitHub accounts. 21 |
    22 |
    23 | 24 |
    25 | <%= label_tag(:settings_rule_email_regex, 'Email regex', class: 'control-label col-sm-2') %> 26 |
    27 | <%= text_field(:settings, :rule_email_regex, class: 'form-control') %> 28 | Regular expression used to validate GitHub email addresses. 29 |
    30 |
    31 | 32 |
    33 | <%= label_tag(:settings_rule_max_sync_age, 'Max sync age', class: 'control-label col-sm-2') %> 34 |
    35 |
    36 | <%= text_field(:settings, :rule_max_sync_age, class: 'form-control') %> 37 | seconds 38 |
    39 | Max time since last successful GitHub and Active Directory synchronization before disabling users. 40 |
    41 |
    42 | 43 |
    44 | <%= label_tag(:settings_github_exclude_users, 'Exclude Users', class: 'control-label col-sm-2') %> 45 |
    46 | <%= text_area(:settings, 47 | :github_exclude_users, 48 | value: @settings.github_exclude_users ? @settings.github_exclude_users.join("\r\n") : nil, 49 | class: 'form-control') %> 50 | GitHub users, one username per line, to exclude when removing users. 51 |
    52 |
    53 | 54 |
    55 | <%= label_tag(:settings_github_external_teams, 'External Teams', class: 'control-label col-sm-2') %> 56 |
    57 | <%= text_area(:settings, 58 | :github_external_teams, 59 | value: @settings.github_external_teams ? @settings.github_external_teams.join("\r\n") : nil, 60 | class: 'form-control') %> 61 | GitHub teams, one slug per line, that allow external users. 62 |
    63 |
    64 | -------------------------------------------------------------------------------- /app/views/settings/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Settings" %> 2 | <% nav_section :settings %> 3 | <% jumbotron do %> 4 |

    Settings

    5 |

    Active Directory, GitHub and application settings

    6 | <% end %> 7 | 8 | <%= form_tag({action: :update}, method: :put, class: 'form-horizontal max-col-sm') do %> 9 | <% if @error %> 10 |
    <%= @error %>
    11 | <% end %> 12 | 13 | <%= render 'company' %> 14 | 15 | <% @section_partials.each do |header, partial| %> 16 |

    <%= header %>

    17 | <%= render partial %> 18 | <% end %> 19 | 20 |
    21 |
    22 | <%= submit_tag('Save', class: 'btn btn-default btn-primary') %> 23 |
    24 |
    25 | <% end %> 26 | -------------------------------------------------------------------------------- /app/views/setup/admin_user/new.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Create admin user - Setup Wizard" %> 2 | <% nav_section :settings %> 3 | <% jumbotron do %> 4 |

    Setup Wizard

    5 |

    Step 3: Create initial admin user

    6 | <% end %> 7 | 8 | 9 |

    Create the initial admin user by signing in with your Active Directory credentials. This user will become an administrator for the GitHub Connector.

    10 | 11 | <%= form_for(resource, as: resource_name, url: setup_admin_path, html: {class: 'form-signin left'}) do |f| %> 12 | 13 | <%= f.text_field :username, 14 | autofocus: true, 15 | placeholder: "Username or email address", 16 | required: true, 17 | class: 'form-control' %> 18 | <%= f.password_field :password, 19 | autocomplete: 'off', 20 | placeholder: "Password", 21 | class: 'form-control' %> 22 | 23 | <%= f.submit "Create admin user", class: 'btn btn-lg btn-primary btn-block' %> 24 | <% end %> 25 | -------------------------------------------------------------------------------- /app/views/setup/company/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Setup Wizard" %> 2 | <% nav_section :settings %> 3 | <% jumbotron do %> 4 |

    Setup Wizard

    5 |

    Step 1: Company settings

    6 | <% end %> 7 | 8 |

    Let's get started configuring the Github Connector.

    9 | 10 |

    This wizard will guide you through configuring the Github Connector. Along the way, we'll make some educated guesses for defaults, but please review each configuration option as you go. You may always update settings after completing the wizard.

    11 | 12 |

    First, enter some information about your company below.

    13 | 14 | <%= form_tag({action: :update}, method: :put, class: 'form-horizontal max-col-sm') do %> 15 | <% if @error %> 16 |
    <%= @error %>
    17 | <% end %> 18 | 19 |

    Company

    20 | <%= render 'settings/company' %> 21 | 22 |
    23 |
    24 | <%= submit_tag('Next', class: 'btn btn-default btn-primary') %> 25 |
    26 |
    27 | <% end %> 28 | -------------------------------------------------------------------------------- /app/views/setup/email/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Setup Wizard" %> 2 | <% nav_section :settings %> 3 | <% jumbotron do %> 4 |

    Setup Wizard

    5 |

    Step 5: Email settings

    6 | <% end %> 7 | 8 |

    The Github Connector sends emails when disabling Github access. Configure your SMTP settings below.

    9 | 10 | <%= form_tag({action: :update}, method: :put, class: 'form-horizontal max-col-sm') do %> 11 | <% if @error %> 12 |
    <%= @error %>
    13 | <% end %> 14 | 15 |

    Email

    16 | <%= render 'settings/email' %> 17 | 18 |
    19 |
    20 | <%= submit_tag('Next', class: 'btn btn-default btn-primary') %> 21 |
    22 |
    23 | <% end %> 24 | -------------------------------------------------------------------------------- /app/views/setup/github/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Setup Wizard" %> 2 | <% nav_section :settings %> 3 | <% jumbotron do %> 4 |

    Setup Wizard

    5 |

    Step 4: Github settings

    6 | <% end %> 7 | 8 |

    Enter information about your Github.com organization(s) below.

    9 | 10 | <%= form_tag({action: :update}, method: :put, class: 'form-horizontal max-col-sm') do %> 11 | <% if @error %> 12 |
    <%= @error %>
    13 | <% end %> 14 | 15 |

    Github

    16 | <%= render 'settings/github' %> 17 | 18 |
    19 |
    20 | <%= submit_tag('Next', class: 'btn btn-default btn-primary') %> 21 |
    22 |
    23 | <% end %> 24 | -------------------------------------------------------------------------------- /app/views/setup/ldap/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Setup Wizard" %> 2 | <% nav_section :settings %> 3 | <% jumbotron do %> 4 |

    Setup Wizard

    5 |

    Step 2: Active Directory settings

    6 | <% end %> 7 | 8 |

    The Github Connector connects to Active Directory using a service account to read information about employees. Enter your Active Directory settings below.

    9 | 10 | <%= form_tag({action: :update}, method: :put, class: 'form-horizontal max-col-sm') do %> 11 | <% if @error %> 12 |
    <%= @error %>
    13 | <% end %> 14 | 15 |

    Active Directory

    16 | <%= render 'settings/active_directory' %> 17 | 18 |
    19 |
    20 | <%= submit_tag('Test and Next', class: 'btn btn-default btn-primary') %> 21 |
    22 |
    23 | <% end %> 24 | -------------------------------------------------------------------------------- /app/views/setup/rules/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Setup Wizard" %> 2 | <% nav_section :settings %> 3 | <% jumbotron do %> 4 |

    Setup Wizard

    5 |

    Step 6: Rules

    6 | <% end %> 7 | 8 |

    The Github Connector decides who can access Github.com based on a list of rules. Configure the rules below.

    9 | 10 | <%= form_tag({action: :update}, method: :put, class: 'form-horizontal max-col-sm') do %> 11 | <% if @error %> 12 |
    <%= @error %>
    13 | <% end %> 14 | 15 |

    Rules

    16 | <%= render 'settings/rules' %> 17 | 18 |
    19 |
    20 | <%= submit_tag('Next', class: 'btn btn-default btn-primary') %> 21 |
    22 |
    23 | <% end %> 24 | -------------------------------------------------------------------------------- /app/views/user_mailer/access_revoked.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

    GitHub access revoked!

    8 |

    9 | Your <%= Rails.application.settings.company %> GitHub access has 10 | been revoked due to the following <%= "error".pluralize(@github_user.failing_rules.count) %>: 11 |

    12 | 17 | 18 |

    19 | Please correct the <%= "error".pluralize(@github_user.failing_rules.count) %> 20 | and reconnect your GitHub account using this link: 21 | <%= link_to(connect_url, connect_url) %> 22 |

    23 | 24 |

    This message was sent from an automated mailbox. Please do not reply.

    25 | 26 | 27 | -------------------------------------------------------------------------------- /app/views/user_mailer/access_revoked.text.erb: -------------------------------------------------------------------------------- 1 | GitHub access revoked! 2 | ====================== 3 | 4 | Your <%= Rails.application.settings.company %> GitHub access has been revoked 5 | due to the following <%= "error".pluralize(@github_user.failing_rules.count) %>: 6 | 7 | <% @github_user.failing_rules.each do |rule| -%> 8 | * <%= rule.error_msg %> 9 | <% end -%> 10 | 11 | Please correct the <%= "error".pluralize(@github_user.failing_rules.count) %> and reconnect your GitHub 12 | account using this link: 13 | <%= connect_url %> 14 | 15 | 16 | --- 17 | This message was sent from an automated mailbox. Please do not reply. 18 | -------------------------------------------------------------------------------- /app/views/users/_github_user.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    3 | 4 | <%= link_to github_user.html_url do %> 5 | <%= image_tag(github_user.avatar_url, height: 32, width: 32) %> 6 | <%= github_user.login %> 7 | <% end %> 8 |

    9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | <% unless github_user.org_memberships.empty? %> 33 | 34 | 35 | 42 | <% end %> 43 | 44 | 45 | 46 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | <% if github_user.sync_error %> 61 | 62 | 63 | 68 | 69 | <% end %> 70 |
    State<%= github_user_state_label(github_user) %>
    <%= 'Email'.pluralize(github_user.emails.size) %> 19 |
      20 | <% github_user.emails.each do |email| %> 21 |
    • <%= email.address %>
    • 22 | <% end %> 23 |
    24 |
    2FA<%= github_user.mfa ? "Yes" : "No" %>
    Organizations 36 | 41 |
    <%= 'Team'.pluralize(github_user.teams.size) %> 47 |
      48 | <% github_user.teams.each do |team| %> 49 |
    • <%= link_to team.full_slug, "https://github.com/orgs/#{team.organization}/teams/#{team.slug}/repositories" %>
    • 50 | <% end %> 51 |
    52 |
    Last sync<%= format_time(github_user.last_sync_at) %>
    Sync error 64 | <%= github_user.sync_error %> 65 |
    66 | <%= format_time(github_user.sync_error_at) %> 67 |
    71 |
    72 | 73 | -------------------------------------------------------------------------------- /app/views/users/_github_users.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    <%= 'GitHub Account'.pluralize(user.github_users.size) %>

    3 | <% if user.github_users.empty? %> 4 |

    No linked GitHub accounts.

    5 | <% else %> 6 | <%= render partial: 'users/github_user', collection: user.github_users.order(updated_at: :desc) %> 7 | <% end %> 8 |
    9 | -------------------------------------------------------------------------------- /app/views/users/_ldap_user.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    Active Directory

    3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | <% if @user.ldap_sync_error %> 22 | 23 | 24 | 29 | 30 | <% end %> 31 |
    Username<%= user.username %>
    Email<%= @user.email %>
    Flags<%= @user.ldap_account_control_flags.map {|flag| flag.to_s.humanize}.join(', ') %>
    Last sync<%= format_time(@user.last_ldap_sync) %>
    Sync error 25 | <%= @user.ldap_sync_error %> 26 |
    27 | <%= format_time(@user.ldap_sync_error_at) %> 28 |
    32 |
    33 | -------------------------------------------------------------------------------- /app/views/users/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% title @user.name %> 2 | <% jumbotron do %> 3 |

    <%= @user.name %>

    4 | <% end %> 5 | 6 | <%= form_for :user, 7 | url: user_path(@user), 8 | method: :patch, 9 | html: { 10 | class: 'form-horizontal max-col-sm' 11 | } do |f| %> 12 | 13 |
    14 | <%= f.label :admin, class: 'control-label col-sm-2' %> 15 |
    16 | <%= f.check_box :admin %> 17 | Allows the user to administer the GitHub Connector application. 18 |
    19 |
    20 | 21 |
    22 |
    23 | <%= f.submit class: 'btn btn-default btn-primary' %> 24 | <%= link_to "Cancel", @user, class: 'btn btn-default' %> 25 |
    26 |
    27 | 28 | <% end %> 29 | -------------------------------------------------------------------------------- /app/views/users/index.html.erb: -------------------------------------------------------------------------------- 1 | <% title "Users" %> 2 | <% nav_section :users %> 3 | <% jumbotron do %> 4 |

    Users

    5 |

    Active Directory users who have logged in to the GitHub Connector

    6 | <% end %> 7 | 8 | <% 9 | # Users can have more than one GitHub user, which we want to display like: 10 | # 11 | # LDAP User | GitHub User | State 12 | # -----------+---------------+--------- 13 | # User 1 | GH User 1a | Enabled 14 | # | GH User 1b | Enabled 15 | # User 2 | GH User 2 | Enabled 16 | # User 3 | | 17 | %> 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | <% @users.each do |user| %> 27 | <% github_users = user.github_users.empty? ? [nil] : user.github_users %> 28 | <% github_users.each_with_index do |ghuser, i| %> 29 | 30 | <% if i == 0 %> 31 | 34 | <% end %> 35 | <% if ghuser %> 36 | 37 | 38 | <% else %> 39 | 40 | <% end %> 41 | 42 | <% end %> 43 | <% end %> 44 |
    UserGitHub AccountState
    32 | <%= link_to(user.name, user) %> 33 | <%= link_to ghuser.login, user_path(user, anchor: ghuser.login) %><%= github_user_state_label(ghuser) %>None
    45 | -------------------------------------------------------------------------------- /app/views/users/show.html.erb: -------------------------------------------------------------------------------- 1 | <% title @user.name %> 2 | <% nav_section :user if @user == current_user %> 3 | <% jumbotron do %> 4 |

    <%= @user.name %>

    5 | <% end %> 6 | 7 | <%= render partial: 'users/ldap_user', locals: {user: @user} %> 8 | <%= render partial: 'users/github_users', locals: {user: @user} %> 9 | 10 |

    Internal

    11 | 12 | 13 | 14 | 15 | 16 | 17 |
    Admin<%= @user.admin? ? 'Yes' : 'No' %>
    18 | 19 | <% if current_user.admin? %> 20 | <%= link_to "Edit", edit_user_path(@user), class: 'btn btn-default' %> 21 | <% end %> 22 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/delayed_job: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment')) 4 | require 'delayed/command' 5 | Delayed::Command.new(ARGV).daemonize 6 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast 4 | # It gets overwritten when you run the `spring binstub` command 5 | 6 | unless defined?(Spring) 7 | require "rubygems" 8 | require "bundler" 9 | 10 | if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ spring \((.*?)\)$.*?^$/m) 11 | ENV["GEM_PATH"] = ([Bundler.bundle_path.to_s] + Gem.path).join(File::PATH_SEPARATOR) 12 | ENV["GEM_HOME"] = "" 13 | Gem.paths = ENV 14 | 15 | gem "spring", match[1] 16 | require "spring/binstub" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /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 Rails.application 5 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module GithubConnector 10 | class Application < Rails::Application 11 | # Settings in config/environments/* take precedence over those specified here. 12 | # Application configuration should go into files in config/initializers 13 | # -- all .rb files in that directory are automatically loaded. 14 | 15 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 16 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 17 | # config.time_zone = 'Central Time (US & Canada)' 18 | 19 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 20 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 21 | # config.i18n.default_locale = :de 22 | 23 | # For not swallow errors in after_commit/after_rollback callbacks. 24 | config.active_record.raise_in_transactional_callbacks = true 25 | 26 | config.active_job.queue_adapter = :delayed_job 27 | 28 | config.autoload_paths << Rails.root.join('lib') 29 | 30 | def settings 31 | @settings ||= Settings.new 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /config/database.yml.example: -------------------------------------------------------------------------------- 1 | development: &default 2 | adapter: postgresql 3 | database: github_connector 4 | pool: 5 5 | timeout: 5 6 | 7 | production: 8 | <<: *default 9 | 10 | # Warning: The database defined as "test" will be erased and 11 | # re-generated from your development database when you run "rake". 12 | # Do not set this db to the same as development or production. 13 | test: 14 | <<: *default 15 | database: github_connector_test 16 | -------------------------------------------------------------------------------- /config/database.yml.travis: -------------------------------------------------------------------------------- 1 | development: &default 2 | adapter: postgresql 3 | database: github_connector 4 | username: postgres 5 | pool: 5 6 | timeout: 5 7 | 8 | production: 9 | <<: *default 10 | 11 | # Warning: The database defined as "test" will be erased and 12 | # re-generated from your development database when you run "rake". 13 | # Do not set this db to the same as development or production. 14 | test: 15 | <<: *default 16 | database: github_connector_test 17 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.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 | # Do not eager load code on boot. 10 | config.eager_load = false 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 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 31 | # yet still be able to expire them through the digest params. 32 | config.assets.digest = true 33 | 34 | # Adds additional error checking when serving assets at runtime. 35 | # Checks for improperly declared sprockets dependencies. 36 | # Raises helpful error messages. 37 | config.assets.raise_runtime_errors = true 38 | 39 | # Raises error for missing translations 40 | # config.action_view.raise_on_missing_translations = true 41 | end 42 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.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 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static asset server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | 37 | # Raises error for missing translations 38 | # config.action_view.raise_on_missing_translations = true 39 | end 40 | -------------------------------------------------------------------------------- /config/initializers/action_mailer.rb: -------------------------------------------------------------------------------- 1 | module ActionMailer 2 | class Base 3 | 4 | # Read mailer configuration settings from the database every time 5 | # we instantiate a new mailer. 6 | def initialize_with_config(*args) 7 | Rails.application.settings.apply_to_action_mailer 8 | initialize_without_config(*args) 9 | end 10 | alias_method_chain :initialize, :config 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /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/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /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. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_github_connector_session' 4 | -------------------------------------------------------------------------------- /config/initializers/state_machine_patch.rb: -------------------------------------------------------------------------------- 1 | # The state_machine gem doesn't support Rails 4.1 out of the box. 2 | # This patches stuff to work. 3 | # 4 | # See: https://github.com/pluginaweek/state_machine/issues/251 5 | module StateMachine 6 | module Integrations 7 | module ActiveModel 8 | public :around_validation 9 | end 10 | 11 | module ActiveRecord 12 | public :around_save 13 | end 14 | end 15 | end 16 | module StateMachine 17 | module Integrations 18 | module ActiveModel 19 | public :around_validation 20 | end 21 | 22 | module ActiveRecord 23 | public :around_save 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /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] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | get 'settings', to: 'settings#edit' 3 | put 'settings', to: 'settings#update' 4 | get 'settings/github_admin', to: 'settings#github_admin' 5 | get 'settings/github_auth_code', to: 'settings#github_auth_code' 6 | 7 | get 'setup', to: redirect('setup/company') 8 | namespace :setup do 9 | # Step 1 10 | get 'company', to: 'company#edit' 11 | put 'company', to: 'company#update' 12 | # Step 2 13 | get 'ldap', to: 'ldap#edit' 14 | put 'ldap', to: 'ldap#update' 15 | # Step 3 16 | devise_scope :user do 17 | get 'admin', to: 'admin_user#new' 18 | post 'admin', to: 'admin_user#create' 19 | end 20 | # Step 4 21 | get 'github', to: 'github#edit' 22 | get 'github_auth_code', to: 'github#github_auth_code' 23 | put 'github', to: 'github#update' 24 | # Step 5 25 | get 'email', to: 'email#edit' 26 | put 'email', to: 'email#update' 27 | # Step 6 28 | get 'rules', to: 'rules#edit' 29 | put 'rules', to: 'rules#update' 30 | end 31 | 32 | get 'connect', to: 'connect#index' 33 | get 'connect/start', to: 'connect#start' 34 | get 'connect/auth_code', to: 'connect#auth_code' 35 | get 'connect/:id', to: 'connect#status', as: 'connect_status', constraints: { id: /\d+/ } 36 | 37 | devise_for :users 38 | 39 | resources :users, only: [:index, :show, :edit, :update], constraints: { id: /[^\/]+?/ } 40 | resources :github_users, only: [:index, :show] 41 | 42 | root 'dashboard#index' 43 | end 44 | -------------------------------------------------------------------------------- /config/secrets.yml.example: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # Make sure the secret is at least 30 characters and all random, 4 | # no regular words or you'll be exposed to dictionary attacks. 5 | # You can use `rake secret` to generate a secure secret key. 6 | # 7 | # Make sure the secrets in this file are kept private 8 | # if you're sharing your code publicly. 9 | 10 | development: &default 11 | # Your secret key is used for verifying the integrity of signed cookies. 12 | # If you change this key, all old signed cookies will become invalid! 13 | secret_key_base: GENERATE A SECRET WITH rake secret AND PASTE IT HERE 14 | 15 | # The secret key used for encrypting sensitive database info. 16 | # If you change this key, all user OAuth tokens will become unreadable! 17 | database_key: GENERATE A SECRET WITH rake secret AND PASTE IT HERE 18 | 19 | production: 20 | <<: *default 21 | 22 | test: 23 | <<: *default 24 | -------------------------------------------------------------------------------- /config/secrets.yml.travis: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # Make sure the secret is at least 30 characters and all random, 4 | # no regular words or you'll be exposed to dictionary attacks. 5 | # You can use `rake secret` to generate a secure secret key. 6 | # 7 | # Make sure the secrets in this file are kept private 8 | # if you're sharing your code publicly. 9 | 10 | development: &default 11 | # Your secret key is used for verifying the integrity of signed cookies. 12 | # If you change this key, all old signed cookies will become invalid! 13 | secret_key_base: ThisIsANot-So-SecretKeyForTravisFOO 14 | 15 | # The secret key used for encrypting sensitive database info. 16 | # If you change this key, all user OAuth tokens will become unreadable! 17 | database_key: ThisIsANot-So-SecretKeyForTravisBAR 18 | 19 | production: 20 | <<: *default 21 | 22 | test: 23 | <<: *default 24 | -------------------------------------------------------------------------------- /cookbook/.kitchen.yml: -------------------------------------------------------------------------------- 1 | --- 2 | driver: 3 | name: vagrant 4 | customize: 5 | memory: 1024 6 | # Use the host's DNS. This ensures everything resolves correctly 7 | # inside the guest when the host is connected to VPN. 8 | natdnshostresolver1: 'on' 9 | 10 | provisioner: 11 | name: chef_solo 12 | 13 | platforms: 14 | - name: ubuntu-14.04 15 | 16 | suites: 17 | - name: default 18 | driver: 19 | network: 20 | - [forwarded_port, {guest: 8080, host: 8080}] 21 | - [forwarded_port, {guest: 8443, host: 8443}] 22 | data_bags_path: test_data_bags 23 | run_list: 24 | - recipe[github_connector::default] 25 | attributes: 26 | authorization: 27 | sudo: 28 | users: [vagrant] 29 | agent_forwarding: true 30 | postgresql: 31 | password: 32 | postgres: insecurepassword 33 | github_connector: 34 | http: 35 | port: 8080 36 | ssl: 37 | port: 8443 38 | enabled: true 39 | -------------------------------------------------------------------------------- /cookbook/Berksfile: -------------------------------------------------------------------------------- 1 | source "https://supermarket.getchef.com" 2 | 3 | # Can switch to upstream once these land: 4 | # https://github.com/fnichol/chef-rvm/pull/186 5 | # https://github.com/fnichol/chef-rvm/pull/212 6 | # https://github.com/fnichol/chef-rvm/pull/247 7 | cookbook 'rvm', '>= 0.9.0', git: 'git://github.com/rapid7-cookbooks/rvm.git', branch: 'patches_0.9.0' 8 | 9 | metadata 10 | 11 | -------------------------------------------------------------------------------- /cookbook/Berksfile.lock: -------------------------------------------------------------------------------- 1 | DEPENDENCIES 2 | github_connector 3 | path: . 4 | metadata: true 5 | rvm 6 | git: git://github.com/rapid7-cookbooks/rvm.git 7 | revision: 19a74ea6e1fcb2c021ef016ce0e482a04a71c108 8 | branch: patches_0.9.0 9 | 10 | GRAPH 11 | apt (2.7.0) 12 | bluepill (2.3.1) 13 | rsyslog (>= 0.0.0) 14 | build-essential (2.2.2) 15 | chef-sugar (3.1.0) 16 | chef_gem (0.1.0) 17 | database (4.0.3) 18 | postgresql (>= 1.0.0) 19 | github_connector (0.1.6) 20 | apt (>= 2.3.10) 21 | database (>= 2.0) 22 | logrotate (>= 1.7.0) 23 | nginx (>= 2.0) 24 | postgresql (~> 3.4) 25 | rvm (= 0.9.0) 26 | ssh_known_hosts (>= 0.0.0) 27 | logrotate (1.9.1) 28 | nginx (2.7.6) 29 | apt (~> 2.2) 30 | bluepill (~> 2.3) 31 | build-essential (~> 2.0) 32 | ohai (~> 2.0) 33 | runit (~> 1.2) 34 | yum-epel (~> 0.3) 35 | ohai (2.0.1) 36 | openssl (4.0.0) 37 | chef-sugar (>= 0.0.0) 38 | partial_search (1.0.8) 39 | postgresql (3.4.18) 40 | apt (>= 1.9.0) 41 | build-essential (>= 0.0.0) 42 | openssl (~> 4.0.0) 43 | rsyslog (1.15.0) 44 | runit (1.5.18) 45 | build-essential (>= 0.0.0) 46 | yum (~> 3.0) 47 | yum-epel (>= 0.0.0) 48 | rvm (0.9.0) 49 | chef_gem (>= 0.0.0) 50 | ssh_known_hosts (2.0.0) 51 | partial_search (>= 0.0.0) 52 | yum (3.5.3) 53 | yum-epel (0.6.0) 54 | yum (~> 3.0) 55 | -------------------------------------------------------------------------------- /cookbook/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'berkshelf' 4 | gem 'kitchen-vagrant' 5 | gem 'test-kitchen' 6 | -------------------------------------------------------------------------------- /cookbook/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.3.8) 5 | berkshelf (4.0.1) 6 | addressable (~> 2.3.4) 7 | berkshelf-api-client (~> 2.0) 8 | buff-config (~> 1.0) 9 | buff-extensions (~> 1.0) 10 | buff-shell_out (~> 0.1) 11 | celluloid (= 0.16.0) 12 | celluloid-io (~> 0.16.1) 13 | cleanroom (~> 1.0) 14 | faraday (~> 0.9.0) 15 | httpclient (~> 2.6.0) 16 | minitar (~> 0.5.4) 17 | octokit (~> 3.0) 18 | retryable (~> 2.0) 19 | ridley (~> 4.3) 20 | solve (~> 1.1) 21 | thor (~> 0.19) 22 | berkshelf-api-client (2.0.0) 23 | faraday (~> 0.9.1) 24 | httpclient (~> 2.6.0) 25 | buff-config (1.0.1) 26 | buff-extensions (~> 1.0) 27 | varia_model (~> 0.4) 28 | buff-extensions (1.0.0) 29 | buff-ignore (1.1.1) 30 | buff-ruby_engine (0.1.0) 31 | buff-shell_out (0.2.0) 32 | buff-ruby_engine (~> 0.1.0) 33 | celluloid (0.16.0) 34 | timers (~> 4.0.0) 35 | celluloid-io (0.16.2) 36 | celluloid (>= 0.16.0) 37 | nio4r (>= 1.1.0) 38 | chef-config (12.6.0) 39 | mixlib-config (~> 2.0) 40 | mixlib-shellout (~> 2.0) 41 | cleanroom (1.0.0) 42 | dep-selector-libgecode (1.0.2) 43 | dep_selector (1.0.3) 44 | dep-selector-libgecode (~> 1.0) 45 | ffi (~> 1.9) 46 | erubis (2.7.0) 47 | faraday (0.9.2) 48 | multipart-post (>= 1.2, < 3) 49 | ffi (1.9.10) 50 | hashie (3.4.3) 51 | hitimes (1.2.3) 52 | httpclient (2.6.0.1) 53 | json (1.8.3) 54 | kitchen-vagrant (0.19.0) 55 | test-kitchen (~> 1.4) 56 | minitar (0.5.4) 57 | mixlib-authentication (1.3.0) 58 | mixlib-log 59 | mixlib-config (2.2.1) 60 | mixlib-log (1.6.0) 61 | mixlib-shellout (2.2.5) 62 | multipart-post (2.0.0) 63 | net-scp (1.2.1) 64 | net-ssh (>= 2.6.5) 65 | net-ssh (2.9.2) 66 | nio4r (1.2.0) 67 | octokit (3.8.0) 68 | sawyer (~> 0.6.0, >= 0.5.3) 69 | retryable (2.0.3) 70 | ridley (4.4.2) 71 | addressable 72 | buff-config (~> 1.0) 73 | buff-extensions (~> 1.0) 74 | buff-ignore (~> 1.1) 75 | buff-shell_out (~> 0.1) 76 | celluloid (~> 0.16.0) 77 | celluloid-io (~> 0.16.1) 78 | chef-config 79 | erubis 80 | faraday (~> 0.9.0) 81 | hashie (>= 2.0.2, < 4.0.0) 82 | httpclient (~> 2.6) 83 | json (>= 1.7.7) 84 | mixlib-authentication (>= 1.3.0) 85 | retryable (~> 2.0) 86 | semverse (~> 1.1) 87 | varia_model (~> 0.4.0) 88 | safe_yaml (1.0.4) 89 | sawyer (0.6.0) 90 | addressable (~> 2.3.5) 91 | faraday (~> 0.8, < 0.10) 92 | semverse (1.2.1) 93 | solve (1.2.1) 94 | dep_selector (~> 1.0) 95 | semverse (~> 1.1) 96 | test-kitchen (1.4.2) 97 | mixlib-shellout (>= 1.2, < 3.0) 98 | net-scp (~> 1.1) 99 | net-ssh (~> 2.7, < 2.10) 100 | safe_yaml (~> 1.0) 101 | thor (~> 0.18) 102 | thor (0.19.1) 103 | timers (4.0.4) 104 | hitimes 105 | varia_model (0.4.1) 106 | buff-extensions (~> 1.0) 107 | hashie (>= 2.0.2, < 4.0.0) 108 | 109 | PLATFORMS 110 | ruby 111 | 112 | DEPENDENCIES 113 | berkshelf 114 | kitchen-vagrant 115 | test-kitchen 116 | 117 | BUNDLED WITH 118 | 1.11.2 119 | -------------------------------------------------------------------------------- /cookbook/README.md: -------------------------------------------------------------------------------- 1 | GitHub Active Directory Connector Cookbook 2 | ========================================== 3 | 4 | Installs and configures the GitHub Active Directory Connector via Chef. 5 | 6 | This performs the following actions: 7 | 8 | 1. Creates a `github` user 9 | 2. Installs PostgreSQL and creates a database 10 | 3. Installs RVM, installs ruby, and configures a `github-connector` gemset 11 | 4. Clones the `github-connector` repository from GitHub 12 | 5. Creates upstart jobs for the web and worker processes 13 | 6. Creates a cron job to synchronize users 14 | -------------------------------------------------------------------------------- /cookbook/attributes/database.rb: -------------------------------------------------------------------------------- 1 | default['github_connector']['db']['host'] = 'localhost' 2 | default['github_connector']['db']['port'] = node['postgresql']['config']['port'] 3 | default['github_connector']['db']['name'] = 'github-connector' 4 | default['github_connector']['db']['user'] = 'github-connector' 5 | -------------------------------------------------------------------------------- /cookbook/attributes/default.rb: -------------------------------------------------------------------------------- 1 | default['github_connector']['user'] = 'github' 2 | default['github_connector']['group'] = node['github_connector']['user'] 3 | default['github_connector']['install_dir'] = '/var/www/github-connector' 4 | 5 | default['github_connector']['repo']['url'] = 'https://github.com/rapid7/github-connector.git' 6 | default['github_connector']['repo']['revision'] = 'v0.1.5' 7 | 8 | # The secrets databag can contain the following keys: 9 | # * database_password 10 | # * database_key 11 | # * secrets_key_base 12 | default['github_connector']['secrets_databag'] = 'github_connector' 13 | default['github_connector']['secrets_databag_item'] = 'secrets' 14 | default['github_connector']['secrets'] = {} 15 | -------------------------------------------------------------------------------- /cookbook/attributes/engines.rb: -------------------------------------------------------------------------------- 1 | default['github_connector']['engines'] = {} 2 | 3 | # Install custom engines like this: 4 | #default['github_connector']['engines']['test_engine'] = { 5 | # 'url' => 'git@github.com:myorg/test_engine.git', 6 | # 'revision' => 'master', 7 | # 'ssh_databag' => 'ssh-keys', 8 | # 'ssh_databag_item => 'test_engine', 9 | #} 10 | -------------------------------------------------------------------------------- /cookbook/attributes/nginx.rb: -------------------------------------------------------------------------------- 1 | default['nginx']['default_site_enabled'] = false 2 | 3 | default['github_connector']['http']['host_name'] = node['fqdn'] 4 | default['github_connector']['http']['host_aliases'] = [] 5 | default['github_connector']['http']['port'] = 80 6 | default['github_connector']['http']['ssl']['port'] = 443 7 | default['github_connector']['http']['ssl']['enabled'] = true 8 | 9 | # The cert databag should have `cert` and `key` keys 10 | default['github_connector']['http']['ssl']['cert_databag'] = 'github_connector' 11 | default['github_connector']['http']['ssl']['cert_databag_item'] = 'ssl_cert' 12 | -------------------------------------------------------------------------------- /cookbook/attributes/ruby.rb: -------------------------------------------------------------------------------- 1 | default['github_connector']['ruby_version'] = 'ruby-2.3.0' 2 | default['github_connector']['ruby_gemset'] = 'github-connector' 3 | default['github_connector']['rvm_alias'] = 'github-connector' 4 | 5 | default['rvm']['version'] = '1.26.11' 6 | default['rvm']['user_rubies'] = [node['github_connector']['ruby_version']] 7 | default['rvm']['user_default_ruby'] = node['github_connector']['ruby_version'] 8 | default['rvm']['user_autolibs'] = 'read-fail' 9 | -------------------------------------------------------------------------------- /cookbook/attributes/ssh.rb: -------------------------------------------------------------------------------- 1 | # To pull from GitHub via ssh, add a data bag with a "private_key" attribute 2 | # containing an SSH private key. Then list the data bag info here: 3 | default['github_connector']['ssh_databag'] = nil 4 | default['github_connector']['ssh_databag_item'] = nil 5 | 6 | 7 | default['github_connector']['github_host_key'] = 'AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==' 8 | -------------------------------------------------------------------------------- /cookbook/libraries/github_connector_helpers.rb: -------------------------------------------------------------------------------- 1 | module GithubConnector 2 | class Helpers 3 | class << self 4 | include Opscode::OpenSSL::Password 5 | 6 | # Loads the given data bag. The databag can be encrypted or unencrypted. 7 | def load_data_bag(data_bag, name) 8 | raw_hash = Chef::DataBagItem.load(data_bag, name) 9 | encrypted = raw_hash.detect do |key, value| 10 | if value.is_a?(Hash) 11 | value.has_key?("encrypted_data") 12 | end 13 | end 14 | if encrypted 15 | secret = Chef::EncryptedDataBagItem.load_secret 16 | Chef::EncryptedDataBagItem.new(raw_hash, secret) 17 | else 18 | raw_hash 19 | end 20 | end 21 | 22 | def database_password(node) 23 | secret('database_password', secure_password, node) 24 | end 25 | 26 | def database_key(node) 27 | secret('database_key', SecureRandom.hex(64), node) 28 | end 29 | 30 | def secret_key_base(node) 31 | secret('secret_key_base', SecureRandom.hex(64), node) 32 | end 33 | 34 | def secret(key, default, node) 35 | data_bag = GithubConnector::Helpers.load_data_bag( 36 | node['github_connector']['secrets_databag'], 37 | node['github_connector']['secrets_databag_item'] 38 | ) rescue nil 39 | 40 | if data_bag && data_bag[key] 41 | return data_bag[key] 42 | end 43 | 44 | unless Chef::Config[:solo] 45 | node.set_unless['github_connector']['secrets'][key] = default 46 | node.save 47 | end 48 | 49 | raise "Must set github_connector.secrets.#{key}!" unless node['github_connector']['secrets'][key] 50 | 51 | node['github_connector']['secrets'][key] 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /cookbook/metadata.rb: -------------------------------------------------------------------------------- 1 | name 'github_connector' 2 | maintainer "Rapid7, Inc." 3 | maintainer_email "engineeringservices@rapid7.com" 4 | license "Apache v2.0" 5 | description "Installs and configures the GitHub Active Directory Connector" 6 | long_description IO.read(File.join(File.dirname(__FILE__), 'README.md')) 7 | source_url "https://github.com/rapid7/github-connector/tree/master/cookbook" 8 | issues_url "https://github.com/rapid7/github-connector/issues" 9 | version "0.1.7" 10 | 11 | supports 'ubuntu' 12 | 13 | depends 'apt', '>= 2.3.10' 14 | depends 'database', '>= 2.0' 15 | depends 'logrotate', '>= 1.7.0' 16 | depends 'nginx', '>= 2.0' 17 | # postgres 4.0 cookbook introduces changes that haven't been tested. 18 | depends 'postgresql', '~> 3.4' 19 | depends 'ssh_known_hosts' 20 | 21 | # rvm is a rapid7 patched version, see Berksfile 22 | depends 'rvm', '= 0.9.0' 23 | -------------------------------------------------------------------------------- /cookbook/recipes/cron.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: github_connector 3 | # Recipe:: cron 4 | # 5 | # Copyright (C) 2014 Brandon Turner 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | cron 'github-connector-cron' do 21 | user node['github_connector']['user'] 22 | minute 56 23 | hour '*/4' 24 | home "/home/#{node['github_connector']['user']}" 25 | command "cd \"#{node['github_connector']['install_dir']}\" && /home/#{node['github_connector']['user']}/.rvm/bin/rvm #{node['github_connector']['rvm_alias']} do rake github:transition_users RAILS_ENV=production >> \"#{node['github_connector']['install_dir']}/log/cron.log\"" 26 | end 27 | -------------------------------------------------------------------------------- /cookbook/recipes/database.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: github_connector 3 | # Recipe:: database 4 | # 5 | # Copyright (C) 2014 Brandon Turner 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | include_recipe 'postgresql::server' 21 | include_recipe 'database::postgresql' 22 | 23 | 24 | postgresql_connection_info = { 25 | :host => "localhost", 26 | :port => node['postgresql']['config']['port'], 27 | :username => 'postgres', 28 | :password => node['postgresql']['password']['postgres'] 29 | } 30 | 31 | # Create database user 32 | postgresql_database_user 'github-connector-database-user' do 33 | connection postgresql_connection_info 34 | username node['github_connector']['db']['user'] 35 | password GithubConnector::Helpers.database_password(node) 36 | end 37 | 38 | # Create database 39 | postgresql_database 'github-connector-database' do 40 | connection postgresql_connection_info 41 | database_name node['github_connector']['db']['name'] 42 | owner node['github_connector']['db']['user'] 43 | end 44 | -------------------------------------------------------------------------------- /cookbook/recipes/default.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: github_connector 3 | # Recipe:: default 4 | # 5 | # Copyright (C) 2014 Brandon Turner 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | include_recipe 'apt' 21 | 22 | package 'git' 23 | 24 | include_recipe 'github_connector::user' 25 | include_recipe 'github_connector::ssh' 26 | include_recipe 'github_connector::database' 27 | include_recipe 'github_connector::ruby' 28 | include_recipe 'github_connector::server' 29 | -------------------------------------------------------------------------------- /cookbook/recipes/nginx.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: github_connector 3 | # Recipe:: nginx 4 | # 5 | # Copyright (C) 2014 Brandon Turner 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | include_recipe 'nginx' 21 | 22 | if node['github_connector']['http']['ssl']['enabled'] 23 | ssl_data_bag = GithubConnector::Helpers.load_data_bag( 24 | node['github_connector']['http']['ssl']['cert_databag'], 25 | node['github_connector']['http']['ssl']['cert_databag_item'] 26 | ) 27 | 28 | # Public key. 29 | file "/etc/ssl/certs/#{node['github_connector']['http']['host_name']}.crt" do 30 | mode 0644 31 | user 'root' 32 | group 'root' 33 | content "#{ssl_data_bag['cert']}" 34 | notifies :reload, 'service[nginx]', :delayed 35 | end 36 | 37 | # Private key. 38 | file "/etc/ssl/private/#{node['github_connector']['http']['host_name']}.key" do 39 | mode 0600 40 | user 'root' 41 | group 'root' 42 | content "#{ssl_data_bag['key']}" 43 | notifies :reload, 'service[nginx]', :delayed 44 | end 45 | end 46 | 47 | template ::File.join(node['nginx']['dir'], 'sites-available', 'github_connector') do 48 | source 'nginx-github-connector.conf.erb' 49 | notifies :reload, 'service[nginx]', :delayed 50 | mode 0644 51 | owner 'root' 52 | group 'root' 53 | action :create 54 | variables( 55 | :host_name => node['github_connector']['http']['host_name'], 56 | :host_aliases => node['github_connector']['http']['host_aliases'] || [], 57 | :ssl_enabled => node['github_connector']['http']['ssl']['enabled'], 58 | :redirect_http => node['github_connector']['http']['ssl']['enabled'], 59 | :listen_port => node['github_connector']['http']['port'], 60 | :ssl_listen_port => node['github_connector']['http']['ssl']['port'], 61 | :install_dir => node['github_connector']['install_dir'] 62 | ) 63 | end 64 | 65 | nginx_site 'github_connector' 66 | -------------------------------------------------------------------------------- /cookbook/recipes/ruby.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: github_connector 3 | # Recipe:: ruby 4 | # 5 | # Copyright (C) 2014 Brandon Turner 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | # gawk is needed to install ruby 2.1.x but is not installed by RVM 21 | package 'gawk' 22 | # libgmp-dev is needed to install some native dependencies with Ruby 2.3.0 23 | package 'libgmp-dev' 24 | 25 | node.default['rvm']['user_installs'] = [{ 26 | user: node['github_connector']['user'], 27 | home: "/home/#{node['github_connector']['user']}", 28 | upgrade: node['rvm']['version'] 29 | }] 30 | include_recipe 'rvm::user' 31 | 32 | rvm_gemset node['github_connector']['ruby_gemset'] do 33 | user node['github_connector']['user'] 34 | ruby_string node['github_connector']['ruby_version'] 35 | end 36 | -------------------------------------------------------------------------------- /cookbook/recipes/ssh.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: github_connector 3 | # Recipe:: ssh 4 | # 5 | # Copyright (C) 2014 Brandon Turner 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | # Create custom wrapper scripts that allow deploying private repos 21 | repos = {} 22 | if node['github_connector']['ssh_databag'] && node['github_connector']['ssh_databag_item'] 23 | repos['github_connector'] = { 24 | 'ssh_databag' => node['github_connector']['ssh_databag'], 25 | 'ssh_databag_item' => node['github_connector']['ssh_databag_item'], 26 | } 27 | end 28 | 29 | node['github_connector']['engines'].each do |engine, attrs| 30 | if attrs['ssh_databag'] && attrs['ssh_databag_item'] 31 | repos[engine] = { 32 | 'ssh_databag' => attrs['ssh_databag'], 33 | 'ssh_databag_item' => attrs['ssh_databag_item'], 34 | } 35 | end 36 | end 37 | 38 | 39 | repos.each do |repo_name, attrs| 40 | ssh_data_bag = GithubConnector::Helpers.load_data_bag( 41 | attrs['ssh_databag'], 42 | attrs['ssh_databag_item'] 43 | ) 44 | 45 | if ssh_data_bag && ssh_data_bag['private_key'] 46 | require 'net/ssh' 47 | private_key = OpenSSL::PKey::RSA.new(ssh_data_bag['private_key']) 48 | public_key = private_key.public_key 49 | ssh_dir = "/home/#{node['github_connector']['user']}/.ssh" 50 | 51 | directory ssh_dir do 52 | mode 0700 53 | owner node['github_connector']['user'] 54 | group node['github_connector']['group'] 55 | end 56 | 57 | file ::File.join(ssh_dir, "#{repo_name}_id_rsa") do 58 | content private_key.to_pem 59 | owner node['github_connector']['user'] 60 | group node['github_connector']['group'] 61 | mode 0600 62 | end 63 | 64 | file ::File.join(ssh_dir, "#{repo_name}_id_rsa.pub") do 65 | content "#{public_key.ssh_type} #{[public_key.to_blob].pack('m0')}\n" 66 | owner node['github_connector']['user'] 67 | group node['github_connector']['group'] 68 | mode 0644 69 | end 70 | 71 | file ::File.join(ssh_dir, "#{repo_name}_ssh_wrapper.sh") do 72 | content "#!/bin/sh -e\nexec ssh -i #{::File.join(ssh_dir, "#{repo_name}_id_rsa")} $@\n" 73 | owner node['github_connector']['user'] 74 | group node['github_connector']['group'] 75 | mode 0755 76 | end 77 | end 78 | end 79 | 80 | if node['github_connector']['github_host_key'] 81 | ssh_known_hosts_entry 'github.com' do 82 | key node['github_connector']['github_host_key'] 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /cookbook/recipes/upstart.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: github_connector 3 | # Recipe:: upstart 4 | # 5 | # Copyright (C) 2014 Brandon Turner 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | directory "#{node['github_connector']['install_dir']}/tmp/sockets" do 21 | owner node['github_connector']['user'] 22 | group node['github_connector']['group'] 23 | mode 0755 24 | end 25 | 26 | template "/etc/init/github-connector-web.conf" do 27 | source 'upstart-github-connector-web.conf.erb' 28 | mode 0644 29 | owner 'root' 30 | group 'root' 31 | action :create 32 | variables( 33 | :home_path => "/home/#{node['github_connector']['user']}", 34 | :rvm_path => "/home/#{node['github_connector']['user']}/.rvm" 35 | ) 36 | end 37 | 38 | template "/etc/init/github-connector-worker.conf" do 39 | source 'upstart-github-connector-worker.conf.erb' 40 | mode 0644 41 | owner 'root' 42 | group 'root' 43 | action :create 44 | variables( 45 | :home_path => "/home/#{node['github_connector']['user']}", 46 | :rvm_path => "/home/#{node['github_connector']['user']}/.rvm" 47 | ) 48 | end 49 | 50 | service 'github-connector-web' do 51 | provider Chef::Provider::Service::Upstart 52 | supports :status => true, :restart => true, :reload => true 53 | action :start 54 | end 55 | 56 | service 'github-connector-worker' do 57 | provider Chef::Provider::Service::Upstart 58 | supports :status => true, :restart => true, :reload => false 59 | action :start 60 | end 61 | -------------------------------------------------------------------------------- /cookbook/recipes/user.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: github_connector 3 | # Recipe:: user 4 | # 5 | # Copyright (C) 2014 Brandon Turner 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | group node['github_connector']['group'] 21 | 22 | user node['github_connector']['user'] do 23 | gid node['github_connector']['group'] 24 | shell '/bin/bash' 25 | home "/home/#{node['github_connector']['user']}" 26 | supports :manage_home => true 27 | end 28 | -------------------------------------------------------------------------------- /cookbook/templates/default/database.yml.erb: -------------------------------------------------------------------------------- 1 | # THIS FILE IS MANAGED BY CHEF 2 | # Local modifications will be discarded. 3 | 4 | development: &default 5 | adapter: postgresql 6 | host: <%= node['github_connector']['db']['host'] %> 7 | port: <%= node['github_connector']['db']['port'] %> 8 | database: <%= node['github_connector']['db']['name'] %> 9 | user: <%= node['github_connector']['db']['user'] %> 10 | password: "<%= GithubConnector::Helpers.database_password(node) %>" 11 | pool: 25 12 | timeout: 5 13 | 14 | production: 15 | <<: *default 16 | 17 | # Warning: The database defined as "test" will be erased and 18 | # re-generated from your development database when you run "rake". 19 | # Do not set this db to the same as development or production. 20 | test: 21 | <<: *default 22 | database: github_connector_test 23 | -------------------------------------------------------------------------------- /cookbook/templates/default/nginx-github-connector.conf.erb: -------------------------------------------------------------------------------- 1 | # THIS FILE IS MANAGED BY CHEF 2 | # Local modifications will be discarded. 3 | 4 | upstream github_connector_server { 5 | server unix://<%= @install_dir %>/tmp/sockets/puma.sock fail_timeout=0; 6 | } 7 | 8 | <% if @redirect_http %> 9 | server { 10 | listen <%= @listen_port %>; 11 | server_name <%= @host_name %> <%= @host_aliases.join(' ') %>; 12 | rewrite ^(.*) https://$host<%= ":#{@ssl_listen_port}" unless @ssl_listen_port == 443 %>$1 permanent; 13 | } 14 | <% end -%> 15 | 16 | server { 17 | <% if @ssl_enabled -%> 18 | listen <%= @ssl_listen_port %>; 19 | <% else -%> 20 | listen <%= @listen_port %>; 21 | <% end -%> 22 | 23 | server_name <%= @host_name %> <%= @host_aliases.join(' ') %>; 24 | root <%= @install_dir %>/public; 25 | 26 | keepalive_timeout 5s; 27 | 28 | <% if @ssl_enabled -%> 29 | ssl on; 30 | ssl_certificate /etc/ssl/certs/<%= @host_name %>.crt; 31 | ssl_certificate_key /etc/ssl/private/<%= @host_name %>.key; 32 | 33 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 34 | ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; 35 | ssl_prefer_server_ciphers on; 36 | 37 | ssl_session_cache shared:SSL:10m; 38 | ssl_session_timeout 10m; 39 | <% end -%> 40 | 41 | try_files $uri/index.html $uri.html $uri @app; 42 | 43 | location @app { 44 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 45 | proxy_set_header X-Forwarded-Proto $scheme; 46 | proxy_set_header Host $http_host; 47 | proxy_redirect off; 48 | proxy_pass http://github_connector_server; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cookbook/templates/default/secrets.yml.erb: -------------------------------------------------------------------------------- 1 | # THIS FILE IS MANAGED BY CHEF 2 | # Local modifications will be discarded. 3 | 4 | # Be sure to restart your server when you modify this file. 5 | # 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | # 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: &default 14 | # Your secret key is used for verifying the integrity of signed cookies. 15 | # If you change this key, all old signed cookies will become invalid! 16 | secret_key_base: <%= GithubConnector::Helpers.secret_key_base(node) %> 17 | 18 | # The secret key used for encrypting sensitive database info. 19 | # If you change this key, all user OAuth tokens will become unreadable! 20 | database_key: <%= GithubConnector::Helpers.database_key(node) %> 21 | 22 | production: 23 | <<: *default 24 | 25 | test: 26 | <<: *default 27 | -------------------------------------------------------------------------------- /cookbook/templates/default/upstart-github-connector-web.conf.erb: -------------------------------------------------------------------------------- 1 | # THIS FILE IS MANAGED BY CHEF 2 | # Local modifications will be discarded. 3 | 4 | description "GitHub Connector" 5 | 6 | start on runlevel [2] 7 | stop on runlevel [016] 8 | 9 | setuid <%= node['github_connector']['user'] %> 10 | chdir <%= node['github_connector']['install_dir'] %> 11 | exec <%= @rvm_path %>/bin/rvm <%= node['github_connector']['rvm_alias'] %> do puma -e production -b unix://<%= node['github_connector']['install_dir'] %>/tmp/sockets/puma.sock 12 | reload signal SIGUSR2 13 | 14 | respawn 15 | -------------------------------------------------------------------------------- /cookbook/templates/default/upstart-github-connector-worker.conf.erb: -------------------------------------------------------------------------------- 1 | # THIS FILE IS MANAGED BY CHEF 2 | # Local modifications will be discarded. 3 | 4 | description "GitHub Connector Worker" 5 | 6 | start on runlevel [2] 7 | stop on runlevel [016] 8 | 9 | setuid <%= node['github_connector']['user'] %> 10 | chdir <%= node['github_connector']['install_dir'] %> 11 | exec env HOME=<%= @home_path %> RAILS_ENV=production <%= @rvm_path %>/bin/rvm <%= node['github_connector']['rvm_alias'] %> do <%= node['github_connector']['install_dir'] %>/bin/delayed_job run 12 | 13 | respawn 14 | -------------------------------------------------------------------------------- /cookbook/test_data_bags/github_connector/secrets.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "secrets", 3 | "database_password": "badpass_db", 4 | "secret_key_base": "badkey_secret", 5 | "database_key": "badkey_db" 6 | } 7 | -------------------------------------------------------------------------------- /cookbook/test_data_bags/github_connector/ssh.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "ssh", 3 | "private_key": "-----BEGIN RSA PRIVATE KEY-----\nTHIS_IS_A_FAKE_KEY\n-----END RSA PRIVATE KEY-----\n" 4 | } 5 | -------------------------------------------------------------------------------- /cookbook/test_data_bags/github_connector/ssl_cert.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "ssl_cert", 3 | "cert": "-----BEGIN CERTIFICATE-----\nMIIEMzCCAxugAwIBAgIJAK/3uISa+d4LMA0GCSqGSIb3DQEBBQUAMG4xCzAJBgNV\nBAYTAlVTMQ4wDAYDVQQIEwVUZXhhczEPMA0GA1UEBxMGQXVzdGluMSowKAYDVQQK\nEyFHaXRodWIgQWN0aXZlIERpcmVjdG9yeSBDb25uZWN0b3IxEjAQBgNVBAMTCWxv\nY2FsaG9zdDAeFw0xNDEwMDYwMjI3MjFaFw0xNTEwMDYwMjI3MjFaMG4xCzAJBgNV\nBAYTAlVTMQ4wDAYDVQQIEwVUZXhhczEPMA0GA1UEBxMGQXVzdGluMSowKAYDVQQK\nEyFHaXRodWIgQWN0aXZlIERpcmVjdG9yeSBDb25uZWN0b3IxEjAQBgNVBAMTCWxv\nY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALQRWnGOJWvY\nQwXO7gBZ7BVH8WOh4Vdvsy+Smw7kjukf3ZeXJrCs2B6cQYLKkDbelX/C0YIGp3w2\nGTFCY1mgsmpZI+svzMrZ8hHi00sEUkXekRxWgT/Bbo2AirP/Fz/r0d8fYle5D+0n\nriBQP3il6ZkYUAlJ0tDlsqCv2oEXxb7bIH88/lwwRXkxidr0GBdTG7HWGGABzG+B\n775DVL5RYMkOLJa7sKP3PGVK1nKIubaVWiQ+jhdiaHOPk2VymWhw3r3tV9YExxYP\n3vwcPS/Of/5EGo/WJ/a5MW57uTb90rWamBDMLMjBJPUXKLiDpNAdW0GFKw+PdWGX\n8/wyqMNrDpUCAwEAAaOB0zCB0DAdBgNVHQ4EFgQURShade6A1YmMODPSNViGEoty\nnGwwgaAGA1UdIwSBmDCBlYAURShade6A1YmMODPSNViGEotynGyhcqRwMG4xCzAJ\nBgNVBAYTAlVTMQ4wDAYDVQQIEwVUZXhhczEPMA0GA1UEBxMGQXVzdGluMSowKAYD\nVQQKEyFHaXRodWIgQWN0aXZlIERpcmVjdG9yeSBDb25uZWN0b3IxEjAQBgNVBAMT\nCWxvY2FsaG9zdIIJAK/3uISa+d4LMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF\nBQADggEBALEAKoLnLNcm4+oEMs97LV9FspaqJMbZSJbzQaPUN6DlQF9o09HgaEgU\nKI4GGuPUEt+wU9OQBDhWmbkJgyqPl3L6Mq8YazadPvIPwVEzTGcUWfeBDdrcmH5T\nNTimbVQgwliArbpXI/kgWJw4G7e3wn5ZptdFr3YscdwE1ki4vIYgXlIBKqXgW+Wh\nlF/T+s1cRPmoX24M1G5A3wgngLGshIVvv+Xr//keLvWpmS5z3uXUzBH3HeLwxrjO\nHzrjOnwi6OvSoBqMZjvLLv/yP9g0hh9DmiqKh0QmB90Vlp/bx5Dv1g5Fup3alNdp\nxjsyvSde1fKm3xSpFh+bXwiYvLHh8LY=\n-----END CERTIFICATE-----\n", 4 | "key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAtBFacY4la9hDBc7uAFnsFUfxY6HhV2+zL5KbDuSO6R/dl5cm\nsKzYHpxBgsqQNt6Vf8LRgganfDYZMUJjWaCyalkj6y/MytnyEeLTSwRSRd6RHFaB\nP8FujYCKs/8XP+vR3x9iV7kP7SeuIFA/eKXpmRhQCUnS0OWyoK/agRfFvtsgfzz+\nXDBFeTGJ2vQYF1MbsdYYYAHMb4HvvkNUvlFgyQ4slruwo/c8ZUrWcoi5tpVaJD6O\nF2Joc4+TZXKZaHDeve1X1gTHFg/e/Bw9L85//kQaj9Yn9rkxbnu5Nv3StZqYEMws\nyMEk9RcouIOk0B1bQYUrD491YZfz/DKow2sOlQIDAQABAoIBABWj0D69Wnnvb36P\nM8MPC3QzRSs4FSCw59Pbxo6voQ0bK0JAhAHPg9mJ5cWWGma9sTG9c/gwXIhs5/In\njFEFIuvs8ogdIntuXc0QeVwWlNyYts+1Batnz6VpwUGIcn7YFEzANM1eDC/wCNkR\nS89wAPbJGTVEjfVU5XayK4xAEx+weh704U75pGJqI+N/Cd4aBIA8rLQ8S5cRLRRs\nx6HUvuysK1A92RTUS3Cvu8UR8lJYzmkgGGf72iZjtqx7IqKSeKLNNE+t1d4Vtigi\nhC8egVlRxFnG3rdrssUqT1+7i7T6RAOCiD8WK1U+fjSmhz6/n88heZ5XpKeUdkiQ\ny1xJsLECgYEA6GP1haXQvg/FIl7HBJw4BJgHUrlMEkwyfvFOF1UYacjHN7uNXV1B\nsSe+gm/MI2uiiNayjJChy7r3pSfckNdauI+0zxsysj6dSXp6B0/1YiKAAJXm3X5i\nx1F+mxWWaMUlyW7YEBO0I4j0bcS0knO6NffWs61OtqTw8OI4uqzYIHsCgYEAxlyU\nTUrBMPMoVNb2KlOVvXJwnEbbLVom3wBiMNg+RVqqzwIl1NB7Bz45n6dHuSCXhbOH\nA/5871aAXQHZq4zj2+aFyr+xAdypcU1Yez/Xio6Q0jkEd6QS/tKVvPFVXCQCGfvX\nYv4rvZLSI4MxG0paF2qp5DZlkm1Oios/4XEHyC8CgYEA1IeWY0PiQ+/oOiaznGPC\nV3EyQVV1XMaS58WHxY7tZNFaYH4GKvy+t2XBtUjJSRuG6d5wLF2ZmtjC4ygxb8WE\nEoZatY4KLzlUX37DWyylHbqvldmB6c9MRz0grHRxuh+TD0VwFEPw2w7FfB4JhmaQ\nRgsDMA+vjRoLwEEj4JVyk0ECgYEAnPcdk5wYDDgeLiR8XzoNQACTA9c+EUFJiSWw\njZ5QiGkayPyWGzVuZWjkCGZC50fXH0HVEWAMVQhKQ073hDzVAmoEbVALLcIDg1kF\nL2JxmX7/MptT4ajAL01MmFsQhP0pfI5A/mDLFBRenSNvdHz9lZIeJiy1a417nT5b\nqnXbBpkCgYBAKidtSdQ2a97HuO1Jctw4B1xOFlJwp36Bi3b3PN14l/hfnbVNg0KS\nols8DZZ7nh7hRjcds5w4174YUYkLfQZxfI5Wi48BoRdB+ruZR4DlGL+tWll28L/z\nQTUsuIdsNVvrv524BSW5lYv9ocpCjw23eGuLAkFnea7Thztg/Ez8jg==\n-----END RSA PRIVATE KEY-----\n" 5 | } 6 | -------------------------------------------------------------------------------- /cortex.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | info: 3 | title: Github Connector 4 | description: The GitHub Active Directory Connector allows managing GitHub organizations 5 | with Active Directory. 6 | x-cortex-git: 7 | github: 8 | alias: r7org 9 | repository: rapid7/github-connector 10 | x-cortex-tag: github-connector 11 | x-cortex-type: service 12 | x-cortex-domain-parents: 13 | - tag: pd-services 14 | openapi: 3.0.1 15 | servers: 16 | - url: "/" 17 | -------------------------------------------------------------------------------- /db/migrate/20140619160007_devise_create_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseCreateUsers < ActiveRecord::Migration 2 | def change 3 | create_table(:users) do |t| 4 | ## LDAP authenticatable 5 | t.string :username, null: false, default: "" 6 | t.string :email 7 | t.string :name 8 | 9 | ## Rememberable 10 | t.datetime :remember_created_at 11 | 12 | ## Trackable 13 | t.integer :sign_in_count, default: 0, null: false 14 | t.datetime :current_sign_in_at 15 | t.datetime :last_sign_in_at 16 | t.string :current_sign_in_ip 17 | t.string :last_sign_in_ip 18 | 19 | ## Confirmable 20 | # t.string :confirmation_token 21 | # t.datetime :confirmed_at 22 | # t.datetime :confirmation_sent_at 23 | # t.string :unconfirmed_email # Only if using reconfirmable 24 | 25 | ## Lockable 26 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 27 | # t.string :unlock_token # Only if unlock strategy is :email or :both 28 | # t.datetime :locked_at 29 | 30 | 31 | t.timestamps 32 | end 33 | 34 | add_index :users, :username, unique: true 35 | # add_index :users, :confirmation_token, unique: true 36 | # add_index :users, :unlock_token, unique: true 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /db/migrate/20140624041139_add_github_attrs_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddGithubAttrsToUser < ActiveRecord::Migration 2 | def change 3 | add_column :users, :encrypted_github_token, :string 4 | add_column :users, :github_login, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20140626181353_create_settings.rb: -------------------------------------------------------------------------------- 1 | class CreateSettings < ActiveRecord::Migration 2 | def change 3 | create_table :settings do |t| 4 | t.string :key 5 | t.string :value 6 | 7 | t.timestamps 8 | end 9 | add_index :settings, :key, unique: true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20140708224056_create_emails.rb: -------------------------------------------------------------------------------- 1 | class CreateEmails < ActiveRecord::Migration 2 | def change 3 | create_table :emails do |t| 4 | t.references :user, index: true 5 | t.string :address 6 | t.string :source 7 | 8 | t.timestamps 9 | end 10 | add_index :emails, :source 11 | 12 | reversible do |dir| 13 | dir.up do 14 | execute "INSERT INTO emails (user_id, address, source, created_at, updated_at) SELECT id, email, 'ldap', NOW(), NOW() FROM users WHERE email IS NOT NULL" 15 | remove_column :users, :email 16 | end 17 | dir.down do 18 | add_column :users, :email, :string 19 | execute "UPDATE users AS u SET email=emails.address, updated_at=NOW() FROM users INNER JOIN emails ON users.id=emails.user_id" 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /db/migrate/20140709045852_add_last_sync_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddLastSyncToUser < ActiveRecord::Migration 2 | def change 3 | add_column :users, :last_ldap_sync, :datetime 4 | add_column :users, :last_github_sync, :datetime 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20140709191104_add_state_attrs_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddStateAttrsToUser < ActiveRecord::Migration 2 | def change 3 | add_column :users, :state, :string, null: false, default: :unknown 4 | add_column :users, :ldap_account_control, :integer 5 | add_column :users, :github_mfa, :boolean 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20140714210644_add_sync_errors_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddSyncErrorsToUser < ActiveRecord::Migration 2 | def change 3 | add_column :users, :github_sync_error, :string 4 | add_column :users, :github_sync_error_at, :datetime 5 | add_column :users, :ldap_sync_error, :string 6 | add_column :users, :ldap_sync_error_at, :datetime 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20140722192112_add_github_teams.rb: -------------------------------------------------------------------------------- 1 | class AddGithubTeams < ActiveRecord::Migration 2 | def change 3 | create_table(:teams) do |t| 4 | t.string :slug 5 | t.string :organization 6 | t.string :name 7 | t.timestamps 8 | end 9 | 10 | create_table :user_teams, id: false do |t| 11 | t.belongs_to :user 12 | t.belongs_to :team 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20140726214806_move_state_to_github_user.rb: -------------------------------------------------------------------------------- 1 | class MoveStateToGithubUser < ActiveRecord::Migration 2 | def change 3 | add_column :github_users, :state, :string, null: false, default: :unknown 4 | remove_column :users, :state, :string, null: false, default: :unknown 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20140811194159_add_github_urls.rb: -------------------------------------------------------------------------------- 1 | class AddGithubUrls < ActiveRecord::Migration 2 | def change 3 | add_column :github_users, :avatar_url, :string 4 | add_column :github_users, :html_url, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20140818012538_add_admin_flag_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddAdminFlagToUser < ActiveRecord::Migration 2 | def change 3 | add_column :users, :admin, :bool 4 | 5 | reversible do |dir| 6 | dir.up do 7 | execute <<-SQL 8 | UPDATE users SET admin='t' 9 | WHERE users.id IN (SELECT id FROM users ORDER BY created_at LIMIT 1) 10 | SQL 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20140915164525_convert_settings_value_to_text.rb: -------------------------------------------------------------------------------- 1 | class ConvertSettingsValueToText < ActiveRecord::Migration 2 | def change 3 | change_column :settings, :value, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20140917184213_create_delayed_jobs.rb: -------------------------------------------------------------------------------- 1 | class CreateDelayedJobs < ActiveRecord::Migration 2 | def self.up 3 | create_table :delayed_jobs, :force => true do |table| 4 | table.integer :priority, :default => 0, :null => false # Allows some jobs to jump to the front of the queue 5 | table.integer :attempts, :default => 0, :null => false # Provides for retries, but still fail eventually. 6 | table.text :handler, :null => false # YAML-encoded string of the object that will do work 7 | table.text :last_error # reason for last failure (See Note below) 8 | table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. 9 | table.datetime :locked_at # Set when a client is working on this object 10 | table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) 11 | table.string :locked_by # Who is working on this object (if locked) 12 | table.string :queue # The name of the queue this job is in 13 | table.timestamps 14 | end 15 | 16 | add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority' 17 | end 18 | 19 | def self.down 20 | drop_table :delayed_jobs 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /db/migrate/20140917184236_add_connect_github_user_statuses.rb: -------------------------------------------------------------------------------- 1 | class AddConnectGithubUserStatuses < ActiveRecord::Migration 2 | def change 3 | create_table(:connect_github_user_statuses) do |t| 4 | t.belongs_to :user 5 | t.belongs_to :github_user 6 | t.string :oauth_code 7 | t.string :status 8 | t.string :step 9 | t.text :error_message 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20140920200517_add_remember_token_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddRememberTokenToUser < ActiveRecord::Migration 2 | def change 3 | add_column :users, :remember_token, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20141018212156_add_github_user_disabled_teams.rb: -------------------------------------------------------------------------------- 1 | class AddGithubUserDisabledTeams < ActiveRecord::Migration 2 | def change 3 | create_table :github_user_disabled_teams, id: false do |t| 4 | t.belongs_to :github_user 5 | t.belongs_to :github_team 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20160215025445_add_github_organization_memberships.rb: -------------------------------------------------------------------------------- 1 | class AddGithubOrganizationMemberships < ActiveRecord::Migration 2 | def change 3 | create_table :github_organization_memberships do |t| 4 | t.references :github_user, index: true, null: false 5 | t.string :organization, null: false 6 | t.string :role 7 | t.string :state 8 | 9 | t.timestamps null: false 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20210311145806_add_user_department.rb: -------------------------------------------------------------------------------- 1 | class AddUserDepartment < ActiveRecord::Migration 2 | def change 3 | add_column :users, :department, :string 4 | end 5 | end -------------------------------------------------------------------------------- /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: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /ldap/README.md: -------------------------------------------------------------------------------- 1 | Development LDAP Server 2 | ======================= 3 | 4 | The code in this directory uses OpenLDAP to emulate the Active 5 | Directory records needed for this application to work. This is 6 | helpful in development and testing if you do not want to connect 7 | to a real Active Directory server. 8 | 9 | ## Install prerequisites 10 | 11 | ### Ubuntu 12 | 13 | Install OpenLDAP's slapd: 14 | 15 | sudo apt-get install slapd ldap-utils 16 | 17 | You may also need to put apparmor into complain mode: 18 | 19 | sudo apt-get install apparmor-utils 20 | sudo aa-complain /usr/sbin/slapd 21 | 22 | ### OSX 23 | 24 | OpenLDAP is installed on OSX by default. There is nothing else 25 | you need to do. 26 | 27 | ## Run test server 28 | 29 | To run the server: 30 | 31 | ./run-server 32 | 33 | ## Accounts 34 | 35 | Several accounts are available: 36 | 37 | * hsimpson - Normal account 38 | * msimpson - Locked account 39 | * bsimpson - Disabled account 40 | * lsimpson - Password expired 41 | 42 | All accounts use password 123456. 43 | -------------------------------------------------------------------------------- /ldap/base.ldif: -------------------------------------------------------------------------------- 1 | 2 | dn: dc=example,dc=com 3 | objectClass: dcObject 4 | objectClass: organizationalUnit 5 | dc: example 6 | ou: example 7 | 8 | # Normal account 9 | dn: cn=Homer Simpson,dc=example,dc=com 10 | objectclass: top 11 | objectClass: person 12 | objectClass: organizationalPerson 13 | objectClass: user 14 | displayName: Homer Simpson 15 | name: Homer Simpson 16 | givenName: Homer 17 | sn: Simpson 18 | mail: Homer_Simpson@example.com 19 | userPrincipalName: hsimpson@example.com 20 | userAccountControl: 512 21 | sAMAccountName: hsimpson 22 | # userPassword: 123456 23 | userPassword: {SSHA}1j5ho2mHI6fHgwQOjBk9aRHF47FzYWx0 24 | 25 | # Locked account 26 | dn: cn=Marge Simpson,dc=example,dc=com 27 | objectclass: top 28 | objectClass: person 29 | objectClass: organizationalPerson 30 | objectClass: user 31 | displayName: Homer Simpson 32 | name: Marge Simpson 33 | givenName: Marge 34 | sn: Simpson 35 | mail: Marge_Simpson@example.com 36 | userPrincipalName: msimpson@example.com 37 | userAccountControl: 528 38 | sAMAccountName: msimpson 39 | # userPassword: 123456 40 | userPassword: {SSHA}1j5ho2mHI6fHgwQOjBk9aRHF47FzYWx0 41 | 42 | # Disabled account 43 | dn: cn=Bart Simpson,dc=example,dc=com 44 | objectclass: top 45 | objectClass: person 46 | objectClass: organizationalPerson 47 | objectClass: user 48 | displayName: Homer Simpson 49 | name: Bart Simpson 50 | givenName: Bart 51 | sn: Simpson 52 | mail: Bart_Simpson@example.com 53 | userPrincipalName: bsimpson@example.com 54 | userAccountControl: 514 55 | sAMAccountName: bsimpson 56 | # userPassword: 123456 57 | userPassword: {SSHA}1j5ho2mHI6fHgwQOjBk9aRHF47FzYWx0 58 | 59 | # Password expired 60 | dn: cn=Lisa Simpson,dc=example,dc=com 61 | objectclass: top 62 | objectClass: person 63 | objectClass: organizationalPerson 64 | objectClass: user 65 | displayName: Homer Simpson 66 | name: Lisa Simpson 67 | givenName: Lisa 68 | sn: Simpson 69 | mail: Lisa_Simpson@example.com 70 | userPrincipalName: lsimpson@example.com 71 | userAccountControl: 8389120 72 | sAMAccountName: lsimpson 73 | # userPassword: 123456 74 | userPassword: {SSHA}1j5ho2mHI6fHgwQOjBk9aRHF47FzYWx0 75 | -------------------------------------------------------------------------------- /ldap/clear.ldif: -------------------------------------------------------------------------------- 1 | dn: cn=Lisa Simpson,dc=example,dc=com 2 | changetype: delete 3 | 4 | dn: cn=Bart Simpson,dc=example,dc=com 5 | changetype: delete 6 | 7 | dn: cn=Marge Simpson,dc=example,dc=com 8 | changetype: delete 9 | 10 | dn: cn=Homer Simpson,dc=example,dc=com 11 | changetype: delete 12 | 13 | dn: dc=example,dc=com 14 | changetype: delete 15 | -------------------------------------------------------------------------------- /ldap/local.schema: -------------------------------------------------------------------------------- 1 | 2 | attributetype ( 1.2.840.113556.1.4.656 3 | NAME 'userPrincipalName' 4 | EQUALITY caseIgnoreMatch 5 | SUBSTR caseIgnoreSubstringsMatch 6 | SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' 7 | SINGLE-VALUE ) 8 | 9 | attributetype ( 1.2.840.113556.1.4.221 10 | NAME 'sAMAccountName' 11 | EQUALITY caseIgnoreMatch 12 | SUBSTR caseIgnoreSubstringsMatch 13 | SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' 14 | SINGLE-VALUE ) 15 | 16 | attributetype ( 1.2.840.113556.1.4.8 17 | NAME 'userAccountControl' 18 | EQUALITY integerMatch 19 | SYNTAX '1.3.6.1.4.1.1466.115.121.1.27' 20 | SINGLE-VALUE ) 21 | 22 | objectclass ( 1.2.840.113556.1.5.9 23 | NAME 'user' 24 | SUP organizationalPerson 25 | STRUCTURAL 26 | MUST ( sAMAccountName $ userAccountControl $ userPrincipalName ) 27 | MAY ( displayName $ givenName $ mail $ name ) ) 28 | -------------------------------------------------------------------------------- /ldap/run-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'erb' 4 | require 'fileutils' 5 | require 'open3' 6 | 7 | FileUtils.chdir(__dir__) 8 | 9 | ## For OSX: 10 | ENV['PATH'] = "#{ENV['PATH']}:/usr/libexec" 11 | FileUtils.mkdir_p(File.join(__dir__, 'openldap-data', 'run')) 12 | 13 | template = File.read('slapd-test.conf.erb') 14 | normal_out = 'slapd-test.conf' 15 | 16 | File.open(normal_out, 'w') do |f| 17 | f.write ERB.new(template).result(binding) 18 | end 19 | 20 | cmd = "slapd -d 32768 -f #{normal_out} -h ldap://localhost:3268" 21 | 22 | started = false 23 | @slap_pid = nil 24 | slap_thread = Thread.new do 25 | Thread.current.abort_on_exception = true 26 | Open3.popen2(cmd) do |stdin, stdout, wait_thr| 27 | @slap_pid = wait_thr.pid 28 | stdin.close 29 | begin 30 | while data = stdout.readpartial(1024) 31 | print data 32 | end 33 | rescue EOFError 34 | # Ignore EOF 35 | end 36 | stdout.close 37 | exit_status = wait_thr.value 38 | end 39 | end 40 | 41 | def kill_slapd 42 | if @slap_pid 43 | Process.kill('TERM', @slap_pid) rescue nil 44 | end 45 | end 46 | 47 | Signal.trap('INT') { kill_slapd } 48 | Signal.trap('TERM') { kill_slapd } 49 | 50 | begin 51 | # TODO: Better test for slapd started 52 | sleep 0.5 53 | 54 | if slap_thread.alive? 55 | ldap_connect_string = "-x -H ldap://localhost:3268 -D 'cn=admin,dc=example,dc=com' -w secret" 56 | system("ldapmodify #{ldap_connect_string} -f clear.ldif") 57 | system("ldapadd #{ldap_connect_string} -f base.ldif") 58 | end 59 | 60 | slap_thread.join 61 | ensure 62 | kill_slapd 63 | end 64 | -------------------------------------------------------------------------------- /ldap/slapd-test.conf.erb: -------------------------------------------------------------------------------- 1 | # 2 | # See slapd.conf(5) for details on configuration options. 3 | # This file should NOT be world readable. 4 | # 5 | <% ldapdir = RUBY_PLATFORM.match(/linux/) ? 'ldap' : 'openldap' %> 6 | include /etc/<%= ldapdir %>/schema/core.schema 7 | include /etc/<%= ldapdir %>/schema/cosine.schema 8 | include /etc/<%= ldapdir %>/schema/inetorgperson.schema 9 | #include /etc/<%= ldapdir %>/schema/nis.schema 10 | #include /etc/<%= ldapdir %>/schema/microsoft.std.schema 11 | #include /etc/<%= ldapdir %>/schema/microsoft.schema 12 | 13 | ## Local definitions 14 | include <%= File.expand_path('local.schema', @conf_root) %> 15 | 16 | # Allow LDAPv2 client connections. This is NOT the default. 17 | allow bind_v2 18 | 19 | # Do not enable referrals until AFTER you have a working directory 20 | # service AND an understanding of referrals. 21 | #referral ldap://root.openldap.org 22 | 23 | pidfile <%= File.expand_path('openldap-data/run/slapd.pid', @conf_root) %> 24 | argsfile <%= File.expand_path('openldap-data/run/slapd.args', @conf_root) %> 25 | 26 | # Load dynamic backend modules: 27 | modulepath /usr/lib/openldap 28 | 29 | access to * 30 | by self write 31 | by * read 32 | by anonymous auth 33 | 34 | ####################################################################### 35 | # ldbm and/or bdb database definitions 36 | ####################################################################### 37 | 38 | database ldif 39 | 40 | suffix "dc=example,dc=com" 41 | directory openldap-data 42 | rootdn "cn=admin,dc=example,dc=com" 43 | ## rootpw = secret 44 | rootpw {SSHA}fFjKcZb4cfOAcwSjJer8nCGOEVRUnwCC 45 | -------------------------------------------------------------------------------- /lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/github-connector/0de9978ad30d02cea43f986d248c39967d453896/lib/assets/.keep -------------------------------------------------------------------------------- /lib/base_executor.rb: -------------------------------------------------------------------------------- 1 | class BaseExecutor 2 | 3 | # A list of errors that occurred while running this synchronizer 4 | # @return [Array] 5 | attr_reader :errors 6 | 7 | # The number of threads to use when running. 8 | # @return [Fixnum] 9 | attr_accessor :thread_count 10 | 11 | # Runs the executor. 12 | # 13 | # @return [BaseExecutor] instance of the executor that ran the task 14 | def self.run! 15 | new.tap { |instance| instance.run! } 16 | end 17 | 18 | def initialize 19 | @thread_count = [5, ActiveRecord::Base.connection_pool.size - 1].min 20 | @semaphore = Mutex.new 21 | @errors = [] 22 | end 23 | 24 | private 25 | 26 | # Obtains a lock, runs the block, and releases the lock when the block completes. 27 | # 28 | # @see `Mutex#synchronize` 29 | def synchronize(&block) 30 | @semaphore.synchronize(&block) 31 | end 32 | 33 | # Runs the given block for each object in parallel. Up to 34 | # {#thread_count} threads are used to run the block. This waits 35 | # for all threads to complete before returning. 36 | # 37 | # @param objs [Enumerable] an array of objects 38 | # @yieldparam obj [Object] a single object from the array of objects 39 | # @return [void] 40 | def thread_for_each(objs) 41 | ary = objs.to_a 42 | ary = ary.dup if ary === objs 43 | 44 | threads = [] 45 | [ary.size, thread_count].min.times do 46 | threads << Thread.new do |thread| 47 | ActiveRecord::Base.connection_pool.with_connection do 48 | while (obj = synchronize { ary.shift }) 49 | yield obj 50 | end 51 | end 52 | end 53 | end 54 | threads.each { |t| t.join } 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/github_connector/navbar.rb: -------------------------------------------------------------------------------- 1 | module GithubConnector 2 | class Navbar 3 | include Rails.application.routes.url_helpers 4 | 5 | def sections 6 | { 7 | connect: { 8 | title: 'Add Account', 9 | url: connect_path, 10 | }, 11 | } 12 | end 13 | 14 | def admin_sections 15 | { 16 | users: { 17 | title: 'Users', 18 | url: users_path, 19 | }, 20 | github_users: { 21 | title: 'GitHub Users', 22 | url: github_users_path, 23 | }, 24 | } 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/ldap_synchronizer.rb: -------------------------------------------------------------------------------- 1 | class LdapSynchronizer < BaseExecutor 2 | 3 | # A hash of statistics from the most recent run. Includes: 4 | # * users_time - User sync execution time 5 | # * users_synced 6 | # * users_errors 7 | # 8 | # @return [Hash] 9 | attr_reader :stats 10 | 11 | # @return [Enumerable] a list of users to synchronize 12 | attr_reader :users 13 | 14 | # @param users [Enumerable] a list of users to synchronize 15 | def initialize(users=User.all) 16 | super() 17 | @stats = {} 18 | @users = users 19 | end 20 | 21 | # Synchronizes all Active Directory users. 22 | # 23 | # @return [Bolean] 24 | def sync_users 25 | start = Time.now 26 | stats[:users_synced] = 0 27 | stats[:user_errors] = 0 28 | threads = [] 29 | 30 | thread_for_each(users) do |user| 31 | begin 32 | user.sync_from_ldap! 33 | synchronize do 34 | if user.ldap_sync_error 35 | stats[:user_errors] += 1 36 | @errors << "Error synchronizing #{user.username}: #{user.ldap_sync_error}" 37 | else 38 | stats[:users_synced] += 1 39 | end 40 | end 41 | rescue => e 42 | synchronize do 43 | stats[:user_errors] += 1 44 | @errors << e 45 | Rails.logger.error "Error processing user #{user.username}: #{e.message}" 46 | end 47 | end 48 | end 49 | 50 | stats[:user_errors] == 0 51 | rescue => e 52 | stats[:user_errors] += 1 53 | @errors << e 54 | Rails.logger.error "Error synchronizing users: #{e.message}" 55 | false 56 | ensure 57 | stats[:users_time] = Time.now.to_f - start.to_f 58 | end 59 | 60 | # Synchronizes Active Directory users with our local database. 61 | # Synchronization is run in threads according to {#thread_count}. 62 | # 63 | # @return [Boolean] `true` if synchronizer executed successfully, `false` otherwise. 64 | def run! 65 | @errors = [] 66 | @stats = {} 67 | 68 | Rails.application.settings.with_disconnected do |settings| 69 | settings.reload 70 | 71 | sync_users 72 | end 73 | 74 | @errors.empty? 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/rules.rb: -------------------------------------------------------------------------------- 1 | module Rules 2 | # @return [Array] an array of enabled {Rules::Base} classes 3 | def self.enabled_rules 4 | @all_rules ||= begin 5 | Dir[File.join(File.dirname(__FILE__), 'rules', '*.rb')].map do |file| 6 | filename = File.basename(file, '.rb') 7 | next if filename == 'base' 8 | Rules.const_get(filename.classify, false) rescue nil 9 | end.compact 10 | end 11 | @all_rules.select { |rule| rule.enabled? } 12 | end 13 | 14 | # @param user [GithubUser] 15 | # @return [Rules::Iterator] an array of {Rules::Base}s for the given user 16 | def self.for_github_user(user) 17 | rules = enabled_rules.map { |klass| klass.new(user) } 18 | Iterator.new(rules) 19 | end 20 | 21 | ## 22 | # An `Enumerable` wrapper around rules. It allows filtering 23 | # and provides summary methods. Assign a `Proc` to 24 | # {Iterator#selectors} to filter rules. 25 | class Iterator 26 | include ::Enumerable 27 | 28 | # @return [Array] an array of rules 29 | attr_reader :rules 30 | 31 | # @return [Proc] callbacks to filter rules 32 | attr_accessor :selectors 33 | 34 | def initialize(rules) 35 | @rules = rules 36 | @selectors = [] 37 | end 38 | 39 | def initialize_copy(other) 40 | super 41 | @selectors = other.selectors.dup 42 | end 43 | 44 | # Calls the given block once for each element in `self`, passing that 45 | # element as a parameter. Elements are filtered by {#selectors} if 46 | # set. 47 | # 48 | # @yieldparam element [Rules:Base] a rule 49 | # @return [void] 50 | def each(&block) 51 | rules.each do |rule| 52 | next unless selectors.all? { |selector| selector.call(rule) } 53 | block.call(rule) 54 | end 55 | end 56 | 57 | # Returns `true` if `self` contains no elements 58 | # @return [Boolean] 59 | def empty? 60 | !any? { true } 61 | end 62 | 63 | # Includes only failing rules 64 | # 65 | # @return [Iterator] self 66 | def failing 67 | self.selectors << lambda { |rule| !rule.valid? } 68 | self 69 | end 70 | 71 | # Includes only rules required for external access 72 | # 73 | # @return [Iterator] self 74 | def external 75 | self.selectors << lambda { |rule| rule.required_for_external? } 76 | self 77 | end 78 | 79 | # Includes only passing rules 80 | # 81 | # @return [Iterator] self 82 | def passing 83 | self.selectors << lambda { |rule| rule.valid? } 84 | self 85 | end 86 | 87 | # Returns the result of all rules in `self` 88 | # @return [Boolean] `true` if all rules are valid, `false` otherwise 89 | def result 90 | all?(&:result) 91 | end 92 | 93 | # Returns `true` if all rules in `self` are valid 94 | # @return [Boolean] 95 | def valid? 96 | !!result 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/rules/active_ldap.rb: -------------------------------------------------------------------------------- 1 | module Rules 2 | ## 3 | # Tests that the Active Directory account is active. The userAccountControl 4 | # LDAP attribute is used to check for disabled users (disabled flag: 0x0002). 5 | class ActiveLdap < Base 6 | 7 | # A descriptive error message to display when this rule 8 | # fails. 9 | # 10 | # @return [String] 11 | def error_msg 12 | return nil if result 13 | 14 | if user && has_flag?(User::AccountControl::ACCOUNT_DISABLED) 15 | "Active Directory account is disabled" 16 | #elsif user && has_flag?(User::AccountControl::PASSWORD_EXPIRED) 17 | # "Active Directory password is expired" 18 | else 19 | "Active Directory account does not meet criteria" 20 | end 21 | end 22 | 23 | # Should this rule notify the user when it is not valid? 24 | # @return [Boolean] 25 | def notify? 26 | false 27 | end 28 | 29 | # This rule is required for external users. 30 | # 31 | # @return [Boolean] false 32 | def required_for_external? 33 | false 34 | end 35 | 36 | # The result of applying this rule to the {User}. 37 | # @return [Boolean] `true` if the rule passes, false otherwise 38 | def result 39 | return false unless user 40 | return false if has_flag?(User::AccountControl::ACCOUNT_DISABLED) 41 | #return false if has_flag?(User::AccountControl::PASSWORD_EXPIRED) 42 | true 43 | end 44 | 45 | private 46 | 47 | def has_flag?(flag) 48 | user.ldap_account_control & flag == flag 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/rules/base.rb: -------------------------------------------------------------------------------- 1 | module Rules 2 | class Base 3 | # @return [GithubUser] 4 | attr_reader :github_user 5 | 6 | # Returns true if this rule is enabled. 7 | # 8 | # @return [Boolean] 9 | def self.enabled? 10 | true 11 | end 12 | 13 | def initialize(github_user) 14 | @github_user = github_user 15 | end 16 | 17 | # A descriptive error message to display when this rule 18 | # fails. 19 | # 20 | # @return [String] 21 | def error_msg 22 | name 23 | end 24 | 25 | # A name for this rule. 26 | # 27 | # @return [String] 28 | def name 29 | self.class.name.demodulize.underscore 30 | end 31 | 32 | # Should this rule notify the user when it is not valid? 33 | # @return [Boolean] 34 | def notify? 35 | true 36 | end 37 | 38 | # This rule is required for external users. 39 | # 40 | # @return [Boolean] 41 | def required_for_external? 42 | true 43 | end 44 | 45 | # The result of applying this rule to the {GithubUser}. 46 | # @return [Boolean] `true` if the rule passes, false otherwise 47 | def result 48 | raise NotImplementedError, "You must implement #{self.class.name}#result" 49 | end 50 | 51 | # Application settings 52 | # @return [GithubConnector::Settings] 53 | def settings 54 | self.class.settings 55 | end 56 | 57 | # Application settings 58 | # @return [GithubConnector::Settings] 59 | def self.settings 60 | Rails.application.settings 61 | end 62 | 63 | # The {User} associated with the {GithubUser} 64 | # @return [User] 65 | def user 66 | github_user.user 67 | end 68 | 69 | # Returns `true` if the result of the rule is `true` 70 | # @return [Boolean] 71 | def valid? 72 | !!result 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/rules/email.rb: -------------------------------------------------------------------------------- 1 | module Rules 2 | ## 3 | # Tests that all GitHub email addresses match the 4 | # {GithubConnector::Settings#rule_email_regex} setting. If 5 | # no `rule_email_regex` setting exists, this rule always 6 | # returns `true`. 7 | class Email < Base 8 | 9 | # Returns true if this rule is enabled. 10 | # 11 | # @return [Boolean] 12 | def self.enabled? 13 | !!settings.rule_email_regex 14 | end 15 | 16 | # A descriptive error message to display when this rule 17 | # fails. 18 | # 19 | # @return [String] 20 | def error_msg 21 | return nil if result 22 | 23 | bad_emails = email_addresses.reject { |email| regex.match(email) } 24 | 25 | "#{bad_emails.count == 1 ? 'Email does' : 'Emails do'} not meet criteria: #{bad_emails.join(', ')}" 26 | end 27 | 28 | # This rule is required for external users. 29 | # 30 | # @return [Boolean] false 31 | def required_for_external? 32 | false 33 | end 34 | 35 | # The result of applying this rule to the {GithubUser}. 36 | # @return [Boolean] `true` if the rule passes, false otherwise 37 | def result 38 | email_addresses.all? { |email| regex.match(email) } 39 | end 40 | 41 | private 42 | 43 | def email_addresses 44 | github_user.emails.map { |email| email.address.downcase } 45 | end 46 | 47 | def regex 48 | @regex ||= Regexp.new(settings.rule_email_regex) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/rules/github_mfa.rb: -------------------------------------------------------------------------------- 1 | module Rules 2 | ## 3 | # Tests that the {GithubUser} has GitHub multi-factor authentication 4 | # enabled. 5 | class GithubMfa < Base 6 | 7 | # A descriptive error message to display when this rule 8 | # fails. 9 | # 10 | # @return [String] 11 | def error_msg 12 | return nil if result 13 | 14 | "Two factor authentication is disabled" 15 | end 16 | 17 | # The result of applying this rule to the {GithubUser}. 18 | # @return [Boolean] `true` if the rule passes, false otherwise 19 | def result 20 | !!github_user.mfa 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/rules/github_oauth.rb: -------------------------------------------------------------------------------- 1 | module Rules 2 | ## 3 | # Tests that a {GithubUser} has valid GitHub OAuth access. This is 4 | # evaluated by looking at the {GithubUser#sync_error} field for 5 | # `notoken` or `unauthorized` errors. 6 | class GithubOauth < Base 7 | 8 | # A descriptive error message to display when this rule 9 | # fails. 10 | # 11 | # @return [String] 12 | def error_msg 13 | return nil if result 14 | 15 | if github_user.token 16 | "Invalid OAuth token" 17 | else 18 | "Missing OAuth token" 19 | end 20 | end 21 | 22 | # This rule is required for external users. 23 | # 24 | # @return [Boolean] false 25 | def required_for_external? 26 | false 27 | end 28 | 29 | # The result of applying this rule to the {GithubUser}. 30 | # @return [Boolean] `true` if the rule passes, false otherwise 31 | def result 32 | return false unless github_user.token 33 | return false if %w(notoken unauthorized).include?(github_user.sync_error) 34 | true 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/rules/last_github_sync.rb: -------------------------------------------------------------------------------- 1 | module Rules 2 | ## 3 | # Tests that a {GithubUser} has synced with GitHub 4 | # within a certain amount of time specified by the 5 | # {GithubConnector::Settings#rule_max_sync_age} setting. If no 6 | # `rule_max_sync_age` setting exists, this rule always returns `true`. 7 | class LastGithubSync < Base 8 | 9 | # Returns true if this rule is enabled. 10 | # 11 | # @return [Boolean] 12 | def self.enabled? 13 | !!settings.rule_max_sync_age 14 | end 15 | 16 | # A descriptive error message to display when this rule 17 | # fails. 18 | # 19 | # @return [String] 20 | def error_msg 21 | return nil if result 22 | 23 | if !github_user.last_sync_at 24 | "GitHub has never been synchronized" 25 | else 26 | "Last GitHub synchronization is too old" 27 | end 28 | end 29 | 30 | # The result of applying this rule to the {GithubUser}. 31 | # @return [Boolean] `true` if the rule passes, false otherwise 32 | def result 33 | return false unless github_user.last_sync_at 34 | 35 | min_sync_time = Time.now - settings.rule_max_sync_age 36 | return false unless github_user.last_sync_at > min_sync_time 37 | 38 | true 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/rules/last_ldap_sync.rb: -------------------------------------------------------------------------------- 1 | module Rules 2 | ## 3 | # Tests that a {GithubUser} has synced with Active Directory 4 | # within a certain amount of time specified by the 5 | # {GithubConnector::Settings#rule_max_sync_age} setting. If no 6 | # `rule_max_sync_age` setting exists, this rule always returns `true`. 7 | class LastLdapSync < Base 8 | 9 | # Returns true if this rule is enabled. 10 | # 11 | # @return [Boolean] 12 | def self.enabled? 13 | !!settings.rule_max_sync_age 14 | end 15 | 16 | # A descriptive error message to display when this rule 17 | # fails. 18 | # 19 | # @return [String] 20 | def error_msg 21 | return nil if result 22 | 23 | if !user 24 | "No active directory user" 25 | elsif !user.last_ldap_sync 26 | "Active Directory has never been synchronized" 27 | else 28 | "Last Active Directory synchornization is too old" 29 | end 30 | end 31 | 32 | # This rule is required for external users. 33 | # 34 | # @return [Boolean] false 35 | def required_for_external? 36 | false 37 | end 38 | 39 | # The result of applying this rule to the {GithubUser}. 40 | # @return [Boolean] `true` if the rule passes, false otherwise 41 | def result 42 | return false unless user 43 | return false unless user.last_ldap_sync 44 | 45 | min_sync_time = Time.now - settings.rule_max_sync_age 46 | return false unless user.last_ldap_sync > min_sync_time 47 | 48 | true 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/settings/definition.rb: -------------------------------------------------------------------------------- 1 | module Settings 2 | ## 3 | # Defines a setting used in {Settings::Base}. 4 | class Definition 5 | # The setting name 6 | # @return [Symbol] 7 | attr_accessor :key 8 | 9 | # The setting type 10 | # @return [Symbol] one of `:string`, `:integer`, `:float`, `:boolean`, `:datetime`, `:array`, `:hash` 11 | attr_accessor :type 12 | 13 | # Whether the value should be encrypted in the database 14 | # @return [Boolean] true if the value should be encrypted, false otherwise 15 | attr_accessor :encrypt 16 | 17 | def initialize(key, opts) 18 | self.key = key.to_sym 19 | self.type = :string 20 | self.encrypt = false 21 | opts.each do |opt, val| 22 | send("#{opt}=", val) if respond_to?("#{opt}=") 23 | end 24 | end 25 | 26 | # Casts the given value for persistence in the database. 27 | # 28 | # @param [Object] val 29 | # @return [String] a string for persisting in the database 30 | def db_cast(val) 31 | return nil if val.nil? 32 | 33 | val = case type 34 | when :boolean then val ? 'true' : 'false' 35 | when :array, :hash then val ? val.to_json : nil 36 | else val.to_s 37 | end 38 | 39 | val 40 | end 41 | 42 | # Should the setting be encrypted when persisting? 43 | # @return [Boolean] true if the value should be encrypted, false otherwise 44 | def encrypt? 45 | !!@encrypt 46 | end 47 | 48 | # Casts the given value according to the `type` setting option. 49 | # 50 | # @param [Object] val 51 | # @return [Object] the value, cast according to the `type` option 52 | def type_cast(val) 53 | return nil if val.nil? 54 | 55 | case type 56 | when :integer then val.to_i rescue val ? 1 : 0 57 | when :float then val.to_f 58 | when :boolean then val.to_s =~ /^(t|1|y)/i ? true : false 59 | when :datetime then DateTime.parse(val.to_s) 60 | when :array then val.is_a?(Array) ? val : JSON.parse(val) 61 | when :hash then val.is_a?(Hash) ? val : JSON.parse(val) 62 | else val 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/github-connector/0de9978ad30d02cea43f986d248c39967d453896/lib/tasks/.keep -------------------------------------------------------------------------------- /lib/tasks/github.rake: -------------------------------------------------------------------------------- 1 | namespace :github do 2 | 3 | task sync: 'sync:github' 4 | 5 | desc "Transitions Github users based on rules and current attributes" 6 | task transition_users: %w(environment sync:ldap sync:github) do 7 | 8 | # Make sure the majority of users have recently synced ldap and github 9 | min_sync_time = Time.now - [Rails.application.settings.rule_max_sync_age, 120].max + 120.seconds 10 | 11 | total_ldap_count = User.count 12 | synced_ldap_count = User.where('last_ldap_sync > ?', min_sync_time).count 13 | if synced_ldap_count < [total_ldap_count / 4, 1].max 14 | puts "Fewer than 25% of LDAP users (#{synced_ldap_count} of #{total_ldap_count}) meet minimum sync time. Skipping transition." 15 | exit 1 16 | end 17 | 18 | total_github_count = GithubUser.active.count 19 | synced_github_count = GithubUser.active.where('last_sync_at > ?', min_sync_time).count 20 | if synced_github_count < [total_github_count / 4, 1].max 21 | puts "Fewer than 25% of GitHub users (#{synced_github_count} of #{total_github_count}) meet minimum sync time. Skipping transition." 22 | exit 2 23 | end 24 | 25 | puts "Checking for users to disable..." 26 | executor = TransitionGithubUsers.new 27 | executor.run! 28 | 29 | disabled_users = executor.transitions.select { |u| u.disabled? } 30 | external_users = executor.transitions.select { |u| u.external? } 31 | 32 | executor.stats.each do |key, val| 33 | puts " #{key}: #{val}" 34 | end 35 | 36 | if disabled_users.empty? && external_users.empty? 37 | puts " No users to disable." 38 | end 39 | unless disabled_users.empty? 40 | puts " Disabled Github users: #{disabled_users.map { |u| u.login }.join(', ')}" 41 | end 42 | unless external_users.empty? 43 | puts " External Github users: #{external_users.map { |u| u.login }.join(', ')}" 44 | end 45 | 46 | unless executor.errors.empty? 47 | puts " Errors:" 48 | executor.errors.each do |error| 49 | puts " #{error}" 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/tasks/sync.rake: -------------------------------------------------------------------------------- 1 | desc "Synchronize LDAP and GitHub users" 2 | task sync: ['sync:ldap', 'sync:github'] 3 | 4 | namespace :sync do 5 | desc "Synchronize Github users and teams" 6 | task github: :environment do 7 | puts "Synchronizing Github..." 8 | sync = GithubSynchronizer.new 9 | sync.run! 10 | sync.stats.each do |key, val| 11 | puts " #{key}: #{val}" 12 | end 13 | unless sync.errors.empty? 14 | puts " Errors:" 15 | sync.errors.each do |error| 16 | puts " #{error}" 17 | end 18 | end 19 | end 20 | 21 | desc "Synchronize Active Directory users" 22 | task ldap: :environment do 23 | puts "Synchronizing Active Directory..." 24 | sync = LdapSynchronizer.new 25 | sync.run! 26 | sync.stats.each do |key, val| 27 | puts " #{key}: #{val}" 28 | end 29 | unless sync.errors.empty? 30 | puts " Errors:" 31 | sync.errors.each do |error| 32 | puts " #{error}" 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/github-connector/0de9978ad30d02cea43f986d248c39967d453896/log/.keep -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The page you were looking for doesn't exist.

    62 |

    You may have mistyped the address or the page may have moved.

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    The change you wanted was rejected.

    62 |

    Maybe you tried to change something you didn't have access to.

    63 |
    64 |

    If you are the application owner check the logs for more information.

    65 |
    66 | 67 | 68 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
    60 |
    61 |

    We're sorry, but something went wrong.

    62 |
    63 |

    If you are the application owner check the logs for more information.

    64 |
    65 | 66 | 67 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /spec/controllers/dashboard_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe DashboardController, :type => :controller do 4 | before do 5 | sign_in 6 | configured 7 | end 8 | 9 | describe "GET 'index'" do 10 | it "returns http success" do 11 | get 'index' 12 | expect(response).to be_success 13 | end 14 | 15 | it 'redirects to setup wizard if application is not configured' do 16 | Rails.application.settings.configured = false 17 | get 'index' 18 | expect(response).to redirect_to(setup_url) 19 | end 20 | 21 | it 'returns a http error if an LDAP authentication error occurs' do 22 | allow(controller).to receive(:index).and_raise(DeviseLdapAuthenticatable::LdapException) 23 | get 'index' 24 | expect(response).to be_error 25 | end 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /spec/controllers/github_users_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe GithubUsersController, :type => :controller do 4 | before do 5 | sign_in(user) 6 | configured 7 | end 8 | 9 | let(:user) { create(:admin_user) } 10 | let(:github_user) { create(:github_user) } 11 | 12 | describe "GET index" do 13 | it "returns http success" do 14 | get :index 15 | expect(response).to be_success 16 | end 17 | end 18 | 19 | describe "GET show" do 20 | it "returns http success" do 21 | get :show, id: github_user.login 22 | expect(response).to be_success 23 | end 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /spec/controllers/setup/admin_user_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Setup::AdminUserController, :type => :controller do 4 | 5 | let(:user) { create(:user) } 6 | 7 | before do 8 | @request.env["devise.mapping"] = Devise.mappings[:user] 9 | end 10 | 11 | describe "GET 'new'" do 12 | it "returns http success" do 13 | get 'new' 14 | expect(response).to be_success 15 | end 16 | 17 | it 'signs out existing users' do 18 | sign_in user 19 | get 'new' 20 | expect(controller).to_not be_signed_in 21 | end 22 | end 23 | 24 | describe "POST 'create'" do 25 | subject { post 'create', user: {username: user.username, password: 'foopass'} } 26 | 27 | it 'sets the admin user' do 28 | allow(controller.warden).to receive(:authenticate!).and_return(user) 29 | expect(subject).to be_redirect 30 | expect(user).to be_an_admin 31 | end 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /spec/controllers/setup/company_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Setup::CompanyController, :type => :controller do 4 | 5 | describe "GET 'edit'" do 6 | it "returns http success" do 7 | get 'edit' 8 | expect(response).to be_success 9 | end 10 | end 11 | 12 | describe "PUT 'update'" do 13 | subject { put 'update', settings: {company: 'foocompany'} } 14 | 15 | it 'saves settings' do 16 | subject 17 | expect(Rails.application.settings.company).to eq('foocompany') 18 | end 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /spec/controllers/setup/email_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Setup::EmailController, :type => :controller do 4 | 5 | describe "GET 'edit'" do 6 | it "returns http success" do 7 | get 'edit' 8 | expect(response).to be_success 9 | end 10 | 11 | it 'sets default email from company name' do 12 | allow(request).to receive(:host).and_return('localhost') 13 | Rails.application.settings.company = 'Example Corp' 14 | get 'edit' 15 | expect(assigns(:settings).email_from).to eq('github@example_corp.com') 16 | end 17 | 18 | it 'sets default email from url domain' do 19 | allow(request).to receive(:host).and_return('foocorp.com') 20 | get 'edit' 21 | expect(assigns(:settings).email_from).to eq('github@foocorp.com') 22 | end 23 | end 24 | 25 | describe "PUT 'update'" do 26 | subject { put 'update', settings: {smtp_address: 'localhost'} } 27 | 28 | it 'saves settings' do 29 | subject 30 | expect(Rails.application.settings.smtp_address).to eq('localhost') 31 | end 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /spec/controllers/setup/github_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Setup::GithubController, :type => :controller do 4 | 5 | describe "GET 'edit'" do 6 | it "returns http success" do 7 | get 'edit' 8 | expect(response).to be_success 9 | end 10 | 11 | it 'sets default orgs' do 12 | Rails.application.settings.company = 'Example' 13 | get 'edit' 14 | expect(assigns(:settings).github_orgs).to eq(['example']) 15 | end 16 | 17 | it 'sets default teams' do 18 | Rails.application.settings.company = 'Example' 19 | get 'edit' 20 | expect(assigns(:settings).github_default_teams).to eq(['example-employees']) 21 | end 22 | end 23 | 24 | describe "PUT 'update'" do 25 | let(:settings) { {github_orgs: 'foocompany'} } 26 | subject { put 'update', settings: settings } 27 | 28 | it 'saves settings' do 29 | subject 30 | expect(Rails.application.settings.github_orgs).to eq(['foocompany']) 31 | end 32 | 33 | context 'with connect_github parameter' do 34 | it 'calls github_admin action' do 35 | expect(controller).to receive(:github_admin) { controller.redirect_to('foobar') } 36 | put 'update', settings: settings, connect_github: 'connect' 37 | end 38 | end 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /spec/controllers/setup/ldap_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Setup::LdapController, :type => :controller do 4 | 5 | describe "GET 'edit'" do 6 | it "returns http success" do 7 | get 'edit' 8 | expect(response).to be_success 9 | end 10 | 11 | it 'redirects to settings if application is already configured' do 12 | Rails.application.settings.configured = true 13 | get 'edit' 14 | expect(response).to redirect_to(controller: '/settings', action: :edit) 15 | end 16 | 17 | it 'sets development defaults for localhost' do 18 | allow(request).to receive(:host).and_return('localhost') 19 | get 'edit' 20 | expect(assigns(:settings).ldap_base).to eq('dc=example,dc=com') 21 | end 22 | end 23 | 24 | describe "PUT 'update'" do 25 | subject { put 'update', settings: {ldap_host: 'foohost', ldap_port: 3389} } 26 | let(:ldap) { double('ldap', bind: true).as_null_object } 27 | 28 | before do 29 | allow(Net::LDAP).to receive(:new).and_return(ldap) 30 | end 31 | 32 | it 'saves settings' do 33 | subject 34 | expect(Rails.application.settings.ldap_host).to eq('foohost') 35 | end 36 | 37 | it 'tests ldap connection before saving' do 38 | expect(ldap).to receive(:bind).and_return(false) 39 | expect(subject).to_not be_redirect 40 | expect(assigns(:error)).to_not be_nil 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/controllers/setup/rules_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Setup::RulesController, :type => :controller do 4 | 5 | describe "GET 'edit'" do 6 | it "returns http success" do 7 | get 'edit' 8 | expect(response).to be_success 9 | end 10 | end 11 | 12 | describe "PUT 'update'" do 13 | subject { put 'update', settings: {rule_max_sync_age: 60} } 14 | 15 | it 'saves settings' do 16 | subject 17 | expect(Rails.application.settings.rule_max_sync_age).to eq(60) 18 | end 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /spec/controllers/users_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe UsersController, :type => :controller do 4 | before do 5 | sign_in(user) 6 | configured 7 | end 8 | 9 | let(:user) { create(:admin_user, name: 'Admin User') } 10 | 11 | describe "GET 'index'" do 12 | it 'returns http success' do 13 | get 'index' 14 | expect(response).to be_success 15 | end 16 | 17 | it 'loads users in order' do 18 | get 'index' 19 | create(:user, name: 'Aaron Sorts First') 20 | names = assigns(:users).map { |user| user.name } 21 | expect(names).to eq(['Aaron Sorts First', 'Admin User']) 22 | end 23 | end 24 | 25 | describe "GET 'show'" do 26 | it "returns http success" do 27 | get 'show', id: user.username 28 | expect(response).to be_success 29 | end 30 | 31 | context 'with admin user' do 32 | it 'shows other users' do 33 | create(:user, username: 'otheruser', name: 'Other User') 34 | get 'show', id: 'otheruser' 35 | expect(response).to be_success 36 | expect(assigns(:user).username).to eq('otheruser') 37 | end 38 | end 39 | 40 | context 'with non-admin user' do 41 | let(:user) { create(:user, name: 'Regular User') } 42 | 43 | it 'shows own user' do 44 | get 'show', id: user.username 45 | expect(response).to be_success 46 | end 47 | 48 | it 'does not show other users' do 49 | create(:user, username: 'otheruser', name: 'Other User') 50 | get 'show', id: 'otheruser' 51 | expect(response).to be_forbidden 52 | end 53 | end 54 | end 55 | 56 | describe "GET 'edit'" do 57 | it "returns http success" do 58 | get 'edit', id: user.username 59 | expect(response).to be_success 60 | end 61 | end 62 | 63 | describe "PATCH 'edit'" do 64 | it "redirects after save" do 65 | patch 'update', id: user.username, user: {admin: 0} 66 | expect(response).to be_redirect 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/factories/github_email.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :github_email do 3 | sequence(:address) { |n| "githubemail#{n}@example.com" } 4 | github_user 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/github_organization_membership.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :github_organization_membership do 3 | sequence(:organization) { |n| "org#{n}" } 4 | github_user 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/factories/github_team.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :github_team do 3 | sequence(:slug) { |n| "githubteam#{n}" } 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/factories/github_user.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :github_user do 3 | sequence(:login) { |n| "githubber#{n}" } 4 | 5 | factory :github_user_with_emails do 6 | transient do 7 | emails_count 2 8 | end 9 | 10 | after(:create) do |github_user, evaluator| 11 | create_list(:github_email, evaluator.emails_count, github_user: github_user) 12 | end 13 | end 14 | 15 | factory :github_user_with_user do 16 | user 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/factories/user.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :user do 3 | sequence(:username) { |n| "fakeuser#{n}" } 4 | 5 | factory :user_with_github_users do 6 | transient do 7 | github_users_count 2 8 | end 9 | end 10 | 11 | factory :admin_user do 12 | admin true 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/helpers/application_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe ApplicationHelper do 4 | describe '#format_time' do 5 | it 'adds data-time attribute' do 6 | html = format_time(Time.now) 7 | expect(html).to include('data-time') 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/helpers/github_users_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe GithubUsersHelper do 4 | describe '#github_user_state_label' do 5 | let(:github_user) { build(:github_user) } 6 | 7 | it 'adds danger label for disabled users' do 8 | github_user.state = 'disabled' 9 | html = github_user_state_label(github_user) 10 | expect(html).to include('label-danger') 11 | end 12 | 13 | it 'adds info label for external users' do 14 | github_user.state = 'external' 15 | html = github_user_state_label(github_user) 16 | expect(html).to include('label-info') 17 | end 18 | 19 | it 'adds info label for excluded users' do 20 | github_user.state = 'excluded' 21 | html = github_user_state_label(github_user) 22 | expect(html).to include('label-info') 23 | end 24 | 25 | it 'adds warning label for unknown users' do 26 | github_user.state = 'unknown' 27 | html = github_user_state_label(github_user) 28 | expect(html).to include('label-warning') 29 | end 30 | 31 | it 'adds success label for enabled users' do 32 | github_user.state = 'enabled' 33 | html = github_user_state_label(github_user) 34 | expect(html).to include('label-success') 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/jobs/connect_github_user_job_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe ConnectGithubUserJob do 4 | subject(:job) { ConnectGithubUserJob.new } 5 | subject(:status) { ConnectGithubUserStatus.new(step: :grant) } 6 | 7 | let(:oauth) { double('oauth', auth_code: double(get_token: oauth_token)) } 8 | let(:oauth_token) { double('oauth_token', token: 'footoken') } 9 | let(:octokit) { double('octokit', user: ghuser) } 10 | let(:ghuser) { double(id: 1337, login: 'githubuser') } 11 | let(:github_user) { build(:github_user, id: 1337) } 12 | 13 | before do 14 | allow(job).to receive(:oauth_client).and_return(oauth) 15 | allow(GithubUser).to receive(:find_or_initialize_by).and_return(github_user) 16 | allow(github_user).to receive(:sync!) { github_user.save! } 17 | allow(github_user).to receive(:add_to_organizations).and_return(true) 18 | allow(github_user).to receive(:do_enable) 19 | allow(github_user).to receive(:do_disable) 20 | allow(github_user).to receive(:do_notify_disabled) 21 | allow(Octokit::Client).to receive(:new).and_return(octokit) 22 | end 23 | 24 | it 'validates OAuth code' do 25 | expect(job).to receive(:oauth_process_auth_code).and_return(github_user) 26 | job.perform(status) 27 | expect(status.status).to eq(:complete) 28 | end 29 | 30 | it 'adds user to organzations' do 31 | expect(github_user).to receive(:add_to_organizations).and_return(true) 32 | job.perform(status) 33 | expect(status.status).to eq(:complete) 34 | end 35 | 36 | it 'enables the user' do 37 | expect(github_user).to receive(:enable) 38 | job.perform(status) 39 | expect(status.status).to eq(:complete) 40 | end 41 | 42 | it 'stores error if OAuth fails' do 43 | oauth_response = double.as_null_object 44 | expect(job).to receive(:oauth_process_auth_code).and_raise(OAuth2::Error.new(oauth_response)) 45 | job.perform(status) 46 | expect(status.status).to eq(:error) 47 | end 48 | 49 | it 'stores error if add_to_organizations fails' do 50 | expect(github_user).to receive(:add_to_organizations).and_return(false) 51 | job.perform(status) 52 | expect(status.status).to eq(:error) 53 | end 54 | 55 | it 'stores error if unexpected error occurs' do 56 | allow(github_user).to receive(:add_to_organizations).and_raise('fooerror') 57 | job.perform(status) 58 | expect(status.status).to eq(:error) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/lib/github_connector/settings_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe GithubConnector::Settings do 4 | 5 | subject(:settings) { GithubConnector::Settings.new } 6 | 7 | describe '#apply_to_action_mailer' do 8 | it 'applies config to ActionMailer::Base' do 9 | settings.smtp_address = 'foohost' 10 | settings.email_base_url = 'https://localhost:443/' 11 | settings.email_from = 'github@fooemail' 12 | settings.email_reply_to = '' 13 | settings.apply_to_action_mailer 14 | expect(ActionMailer::Base.smtp_settings[:address]).to eq('foohost') 15 | expect(ActionMailer::Base.default_url_options).to eq({host: 'localhost', protocol: 'https'}) 16 | expect(ActionMailer::Base.default[:from]).to eq('github@fooemail') 17 | expect(ActionMailer::Base.default.keys).to_not include(:reply_to) 18 | end 19 | end 20 | 21 | describe '#email_keys' do 22 | it 'returns a list of email keys' do 23 | expect(settings.email_keys).to eq(%i(email_base_url email_from email_reply_to)) 24 | end 25 | end 26 | 27 | describe '#email_config' do 28 | before do 29 | Setting.create(key: :email_from, value: 'fooemail@example.com') 30 | end 31 | 32 | it 'returns hash with email_ key prefixes removed' do 33 | config = settings.email_config 34 | expect(config).to have_key(:from) 35 | expect(config).to_not have_key(:email_from) 36 | end 37 | end 38 | 39 | describe '#github_admin_oauth_scope' do 40 | it 'includes the user scope' do 41 | expect(settings).to receive(:github_user_oauth_scope).and_return('foouser:fooscope') 42 | expect(settings.github_admin_oauth_scope).to include('foouser:fooscope') 43 | end 44 | 45 | it 'includes admin:org' do 46 | expect(settings.github_admin_oauth_scope).to include('admin:org') 47 | end 48 | end 49 | 50 | describe '#github_user_oauth_scope' do 51 | it 'includes required scopes' do 52 | expect(settings.github_user_oauth_scope).to include('user:email') 53 | expect(settings.github_user_oauth_scope).to include('read:public_key') 54 | expect(settings.github_user_oauth_scope).to include('write:org') 55 | end 56 | end 57 | 58 | describe '#ldap_keys' do 59 | it 'returns a list of ldap keys' do 60 | expect(settings.ldap_keys).to eq(%i(ldap_host ldap_port ldap_ssl ldap_admin_user ldap_admin_password ldap_attribute ldap_base)) 61 | end 62 | end 63 | 64 | describe '#ldap_config' do 65 | before do 66 | Setting.create(key: :ldap_host, value: 'localhost') 67 | end 68 | 69 | it 'returns hash with ldap_ key prefixes removed' do 70 | config = settings.ldap_config 71 | expect(config).to have_key('host') 72 | expect(config).to_not have_key('ldap_host') 73 | end 74 | end 75 | 76 | describe '#smtp_keys' do 77 | it 'returns a list of smtp keys' do 78 | expect(settings.smtp_keys).to eq(%i(smtp_address smtp_port smtp_enable_starttls_auto smtp_user_name smtp_password smtp_authentication smtp_domain)) 79 | end 80 | end 81 | 82 | describe '#smtp_config' do 83 | before do 84 | Setting.create(key: :smtp_address, value: 'localhost') 85 | end 86 | 87 | it 'returns hash with smtp_ key prefixes removed' do 88 | config = settings.smtp_config 89 | expect(config).to have_key(:address) 90 | expect(config).to_not have_key(:smtp_address) 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /spec/lib/ldap_synchronizer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe LdapSynchronizer do 4 | 5 | subject(:synchronizer) { LdapSynchronizer.new([user]) } 6 | let(:user) { build(:user) } 7 | 8 | before do 9 | allow(user).to receive(:sync_from_ldap).and_return(true) 10 | end 11 | 12 | describe '#sync_users' do 13 | it 'calls sync_from_ldap' do 14 | expect(user).to receive(:sync_from_ldap).and_return(true) 15 | expect(synchronizer.sync_users).to eq(true) 16 | end 17 | 18 | it 'continues if errors occur' do 19 | allow(synchronizer).to receive(:users).and_raise('foo error') 20 | expect(synchronizer.sync_users).to eq(false) 21 | expect(synchronizer.errors).to be_a(Array) 22 | expect(synchronizer.errors).to_not be_empty 23 | end 24 | 25 | it 'continues if errors occur in threads' do 26 | allow(user).to receive(:sync_from_ldap).and_raise('foo error') 27 | expect(synchronizer.sync_users).to eq(false) 28 | expect(synchronizer.errors).to be_a(Array) 29 | expect(synchronizer.errors).to_not be_empty 30 | end 31 | 32 | it 'counts sync errors as errors' do 33 | allow(user).to receive(:ldap_sync_error).and_return('foo error') 34 | expect(synchronizer.sync_users).to eq(false) 35 | expect(synchronizer.errors).to be_a(Array) 36 | expect(synchronizer.errors.first).to include('foo error') 37 | end 38 | end 39 | 40 | describe '#run!' do 41 | it 'synchronizes users' do 42 | expect(synchronizer).to receive(:sync_users) 43 | synchronizer.run! 44 | end 45 | 46 | it 'returns true if successful' do 47 | expect(synchronizer.run!).to eq(true) 48 | end 49 | 50 | it 'returns false if errors occurred' do 51 | allow(user).to receive(:sync_from_ldap).and_raise("foo error") 52 | expect(synchronizer.run!).to eq(false) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/lib/rules/active_ldap_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe Rules::ActiveLdap do 4 | subject(:rule) { Rules::ActiveLdap.new(github_user) } 5 | let(:user) { github_user.user } 6 | let(:github_user) { build(:github_user_with_user) } 7 | let(:settings) { double } 8 | 9 | before do 10 | allow(described_class).to receive(:settings).and_return(settings) 11 | end 12 | 13 | it 'is valid for a normal account' do 14 | user.ldap_account_control = 512 15 | expect(rule).to be_valid 16 | end 17 | 18 | it 'is not valid when account is disabled' do 19 | user.ldap_account_control = 514 20 | expect(rule).to_not be_valid 21 | end 22 | 23 | it 'is not valid without a User' do 24 | github_user.user = nil 25 | expect(rule).to_not be_valid 26 | end 27 | 28 | it 'does not notify' do 29 | expect(rule).to_not be_notify 30 | end 31 | 32 | it 'is not required for external users' do 33 | expect(rule).to_not be_required_for_external 34 | end 35 | 36 | describe '#error_msg' do 37 | it 'returns a generic error message' do 38 | github_user.user = nil 39 | expect(rule.error_msg).to be_a(String) 40 | expect(rule.error_msg).to include('criteria') 41 | end 42 | 43 | it 'returns an account disabled error message' do 44 | user.ldap_account_control = User::AccountControl::ACCOUNT_DISABLED 45 | expect(rule.error_msg).to include('disabled') 46 | end 47 | 48 | #it 'returns a password expired error message' do 49 | # user.ldap_account_control = User::AccountControl::PASSWORD_EXPIRED 50 | # expect(rule.error_msg).to include('password') 51 | #end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/lib/rules/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe Rules::Base do 4 | class TestRule < Rules::Base 5 | end 6 | 7 | subject(:rule) { TestRule.new(user) } 8 | let(:user) { double } 9 | 10 | it 'does not implement #result' do 11 | expect { rule.result }.to raise_error(NotImplementedError) 12 | end 13 | 14 | it 'notifies by default' do 15 | expect(rule.notify?).to eq(true) 16 | end 17 | 18 | it 'is required for external users by default' do 19 | expect(rule).to be_required_for_external 20 | end 21 | 22 | it 'converts class name to a rule name' do 23 | expect(rule.name).to eq('test_rule') 24 | end 25 | 26 | it 'references the application settings singleton' do 27 | expect(Rails.application).to receive(:settings).and_call_original 28 | expect(rule.settings).to be_a(GithubConnector::Settings) 29 | end 30 | 31 | it 'returns an error message' do 32 | expect(rule.error_msg).to be_a(String) 33 | expect(rule.error_msg).to_not be_empty 34 | end 35 | 36 | it 'is enabled by default' do 37 | expect(TestRule).to be_enabled 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/lib/rules/email_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe Rules::Email do 4 | subject(:rule) { Rules::Email.new(github_user) } 5 | let(:github_user) { create(:github_user_with_emails, user: user) } 6 | let(:user) { create(:user) } 7 | let(:settings) { double(rule_email_regex: regex) } 8 | 9 | before do 10 | allow(described_class).to receive(:settings).and_return(settings) 11 | end 12 | 13 | context 'with email regex' do 14 | let(:regex) { '@example\.com$' } 15 | 16 | it 'is enabled' do 17 | expect(described_class).to be_enabled 18 | end 19 | 20 | it 'is valid when regex matches' do 21 | expect(rule).to be_valid 22 | end 23 | 24 | it "is not valid when regex doesn't match" do 25 | github_email = github_user.emails.last 26 | github_email.address = 'bsimpson@example.org' 27 | github_email.save 28 | expect(rule).to_not be_valid 29 | end 30 | 31 | it 'does not check ldap address' do 32 | user.email = 'bsimpson@example.org' 33 | expect(rule).to be_valid 34 | end 35 | 36 | it 'is not required for external users' do 37 | expect(rule).to_not be_required_for_external 38 | end 39 | 40 | it 'returns an error message' do 41 | github_email = github_user.emails.last 42 | github_email.address = 'bsimpson@example.org' 43 | github_email.save 44 | expect(rule.error_msg).to be_a(String) 45 | expect(rule.error_msg).to include('bsimpson@example.org') 46 | end 47 | end 48 | 49 | context 'without email regex' do 50 | let(:regex) { nil } 51 | 52 | it 'is not enabled' do 53 | expect(described_class).to_not be_enabled 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/lib/rules/github_mfa_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe Rules::GithubMfa do 4 | subject(:rule) { Rules::GithubMfa.new(github_user) } 5 | let(:github_user) { build(:github_user_with_user) } 6 | let(:user) { github_user.user } 7 | let(:settings) { double } 8 | 9 | before do 10 | allow(described_class).to receive(:settings).and_return(settings) 11 | end 12 | 13 | it 'is valid when MFA is enabled' do 14 | github_user.mfa = true 15 | expect(rule).to be_valid 16 | end 17 | 18 | it 'is invaid when MFA is disabled' do 19 | github_user.mfa = false 20 | expect(rule).to_not be_valid 21 | end 22 | 23 | it 'is invalid when MFA is unknown' do 24 | github_user.mfa = nil 25 | expect(rule).to_not be_valid 26 | end 27 | 28 | it 'is required for external users' do 29 | expect(rule).to be_required_for_external 30 | end 31 | 32 | it 'returns an error message' do 33 | expect(rule.error_msg).to be_a(String) 34 | expect(rule.error_msg).to include('factor') 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/lib/rules/github_oauth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe Rules::GithubOauth do 4 | subject(:rule) { Rules::GithubOauth.new(github_user) } 5 | let(:github_user) { build(:github_user_with_user) } 6 | let(:user) { github_user.user } 7 | let(:settings) { double } 8 | 9 | before do 10 | allow(described_class).to receive(:settings).and_return(settings) 11 | github_user.token = 'footoken' 12 | end 13 | 14 | it 'is invalid when GitHub token is missing' do 15 | github_user.token = nil 16 | expect(rule).to_not be_valid 17 | end 18 | 19 | it 'is invalid with notoken GitHub error' do 20 | github_user.sync_error = 'notoken' 21 | expect(rule).to_not be_valid 22 | end 23 | 24 | it 'is invalid with unauthorized GitHub error' do 25 | github_user.sync_error = 'unauthorized' 26 | expect(rule).to_not be_valid 27 | end 28 | 29 | it 'is valid with no errors' do 30 | expect(rule).to be_valid 31 | end 32 | 33 | it 'is valid with GitHub server error' do 34 | github_user.sync_error = 'internal_server_error' 35 | expect(rule).to be_valid 36 | end 37 | 38 | it 'is not required for external users' do 39 | expect(rule).to_not be_required_for_external 40 | end 41 | 42 | it 'returns an error message when a token is missing' do 43 | github_user.token = nil 44 | expect(rule.error_msg).to be_a(String) 45 | expect(rule.error_msg.downcase).to include('missing') 46 | end 47 | 48 | it 'returns an error message when a token is missing' do 49 | github_user.sync_error = 'unauthorized' 50 | expect(rule.error_msg).to be_a(String) 51 | expect(rule.error_msg.downcase).to include('invalid') 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/lib/rules/last_github_sync_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe Rules::LastGithubSync do 4 | subject(:rule) { described_class.new(github_user) } 5 | let(:user) { github_user.user } 6 | let(:github_user) { build(:github_user_with_user) } 7 | let(:settings) { double(rule_max_sync_age: max_sync_age) } 8 | 9 | before do 10 | allow(described_class).to receive(:settings).and_return(settings) 11 | end 12 | 13 | context 'with max sync setting' do 14 | let(:max_sync_age) { 86400 } 15 | 16 | it 'is enabled' do 17 | expect(described_class).to be_enabled 18 | end 19 | 20 | it 'is valid when Github was recently synced' do 21 | github_user.last_sync_at = Time.now 22 | expect(rule).to be_valid 23 | end 24 | 25 | it 'is not valid when Github sync is out of date' do 26 | github_user.last_sync_at = Time.now - 2.days 27 | expect(rule).to_not be_valid 28 | end 29 | 30 | it 'is not valid when GitHub sync date is missing' do 31 | github_user.last_sync_at = nil 32 | expect(rule).to_not be_valid 33 | end 34 | 35 | it 'is required for external users' do 36 | expect(rule).to be_required_for_external 37 | end 38 | 39 | describe '#error_msg' do 40 | it 'returns an error message if GitHub user has never synced' do 41 | github_user.last_sync_at = nil 42 | expect(rule.error_msg).to include('never') 43 | end 44 | 45 | it 'returns an error message if GitHub user is too old' do 46 | github_user.last_sync_at = Time.now - 2.days 47 | expect(rule.error_msg).to include('old') 48 | end 49 | end 50 | 51 | end 52 | 53 | context 'without max sync setting' do 54 | let(:max_sync_age) { nil } 55 | 56 | it 'is not enabled' do 57 | expect(described_class).to_not be_enabled 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/lib/rules/last_ldap_sync_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe Rules::LastLdapSync do 4 | subject(:rule) { described_class.new(github_user) } 5 | let(:user) { github_user.user } 6 | let(:github_user) { build(:github_user_with_user) } 7 | let(:settings) { double(rule_max_sync_age: max_sync_age) } 8 | 9 | before do 10 | allow(described_class).to receive(:settings).and_return(settings) 11 | end 12 | 13 | context 'with max sync setting' do 14 | let(:max_sync_age) { 86400 } 15 | 16 | it 'is enabled' do 17 | expect(described_class).to be_enabled 18 | end 19 | 20 | it 'is valid when Active Directory was recently synced' do 21 | user.last_ldap_sync = Time.now 22 | expect(rule).to be_valid 23 | end 24 | 25 | it 'is not valid when Active Directory sync is out of date' do 26 | user.last_ldap_sync = Time.now - 2.days 27 | expect(rule).to_not be_valid 28 | end 29 | 30 | it 'is not valid when Active Directory sync date is missing' do 31 | user.last_ldap_sync = nil 32 | expect(rule).to_not be_valid 33 | end 34 | 35 | it 'is not required for external users' do 36 | expect(rule).to_not be_required_for_external 37 | end 38 | 39 | describe '#error_msg' do 40 | it 'returns an error message if LDAP user doesn\'t exist' do 41 | github_user.user = nil 42 | expect(rule.error_msg).to include('user') 43 | end 44 | 45 | it 'returns an error message if Active Directory has never synced' do 46 | user.last_ldap_sync = nil 47 | expect(rule.error_msg).to include('never') 48 | end 49 | 50 | it 'returns an error message if Active Directory is too old' do 51 | user.last_ldap_sync = Time.now - 2.days 52 | expect(rule.error_msg).to include('old') 53 | end 54 | end 55 | 56 | end 57 | 58 | context 'without max sync setting' do 59 | let(:max_sync_age) { nil } 60 | 61 | it 'is not enabled' do 62 | expect(described_class).to_not be_enabled 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/lib/rules_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe Rules do 4 | it 'returns default rules' do 5 | rules = [ 6 | Rules::Email, 7 | Rules::LastGithubSync, 8 | Rules::LastLdapSync, 9 | Rules::ActiveLdap, 10 | Rules::GithubMfa 11 | ] 12 | 13 | rules.each do |rule_klass| 14 | allow(rule_klass).to receive(:enabled?).and_return(true) 15 | end 16 | 17 | rules.each do |rule_klass| 18 | expect(Rules.enabled_rules).to include(rule_klass) 19 | end 20 | end 21 | 22 | it 'returns instantiated objects for a specific user' do 23 | user = build(:github_user) 24 | rule = Rules.for_github_user(user).first 25 | expect(rule).to be_a(Rules::Base) 26 | expect(rule.github_user).to eq(user) 27 | end 28 | 29 | describe Rules::Iterator do 30 | let(:user) { double(:user) } 31 | let(:rules) {[ 32 | double(:rule1, name: 'rule1', valid?: false, required_for_external?: true), 33 | double(:rule2, name: 'rule2', valid?: false, required_for_external?: false), 34 | double(:rule3, name: 'rule3', valid?: true, required_for_external?: true), 35 | ]} 36 | let(:iterator) { described_class.new(rules) } 37 | 38 | it 'filters for failing rules' do 39 | expect(iterator.failing.map(&:name)).to eq(%w(rule1 rule2)) 40 | end 41 | 42 | it 'filters for passing rules' do 43 | expect(iterator.passing.map(&:name)).to eq(%w(rule3)) 44 | end 45 | 46 | it 'filters for external rules' do 47 | expect(iterator.external.map(&:name)).to eq(%w(rule1 rule3)) 48 | end 49 | 50 | it 'allows chaining filters' do 51 | expect(iterator.failing.external.map(&:name)).to eq(%w(rule1)) 52 | end 53 | 54 | it 'adds filters to clones without filtering original' do 55 | iterator2 = iterator.dup 56 | expect(iterator2.external.map(&:name)).to eq(%w(rule1 rule3)) 57 | expect(iterator.map(&:name)).to eq(%w(rule1 rule2 rule3)) 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/mailers/user_mailer_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe UserMailer, :type => :mailer do 4 | 5 | before do 6 | Rails.application.settings.email_base_url = 'http://localhost:3000' 7 | end 8 | 9 | describe '#access_revoked' do 10 | subject(:mail) { UserMailer.access_revoked(user, github_user) } 11 | 12 | let(:user) { build(:user) } 13 | let(:github_user) { build(:github_user, user: user) } 14 | 15 | it 'renders subject' do 16 | expect(mail.subject).to eq('GitHub Access Revoked') 17 | end 18 | 19 | it 'renders html' do 20 | expect(mail).to be_multipart 21 | expect(mail.html_part.body).to include('GitHub access revoked!') 22 | end 23 | 24 | it 'renders plaintext' do 25 | expect(mail).to be_multipart 26 | expect(mail.text_part.body).to include('GitHub access revoked!') 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/models/connect_github_user_status_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe ConnectGithubUserStatus do 4 | subject(:status) { described_class.new(step: :request, status: :running) } 5 | let(:step) { :request } 6 | 7 | it 'computes completed steps' do 8 | expect(status.step_complete?(:add)).to eq(false) 9 | expect(status.steps_completed).to be_empty 10 | status.step = :add 11 | expect(status.step_complete?(:request)).to eq(true) 12 | status.status = :complete 13 | expect(status.step_complete?(:teams)).to eq(true) 14 | end 15 | 16 | it 'computes disabled steps' do 17 | expect(status.step_disabled?(:grant)).to eq(true) 18 | expect(status.step_disabled?(:create)).to eq(false) 19 | end 20 | 21 | it 'computes in progress status' do 22 | expect(status.in_progress?).to eq(true) 23 | status.status = :complete 24 | expect(status.in_progress?).to eq(false) 25 | end 26 | 27 | it 'computes complete status' do 28 | expect(status.complete?).to eq(false) 29 | status.status = :complete 30 | expect(status.complete?).to eq(true) 31 | end 32 | 33 | it 'computes error steps' do 34 | expect(status.step_error?(:request)).to eq(false) 35 | status.status = :error 36 | expect(status.step_error?(:request)).to eq(true) 37 | expect(status.step_error?(:create)).to eq(false) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/models/github_team_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe GithubTeam do 4 | subject(:team) { described_class.new(id: 1) } 5 | 6 | context 'with GitHub' do 7 | let(:github_admin) { double('github_admin') } 8 | let(:gh_teams) {{ 9 | 1 => {id: 1, name: 'My Team 1', slug: 'my_team_1', organization: 'org1'}, 10 | 5 => {id: 5, name: 'My Team 5', slug: 'my_team_5', organization: 'org1'}, 11 | }} 12 | let(:team_members) {{ 13 | 'hsimpson' => {login: 'hsimpson', name: 'Homer Simpson'}, 14 | 'msimpson' => {login: 'msimpson', name: 'Marge Simpson'}, 15 | }} 16 | 17 | before do 18 | allow(team).to receive(:github_admin).and_return(github_admin) 19 | allow(github_admin).to receive(:teams).and_return(gh_teams) 20 | allow(github_admin).to receive(:team) do |team_id| 21 | gh_teams.values.find { |t| t[:id] == team_id || t[:slug] == team_id } 22 | end 23 | allow(github_admin).to receive(:team_members).and_return(team_members) 24 | end 25 | 26 | it 'synchronizes team information' do 27 | team.sync 28 | expect(team.name).to eq('My Team 1') 29 | expect(team.organization).to eq('org1') 30 | expect(team.slug).to eq('my_team_1') 31 | end 32 | 33 | it 'synchronizes added members' do 34 | create(:github_user, login: 'hsimpson') 35 | create(:github_user, login: 'msimpson') 36 | team.sync! 37 | expect(team.github_users.size).to eq(2) 38 | members = team.github_users.map { |t| t.login } 39 | expect(members).to include('hsimpson', 'msimpson') 40 | end 41 | 42 | it 'synchronizes removed members' do 43 | team.github_users << create(:github_user, login: 'foouser') 44 | team.sync 45 | members = team.github_users.map { |t| t.login } 46 | expect(members).to_not include('foouser') 47 | end 48 | 49 | it 'only saves if information changed' do 50 | team.sync 51 | expect(team).to_not receive(:save) 52 | expect(team).to_not receive(:save!) 53 | expect(team.sync).to eq(true) 54 | end 55 | 56 | it 'only saves if information changed' do 57 | team.sync 58 | expect(team).to_not receive(:save) 59 | expect(team).to_not receive(:save!) 60 | expect(team.sync).to eq(true) 61 | end 62 | end 63 | 64 | it 'returns a GithubAdmin client' do 65 | expect(team.github_admin).to be_a(GithubAdmin) 66 | end 67 | 68 | it 'returns a "full" slug' do 69 | team.organization = "org1" 70 | team.slug = "my_team_1" 71 | expect(team.full_slug).to eq("org1/my_team_1") 72 | end 73 | 74 | it 'finds by "full" slug' do 75 | team.organization = "org1" 76 | team.slug = "my_team_1" 77 | team.save 78 | 79 | found_team = described_class.find_by_full_slug('org1/my_team_1') 80 | expect(found_team).to be_a(GithubTeam) 81 | expect(found_team.id).to eq(team.id) 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/models/setting_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Setting, :type => :model do 4 | end 5 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV["RAILS_ENV"] ||= 'test' 3 | require 'spec_helper' 4 | require File.expand_path("../../config/environment", __FILE__) 5 | require 'rspec/rails' 6 | 7 | # Requires supporting ruby files with custom matchers and macros, etc, in 8 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 9 | # run as spec files by default. This means that files in spec/support that end 10 | # in _spec.rb will both be required and run as specs, causing the specs to be 11 | # run twice. It is recommended that you do not name files matching this glob to 12 | # end with _spec.rb. You can configure this pattern with with the --pattern 13 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 14 | Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } 15 | 16 | # Checks for pending migrations before tests are run. 17 | # If you are not using ActiveRecord, you can remove this line. 18 | ActiveRecord::Migration.maintain_test_schema! 19 | 20 | RSpec.configure do |config| 21 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 22 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 23 | 24 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 25 | # examples within a transaction, remove the following line or assign false 26 | # instead of true. 27 | config.use_transactional_fixtures = false 28 | 29 | # RSpec Rails can automatically mix in different behaviours to your tests 30 | # based on their file location, for example enabling you to call `get` and 31 | # `post` in specs under `spec/controllers`. 32 | # 33 | # You can disable this behaviour by removing the line below, and instead 34 | # explicitly tag your specs with their type, e.g.: 35 | # 36 | # RSpec.describe UsersController, :type => :controller do 37 | # # ... 38 | # end 39 | # 40 | # The different available types are documented in the features, such as in 41 | # https://relishapp.com/rspec/rspec-rails/docs 42 | config.infer_spec_type_from_file_location! 43 | 44 | config.include FactoryGirl::Syntax::Methods 45 | config.include Devise::TestHelpers, type: :controller 46 | config.include Devise::TestHelpers, type: :view 47 | config.include ControllerHelpers, type: :controller 48 | 49 | config.before(:suite) do 50 | FactoryGirl.lint 51 | DatabaseCleaner.clean_with(:deletion) 52 | end 53 | 54 | DatabaseCleaner.strategy = :deletion 55 | config.around(:each) do |example| 56 | DatabaseCleaner.cleaning do 57 | example.run 58 | end 59 | end 60 | 61 | end 62 | -------------------------------------------------------------------------------- /spec/support/controller_helpers.rb: -------------------------------------------------------------------------------- 1 | module ControllerHelpers 2 | def sign_in(user=nil) 3 | user = create(:user) unless user 4 | super(user) 5 | end 6 | 7 | def configured 8 | Rails.application.settings.configured = true 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/views/connect/index.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe "connect/index.html.erb", type: :view do 4 | let(:connect_status) { ConnectGithubUserStatus.new(step: :request) } 5 | let(:user) { build(:user) } 6 | 7 | before do 8 | assign(:connect_status, connect_status) 9 | allow(view).to receive(:current_user).and_return(user) 10 | end 11 | 12 | it 'renders' do 13 | render 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/views/layouts/application.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe "layouts/application", type: :view do 4 | before do 5 | Rails.application.settings.configured = true 6 | assign(:navbar, GithubConnector::Navbar.new) 7 | end 8 | 9 | context 'without user' do 10 | it 'does not display login items' do 11 | render 12 | expect(rendered).to_not include('Logout') 13 | end 14 | end 15 | 16 | context 'with user' do 17 | let(:user) { create(:user) } 18 | 19 | before do 20 | sign_in(user) 21 | end 22 | 23 | it 'displays login items' do 24 | render 25 | expect(rendered).to include('Logout') 26 | end 27 | 28 | it 'does not display admin navigation' do 29 | render 30 | expect(rendered).to_not include('Settings') 31 | end 32 | end 33 | 34 | context 'with admin user' do 35 | let(:user) { create(:admin_user) } 36 | 37 | before do 38 | sign_in(user) 39 | end 40 | 41 | it 'displays admin navigation' do 42 | render 43 | expect(rendered).to include('Settings') 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/views/settings/edit.html.erb_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe "settings/edit.html.erb", type: :view do 4 | # We can't call this "settings" because ViewExampleGroup adds view helpers 5 | # to the spec -- the settings helper would take precedence over a let 6 | let(:test_settings) { GithubConnector::Settings.new.disconnect; } 7 | let(:section_partials) { SettingsController.new.send(:section_partials) } 8 | let(:user) { build(:user) } 9 | 10 | before do 11 | controller.extend(SettingsMixin) 12 | controller.instance_variable_set('@settings', test_settings) 13 | assign(:settings, test_settings) 14 | assign(:section_partials, section_partials) 15 | allow(view).to receive(:current_user).and_return(user) 16 | end 17 | 18 | it 'replaces existing password with placeholder' do 19 | test_settings.ldap_admin_password = 'foopass' 20 | test_settings.save 21 | render 22 | expect(rendered).to_not include('foopass') 23 | end 24 | 25 | it 'does not replace new password' do 26 | test_settings.ldap_admin_password = 'foopass' 27 | render 28 | expect(rendered).to include('foopass') 29 | end 30 | 31 | it 'replaces existing GitHub token with placeholder' do 32 | test_settings.github_admin_token = 'footoken' 33 | test_settings.save 34 | render 35 | expect(rendered).to_not include('footoken') 36 | end 37 | 38 | it 'does not replace new GitHub token' do 39 | test_settings.github_admin_token = 'footoken' 40 | render 41 | expect(rendered).to include('footoken') 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/github-connector/0de9978ad30d02cea43f986d248c39967d453896/vendor/assets/javascripts/.keep -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rapid7/github-connector/0de9978ad30d02cea43f986d248c39967d453896/vendor/assets/stylesheets/.keep --------------------------------------------------------------------------------