raise-if-root 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,92 @@
1
+ # Raise If Root
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/raise-if-root.svg)](https://rubygems.org/gems/raise-if-root)
4
+ [![Build status](https://travis-ci.org/ab/raise-if-root.svg)](https://travis-ci.org/ab/raise-if-root)
5
+ [![Code Climate](https://codeclimate.com/github/ab/raise-if-root.svg)](https://codeclimate.com/github/ab/raise-if-root)
6
+ [![Inline Docs](http://inch-ci.org/github/ab/raise-if-root.svg?branch=master)](http://www.rubydoc.info/github/ab/raise-if-root/master)
7
+
8
+ *Raise If Root* is a small gem that helps prevent your application from ever
9
+ running as the root user (uid 0).
10
+
11
+ ## Why?
12
+
13
+ Many software systems rely on user privilege separation for security reasons.
14
+ Especially within containers or chroots, running as a non-privileged user gives
15
+ stronger isolation.
16
+
17
+ *Raise If Root* helps enforce that you never inadvertently load your
18
+ application code as root.
19
+
20
+ Will it protect you if your attacker is already running as root? Probably not.
21
+ But it does help remove opportunities for error, where you might accidentally
22
+ run root rake tasks, cron jobs, or deploy scripts.
23
+
24
+ ## Usage
25
+
26
+ Add the gem to your application's Gemfile:
27
+
28
+ ```ruby
29
+ gem 'raise-if-root', '~> 0'
30
+ ```
31
+
32
+ Require it from your main application code:
33
+
34
+ ```ruby
35
+ require 'raise-if-root'
36
+ ```
37
+
38
+ There is no step three! This will raise `RaiseIfRoot::AssertionFailed` if the
39
+ current uid is 0.
40
+
41
+ ### More complex patterns
42
+
43
+ See the [YARD documentation](http://www.rubydoc.info/github/ab/raise-if-root/master).
44
+
45
+ If you want to enforce that the application is running as a particular user,
46
+ there are several more specific functions available.
47
+
48
+ Load the library, which doesn't immediately raise when you load it.
49
+
50
+ ```ruby
51
+ # load the library, which doesn't raise
52
+ require 'raise-if-root/library'
53
+
54
+ # raise if running as uid 1000
55
+ RaiseIfRoot.raise_if_uid(1000)
56
+
57
+ # raise unless user is nobody
58
+ RaiseIfRoot.raise_if(username_not: 'nobody')
59
+
60
+ # raise with multiple conditions
61
+ RaiseIfRoot.raise_if(uid_not: 1000, gid_not: 500)
62
+ ```
63
+
64
+ ### Notification callbacks
65
+
66
+ If you want to sound the alarm with something more than just the exception, you
67
+ can add callbacks to send emails, smoke signals, etc.
68
+
69
+ ```ruby
70
+ # load the library, which doesn't raise
71
+ require 'raise-if-root/library'
72
+
73
+ RaiseIfRoot.add_assertion_callback do |err|
74
+ Mail.deliver do
75
+ from 'system@example.com'
76
+ to 'alerts@example.com'
77
+ subject 'App was run as root'
78
+ body "RaiseIfRoot is raising an exception:\n #{err.inspect}\n"
79
+ end
80
+ end
81
+
82
+ # ensure we're not root
83
+ RaiseIfRoot.raise_if_root
84
+ ```
85
+
86
+ ## Contributing
87
+
88
+ 1. Fork it
89
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
90
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
91
+ 4. Push to the branch (`git push origin my-new-feature`)
92
+ 5. Create new Pull Request
@@ -0,0 +1,15 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'bundler/setup'
3
+ require 'rspec/core/rake_task'
4
+
5
+ task :default do
6
+ sh 'rake -T'
7
+ end
8
+
9
+ RSpec::Core::RakeTask.new(:spec)
10
+
11
+ def alias_task(alias_task, original)
12
+ desc "Alias for rake #{original}"
13
+ task alias_task, Rake.application[original].arg_names => original
14
+ end
15
+ alias_task(:test, :spec)
@@ -0,0 +1,3 @@
1
+ require_relative 'raise-if-root/library'
2
+
3
+ RaiseIfRoot.raise_if_root
@@ -0,0 +1,169 @@
1
+ require 'etc'
2
+
3
+ require_relative './version'
4
+
5
+ module RaiseIfRoot
6
+ # Error class for RaiseIfRoot assertion failures. Inherits directly from
7
+ # Exception because we don't want a bare rescue to catch this.
8
+ # rubocop:disable Lint/InheritException
9
+ class AssertionFailed < Exception; end
10
+
11
+ # Raise if the process UID/EUID is 0 or if the process GID/EGID is 0.
12
+ #
13
+ # @raise [AssertionFailed] if running as root
14
+ #
15
+ def self.raise_if_root
16
+ raise_if(uid: 0, gid: 0)
17
+ end
18
+
19
+ # Raise if the process UID or EUID equals +uid+.
20
+ #
21
+ # @param uid [Integer]
22
+ #
23
+ # @raise [AssertionFailed]
24
+ #
25
+ # @see .raise_if
26
+ #
27
+ def self.raise_if_uid(uid)
28
+ raise_if(uid: uid)
29
+ end
30
+
31
+ # Raise AssertionFailed if any of the specified conditions are met. This is
32
+ # the primary method powering RaiseIfRoot.
33
+ #
34
+ # @param uid [Integer] Raise if the process UID or EUID matches
35
+ # @param gid [Integer] Raise if the process GID or EGID matches
36
+ #
37
+ # @param uid_not [Integer] Raise if the process UID or EUID does not match
38
+ # the provided value
39
+ # @param gid_not [Integer] Raise if the process GID or EGID does not match
40
+ # the provided value
41
+ #
42
+ # @param username [String] Raise if the username of the process UID or EUID
43
+ # matches the provided value
44
+ # @param username_not [String] Raise if the username of the process UID or
45
+ # EUID does not match the provided value
46
+ #
47
+ # @raise [AssertionFailed] if any of the conditions match.
48
+ #
49
+ # rubocop:disable Metrics/ParameterLists
50
+ def self.raise_if(uid: nil, gid: nil, uid_not: nil, gid_not: nil,
51
+ username: nil, username_not: nil)
52
+ if uid
53
+ assert_not_equal('UID', Process.uid, uid)
54
+ assert_not_equal('EUID', Process.euid, uid)
55
+ end
56
+
57
+ if gid
58
+ assert_not_equal('GID', Process.gid, gid)
59
+ assert_not_equal('EGID', Process.egid, gid)
60
+ end
61
+
62
+ if uid_not
63
+ assert_equal('UID', Process.uid, uid_not)
64
+ assert_equal('EUID', Process.euid, uid_not)
65
+ end
66
+
67
+ if gid_not
68
+ assert_equal('GID', Process.gid, gid_not)
69
+ assert_equal('EGID', Process.egid, gid_not)
70
+ end
71
+
72
+ # raise if username
73
+ if username
74
+ assert_not_equal('username', Etc.getpwuid(Process.uid).name, username)
75
+ assert_not_equal('effective username', Etc.getpwuid(Process.euid).name,
76
+ username)
77
+ end
78
+
79
+ # raise unless username is username_not
80
+ if username_not
81
+ assert_equal('username', Etc.getpwuid(Process.uid).name, username_not)
82
+ assert_equal('effective username', Etc.getpwuid(Process.euid).name,
83
+ username_not)
84
+ end
85
+ end
86
+
87
+ # Assert that two values are equal. If they are not, run assertion callbacks
88
+ # and raise AssertionFailed.
89
+ #
90
+ # @param label [String] The label for the comparison we're making
91
+ # @param actual The actual value
92
+ # @param expected The expected value
93
+ #
94
+ # @raise [AssertionFailed] if the values are not equal
95
+ #
96
+ def self.assert_equal(label, actual, expected)
97
+ if expected.nil?
98
+ warn('warning: RaiseIfRoot.assert_equal called with expected=nil')
99
+ end
100
+ if actual != expected
101
+ err = new_assertion_failed(label, actual, expected)
102
+ run_assertion_callbacks(err)
103
+ raise err
104
+ end
105
+ end
106
+
107
+ # Assert that two values are not equal. But if they are equal, run assertion
108
+ # callbacks and raise AssertionFailed.
109
+ #
110
+ # @param label [String] The label for the comparison we're making
111
+ # @param actual The actual value
112
+ # @param expected The expected value
113
+ #
114
+ # @raise [AssertionFailed] if the values are equal
115
+ #
116
+ def self.assert_not_equal(label, actual, expected)
117
+ if actual == expected
118
+ err = new_assertion_failed(label, actual)
119
+ run_assertion_callbacks(err)
120
+ raise err
121
+ end
122
+ end
123
+
124
+ # Create a new AssertionFailed object.
125
+ #
126
+ # @param label [String] The label for the comparison we're making
127
+ # @param actual The actual value
128
+ # @param expected The expected value, if any
129
+ #
130
+ # @return [AssertionFailed]
131
+ #
132
+ def self.new_assertion_failed(label, actual, expected=nil)
133
+ # rubocop:disable Style/SpecialGlobalVars
134
+ message = "Process[#{$$}] #{label} is #{actual.inspect}"
135
+ if expected
136
+ message << ", expected #{expected.inspect}"
137
+ end
138
+
139
+ AssertionFailed.new(message)
140
+ end
141
+
142
+ # Add a callback to the list of assertion callbacks that are executed when an
143
+ # assertion fails. The callback will be passed one argument: the
144
+ # AssertionFailed exception object just before it is raised.
145
+ def self.add_assertion_callback(&block)
146
+ raise ArgumentError.new("Must pass block") unless block
147
+
148
+ assertion_callbacks << block
149
+ end
150
+
151
+ # The list of stored assertion callbacks. These are executed when an
152
+ # assertion fails just before the assertion is raised.
153
+ #
154
+ # @return [Array<Proc>]
155
+ #
156
+ def self.assertion_callbacks
157
+ @assertion_callbacks ||= []
158
+ end
159
+
160
+ # Execute all of the stored assertion callbacks.
161
+ #
162
+ # @param [AssertionFailed] err The exception object to pass to each callback.
163
+ #
164
+ # @return [Array] The collected return values of the callbacks.
165
+ #
166
+ def self.run_assertion_callbacks(err)
167
+ assertion_callbacks.map { |block| block.call(err) }
168
+ end
169
+ end
@@ -0,0 +1,4 @@
1
+ module RaiseIfRoot
2
+ # version string
3
+ VERSION = '0.0.1'.freeze
4
+ end
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'raise-if-root/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'raise-if-root'
9
+ spec.version = RaiseIfRoot::VERSION
10
+ spec.authors = ['Andy Brody']
11
+ spec.email = ['git@abrody.com']
12
+ spec.summary = 'Library that raises when run as root user'
13
+ spec.description = <<-EOM
14
+ Raise If Root is a small library that raises an exception when run as the
15
+ root user (uid 0). This helps ensure that you never accidentally run your
16
+ application as root.
17
+ EOM
18
+ spec.homepage = 'https://github.com/ab/raise-if-root'
19
+ spec.license = 'GPL-3'
20
+
21
+ spec.files = `git ls-files`.split($/)
22
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
23
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
24
+ spec.require_paths = ['lib']
25
+
26
+ spec.add_development_dependency 'bundler', '~> 1.3'
27
+ spec.add_development_dependency 'pry'
28
+ spec.add_development_dependency 'rake'
29
+ spec.add_development_dependency 'rspec', '~> 3.0'
30
+ spec.add_development_dependency 'rubocop', '~> 0'
31
+ spec.add_development_dependency 'yard'
32
+
33
+ spec.required_ruby_version = '>= 2.0'
34
+ end
@@ -0,0 +1,88 @@
1
+ require_relative '../lib/raise-if-root/library'
2
+
3
+ # Given that it is always loaded, you are encouraged to keep this file as
4
+ # light-weight as possible. Requiring heavyweight dependencies from this file
5
+ # will add to the boot time of your test suite on EVERY test run, even for an
6
+ # individual file that may not need all of that loaded. Instead, consider making
7
+ # a separate helper file that requires the additional dependencies and performs
8
+ # the additional setup, and require it from the spec files that actually need
9
+ # it.
10
+ #
11
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
12
+ RSpec.configure do |config|
13
+ # rspec-expectations config goes here. You can use an alternate
14
+ # assertion/expectation library such as wrong or the stdlib/minitest
15
+ # assertions if you prefer.
16
+ config.expect_with :rspec do |expectations|
17
+ # This option will default to `true` in RSpec 4. It makes the `description`
18
+ # and `failure_message` of custom matchers include text for helper methods
19
+ # defined using `chain`, e.g.:
20
+ # be_bigger_than(2).and_smaller_than(4).description
21
+ # # => "be bigger than 2 and smaller than 4"
22
+ # ...rather than:
23
+ # # => "be bigger than 2"
24
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
25
+ end
26
+
27
+ # rspec-mocks config goes here. You can use an alternate test double
28
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
29
+ config.mock_with :rspec do |mocks|
30
+ # Prevents you from mocking or stubbing a method that does not exist on
31
+ # a real object. This is generally recommended, and will default to
32
+ # `true` in RSpec 4.
33
+ mocks.verify_partial_doubles = true
34
+ end
35
+
36
+ # The settings below are suggested to provide a good initial experience
37
+ # with RSpec, but feel free to customize to your heart's content.
38
+
39
+ # These two settings work together to allow you to limit a spec run
40
+ # to individual examples or groups you care about by tagging them with
41
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
42
+ # get run.
43
+ # config.filter_run :focus
44
+ # config.run_all_when_everything_filtered = true
45
+
46
+ # Allows RSpec to persist some state between runs in order to support
47
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
48
+ # you configure your source control system to ignore this file.
49
+ # config.example_status_persistence_file_path = "spec/examples.txt"
50
+
51
+ # Limits the available syntax to the non-monkey patched syntax that is
52
+ # recommended. For more details, see:
53
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
54
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
55
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
56
+ config.disable_monkey_patching!
57
+
58
+ # This setting enables warnings. It's recommended, but in some cases may
59
+ # be too noisy due to issues in dependencies.
60
+ config.warnings = true
61
+
62
+ # Many RSpec users commonly either run the entire suite or an individual
63
+ # file, and it's useful to allow more verbose output when running an
64
+ # individual spec file.
65
+ if config.files_to_run.one?
66
+ # Use the documentation formatter for detailed output,
67
+ # unless a formatter has already been configured
68
+ # (e.g. via a command-line flag).
69
+ config.default_formatter = 'doc'
70
+ end
71
+
72
+ # Print the 10 slowest examples and example groups at the
73
+ # end of the spec run, to help surface which specs are running
74
+ # particularly slow.
75
+ config.profile_examples = 10
76
+
77
+ # Run specs in random order to surface order dependencies. If you find an
78
+ # order dependency and want to debug it, you can fix the order by providing
79
+ # the seed, which is printed after each run.
80
+ # --seed 1234
81
+ config.order = :random
82
+
83
+ # Seed global randomization in this process using the `--seed` CLI option.
84
+ # Setting this allows you to use `--seed` to deterministically reproduce
85
+ # test failures related to randomization by passing the same `--seed` value
86
+ # as the one that triggered the failure.
87
+ Kernel.srand config.seed
88
+ end
@@ -0,0 +1,140 @@
1
+ # NB: These tests cannot be run as root (uid 0 or gid 0), sorry.
2
+ RSpec.describe RaiseIfRoot do
3
+
4
+ describe '.raise_if_root' do
5
+ it 'should raise if uid is 0' do
6
+ allow(Process).to receive(:uid).and_return(0)
7
+ expect {
8
+ RaiseIfRoot.raise_if_root
9
+ }.to raise_error(RaiseIfRoot::AssertionFailed, /\bUID\b/)
10
+ end
11
+ it 'should raise if euid is 0' do
12
+ allow(Process).to receive(:euid).and_return(0)
13
+ expect {
14
+ RaiseIfRoot.raise_if_root
15
+ }.to raise_error(RaiseIfRoot::AssertionFailed, /\bEUID\b/)
16
+ end
17
+ it 'should raise if gid is 0' do
18
+ allow(Process).to receive(:gid).and_return(0)
19
+ expect {
20
+ RaiseIfRoot.raise_if_root
21
+ }.to raise_error(RaiseIfRoot::AssertionFailed, /\bGID\b/)
22
+ end
23
+ it 'should raise if egid is 0' do
24
+ allow(Process).to receive(:egid).and_return(0)
25
+ expect {
26
+ RaiseIfRoot.raise_if_root
27
+ }.to raise_error(RaiseIfRoot::AssertionFailed, /\bEGID\b/)
28
+ end
29
+
30
+ it 'should not raise otherwise' do
31
+ expect(RaiseIfRoot.raise_if_root).to eq nil
32
+ end
33
+
34
+ it 'runs assertion callbacks' do
35
+ allow(Process).to receive(:uid).and_return(0)
36
+ expect(RaiseIfRoot).to receive(:run_assertion_callbacks).with(
37
+ instance_of(RaiseIfRoot::AssertionFailed)
38
+ ).once.and_call_original
39
+
40
+ expect {
41
+ RaiseIfRoot.raise_if_root
42
+ }.to raise_error(RaiseIfRoot::AssertionFailed)
43
+ end
44
+ end
45
+
46
+ describe '.add_assertion_callback' do
47
+ before do
48
+ RaiseIfRoot.assertion_callbacks.clear
49
+ end
50
+ after do
51
+ RaiseIfRoot.assertion_callbacks.clear
52
+ end
53
+
54
+ it 'adds a callback' do
55
+ expect(RaiseIfRoot.assertion_callbacks.length).to eq 0
56
+
57
+ RaiseIfRoot.add_assertion_callback { :returnvalue }
58
+ expect(RaiseIfRoot.assertion_callbacks.last).is_a?(Proc)
59
+ expect(RaiseIfRoot.assertion_callbacks.length).to eq 1
60
+
61
+ expect(RaiseIfRoot.run_assertion_callbacks(nil)).to eq([:returnvalue])
62
+ end
63
+ end
64
+
65
+ describe '.run_assertion_callbacks' do
66
+ before do
67
+ RaiseIfRoot.assertion_callbacks.clear
68
+ end
69
+ after do
70
+ RaiseIfRoot.assertion_callbacks.clear
71
+ end
72
+
73
+ it 'runs callbacks' do
74
+ sentinel = double('Sentinel')
75
+ expect(sentinel).to(receive(:callback).with(
76
+ instance_of(RaiseIfRoot::AssertionFailed)
77
+ ).twice.and_return(:callbackresponse1, :callbackresponse2))
78
+
79
+ expect(RaiseIfRoot.assertion_callbacks.length).to eq 0
80
+
81
+ RaiseIfRoot.add_assertion_callback { |err| sentinel.callback(err) }
82
+ RaiseIfRoot.add_assertion_callback { |err| sentinel.callback(err) }
83
+
84
+ expect(RaiseIfRoot.assertion_callbacks.length).to eq 2
85
+
86
+ expect(RaiseIfRoot.run_assertion_callbacks(
87
+ RaiseIfRoot::AssertionFailed.new
88
+ )).to eq([:callbackresponse1, :callbackresponse2])
89
+ end
90
+ end
91
+
92
+ describe '.raise_if' do
93
+ before do
94
+ allow(Process).to receive(:uid).and_return(5000)
95
+ allow(Process).to receive(:euid).and_return(5000)
96
+
97
+ allow(Process).to receive(:gid).and_return(5000)
98
+ allow(Process).to receive(:egid).and_return(5000)
99
+
100
+ @someuser = double('Etc::Passwd', name: 'someuser')
101
+ end
102
+
103
+ it 'does not raise by default' do
104
+ expect(RaiseIfRoot.raise_if).to eq nil
105
+ expect(RaiseIfRoot.raise_if(uid: 0)).to eq nil
106
+ expect(RaiseIfRoot.raise_if(gid: 0)).to eq nil
107
+ expect(RaiseIfRoot.raise_if(uid_not: 5000)).to eq nil
108
+ expect(RaiseIfRoot.raise_if(gid_not: 5000)).to eq nil
109
+ expect(RaiseIfRoot.raise_if(uid: 0, gid: 0)).to eq nil
110
+ end
111
+
112
+ it 'raises when any condition matches' do
113
+ expect {
114
+ RaiseIfRoot.raise_if(uid: 0, gid: 0, uid_not: 123, gid_not: 5000)
115
+ }.to raise_error(RaiseIfRoot::AssertionFailed, /\bUID\b/)
116
+ expect {
117
+ RaiseIfRoot.raise_if(uid: 0, gid: 5000, uid_not: 5000, gid_not: 5000)
118
+ }.to raise_error(RaiseIfRoot::AssertionFailed, /\bGID\b/)
119
+ expect {
120
+ RaiseIfRoot.raise_if(uid: 5000, gid: 0)
121
+ }.to raise_error(RaiseIfRoot::AssertionFailed, /\bUID\b/)
122
+ end
123
+
124
+ it 'handles username and not_username' do
125
+ allow(Etc).to receive(:getpwuid).with(5000).and_return(@someuser)
126
+
127
+ expect {
128
+ RaiseIfRoot.raise_if(username: 'someuser')
129
+ }.to raise_error(RaiseIfRoot::AssertionFailed, /\busername\b/)
130
+
131
+ expect(RaiseIfRoot.raise_if(username: 'other')).to eq nil
132
+
133
+ expect {
134
+ RaiseIfRoot.raise_if(username_not: 'other')
135
+ }.to raise_error(RaiseIfRoot::AssertionFailed, /\busername\b/)
136
+
137
+ expect(RaiseIfRoot.raise_if(username_not: 'someuser')).to eq nil
138
+ end
139
+ end
140
+ end