ssh-allow 0.6.0

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,6 @@
1
+ .DS_Store
2
+ *.gem
3
+ .bundle
4
+ Gemfile.lock
5
+ pkg/*
6
+ tmp/*
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --format=doc
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in ssh-allow.gemspec
4
+ gemspec
@@ -0,0 +1,24 @@
1
+ # License & Copywrite
2
+
3
+ **Copyright (c) 2008 by the Trustees of Dartmouth College. All rights reserved.**
4
+
5
+ The remote_key Ruby library, and accompanying documentation, are provided
6
+ subject to the following license agreement. By obtaining and/or using the
7
+ software, you agree that you have read and understood this agreement, and
8
+ will comply with its terms and conditions.
9
+
10
+ 1. Permission to use, copy, modify and distribute this software and
11
+ documentation without fee is hereby granted, provided that the copyright
12
+ notice and this agreement appear on all copies of the software and
13
+ documentation.
14
+
15
+ 2. Any documentation and advertising material relating to this software must
16
+ acknowledge that the software was developed by Dartmouth College.
17
+
18
+ 3. The name of Dartmouth College may not be used to endorse or promote
19
+ products derived from this software without specific prior written
20
+ permission.
21
+
22
+ 4. Neither the Trustees of Dartmouth College nor the authors make any
23
+ representations about the suitability of this software for any purpose. It is
24
+ provided "as is", without any express or implied warranty.
@@ -0,0 +1,70 @@
1
+ # ssh-allow
2
+
3
+ A mini-DSL for specifying which remote SSH commands are allowed (or denied) during a remote shell session, authenticated via an SSH key. Also comes with a command-line binary for comparing the current remote command against the list.
4
+
5
+ ## Purpose
6
+
7
+ Guarding against the SSH_ORIGINAL_COMMAND environment is fairly command and allowing/denying simple commands is easy to do with a simple bash shell. Unfortunately, if what you need to do is allow a very narrow set of non-trivial commands, where the various command-line options and arguments need to be specified, trying to accomplish this with a bash script quickly becomes arduous.
8
+
9
+ To try and solve this problem, I developed an extremely simple _DSL_, which is really just some stripped down ruby code, that would make specifying almost any range of commands, with almost any range of options and/or arguments, a straightforward problem. Those **rules** needed a specialized command-line binary to both process them and compare the allowed (or denied) commands against ENV['SSH_ORIGINAL_COMMAND'].
10
+
11
+ That's ssh-allow.
12
+
13
+ ---
14
+
15
+ ## Installation
16
+
17
+ $ sudo gem install ssh-allow
18
+
19
+ ---
20
+
21
+ ## Usage
22
+
23
+ ### guard (default command)
24
+
25
+ $ ssh-allow guard [-r | --rules=<file>] [-e | --echo]
26
+
27
+ The --rules `<file>` path defaults to `~/.ssh-rules`. If the intent is to deploy ssh-allow across multiple accounts on a single Unix/Linux server, then it's recommended that you create `/etc/ssh-allow/` to hold the various rules files.
28
+
29
+ The --echo switch echos the command to std_in, prior to executing it.
30
+
31
+ The guard command is intended to be specified in the ~/.ssh/authorized_keys file, as the front-part of the line containing the SSH key that needs command guarding. The configuration usually looks like this:
32
+
33
+ command="/usr/local/bin/ssh-allow guard -r=/etc/ssh-allow/rule_file" ssh-rsa AAAAB3Nza
34
+
35
+ ---
36
+
37
+ ## Rules Specification DSL
38
+
39
+ Allowing an `ls` command, with no options and no arguments.
40
+
41
+ ```ruby
42
+ allow!('ls')
43
+ ```
44
+
45
+ Allowing an `ls` command, with any options and any arguments.
46
+
47
+ ```ruby
48
+ allow!('ls') do
49
+ opts :any
50
+ args :any
51
+ end
52
+ ```
53
+
54
+ Allowing an `ls` command, with a specific option and a file-path argument regex.
55
+
56
+ ```ruby
57
+ allow!('ls') do
58
+ opts '-ld'
59
+ args '^/foo/bar/.*'
60
+ end
61
+ ```
62
+
63
+ Allowing a `cp` command, with no options and two file-path argument regexes.
64
+
65
+ ```ruby
66
+ allow!('cp') do
67
+ args '^/foo/bar/.*'
68
+ args '^/foo/baz/.*'
69
+ end
70
+ ```
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'ssh/allow'
4
+ SSH::Allow::CLI.start
@@ -0,0 +1,4 @@
1
+ require 'ssh/allow/cli'
2
+ require 'ssh/allow/rule_set'
3
+ require 'ssh/allow/rule'
4
+ require 'ssh/allow/command'
@@ -0,0 +1,33 @@
1
+ require 'thor'
2
+
3
+ module SSH ; module Allow
4
+ class CLI < Thor
5
+
6
+ desc "guard --rules | -r=<file> [--echo | -e]",
7
+ "Guard against the SSH_ORIGINAL_COMMAND, using rules in the --rules file."
8
+ method_option :rules, :type => :string, :aliases => '-r', :required => true,
9
+ :default => File.expand_path("~/.ssh-rules"), :banner => "Path to rules file"
10
+ method_option :echo, :type => :boolean, :default => false, :aliases => '-e',
11
+ :banner => "Echo the SSH_ORIGINAL_COMMAND."
12
+ def guard
13
+ rule_set.read(options[:rules])
14
+ puts command if options[:echo]
15
+ command.allowed?(rule_set.rules) ? command.run : fail
16
+ end
17
+
18
+ private
19
+
20
+ def rule_set
21
+ @rule_set ||= SSH::Allow::RuleSet.new
22
+ end
23
+
24
+ def command
25
+ @command ||= SSH::Allow::Command.new(ENV['SSH_ORIGINAL_COMMAND'])
26
+ end
27
+
28
+ def fail
29
+ raise Thor::Error, "Remote Command Not Allowed: #{command}"
30
+ end
31
+
32
+ end
33
+ end ; end
@@ -0,0 +1,41 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__))
2
+
3
+ require 'polyglot'
4
+ require 'treetop'
5
+ require 'command_line'
6
+
7
+ module SSH ; module Allow
8
+ class Command
9
+ attr_reader :name, :options, :arguments
10
+
11
+ def initialize(cmd)
12
+ @cmd = cmd.to_s
13
+ @name = parsed.name.text_value
14
+ @options = parsed.option_list
15
+ @arguments = parsed.argument_list
16
+ end
17
+
18
+ def to_s
19
+ @cmd
20
+ end
21
+
22
+ def run
23
+ system(@cmd)
24
+ end
25
+
26
+ def allowed?(rules)
27
+ match, allow = false, false
28
+ rules.each do |rule|
29
+ match, allow = rule.match?(self)
30
+ break if match
31
+ end
32
+ allow
33
+ end
34
+
35
+ private
36
+
37
+ def parsed
38
+ @parsed ||= CommandLineParser.new.parse(@cmd)
39
+ end
40
+ end
41
+ end ; end
@@ -0,0 +1,104 @@
1
+ grammar CommandLine
2
+ rule command
3
+ name (delimited_option / delimited_argument)* {
4
+ def option_list
5
+ opts = self.elements[1].elements.inject([]) do |a, e|
6
+ a << e.opt_value
7
+ end
8
+ opts.compact
9
+ end
10
+
11
+ def argument_list
12
+ args = self.elements[1].elements.inject([]) do |a, e|
13
+ a << e.arg_value
14
+ end
15
+ args.compact
16
+ end
17
+ }
18
+ end
19
+
20
+ rule name
21
+ argument
22
+ end
23
+
24
+ rule delimited_option
25
+ ' ' option {
26
+ def opt_value
27
+ self.option.opt_value
28
+ end
29
+
30
+ def arg_value
31
+ self.option.arg_value
32
+ end
33
+ }
34
+ end
35
+
36
+ rule delimited_argument
37
+ ' ' argument {
38
+ def opt_value
39
+ nil
40
+ end
41
+
42
+ def arg_value
43
+ self.argument.text_value
44
+ end
45
+ }
46
+ end
47
+
48
+ rule option
49
+ (long_option / short_option)
50
+ end
51
+
52
+ rule long_option
53
+ '--' (opt_name '=' argument / opt_name) {
54
+ def opt_value
55
+ self.elements[1].respond_to?(:opt_name) ?
56
+ self.elements[1].opt_name.text_value : self.elements[1].text_value
57
+ end
58
+
59
+ def arg_value
60
+ self.elements[1].respond_to?(:opt_name) ?
61
+ self.elements[1].argument.text_value : nil
62
+ end
63
+ }
64
+ end
65
+
66
+ rule short_option
67
+ '-' (label_char '=' argument / opt_name) {
68
+ def opt_value
69
+ self.elements[1].respond_to?(:label_char) ?
70
+ self.elements[1].label_char.text_value : self.elements[1].text_value
71
+ end
72
+
73
+ def arg_value
74
+ self.elements[1].respond_to?(:label_char) ?
75
+ self.elements[1].argument.text_value : nil
76
+ end
77
+ }
78
+ end
79
+
80
+ rule argument
81
+ (quoted_chars / unquoted_chars) {
82
+ def arg_value
83
+ self.text_value
84
+ end
85
+ }
86
+ end
87
+
88
+ rule opt_name
89
+ label_char+
90
+ end
91
+
92
+ rule label_char
93
+ [a-zA-Z0-9]
94
+ end
95
+
96
+ rule quoted_chars
97
+ '"' (!'"' . / '\"')* '"'
98
+ end
99
+
100
+ rule unquoted_chars
101
+ (!' ' . / '\ ')+
102
+ end
103
+
104
+ end
@@ -0,0 +1,97 @@
1
+ module SSH ; module Allow
2
+
3
+ class Rule
4
+ attr_reader :command, :options, :arguments
5
+
6
+ def initialize(cmds)
7
+ @command = [cmds].flatten
8
+ @options = [:none]
9
+ @arguments = [:none]
10
+ end
11
+
12
+ def opts(opt_text)
13
+ opt_text.gsub!(/^--?/, '') if opt_text.is_a?(String)
14
+ push(:options, opt_text)
15
+ end
16
+
17
+ def args(arg_text)
18
+ arg_text = Regexp.new(arg_text) if arg_text.is_a?(String)
19
+ push(:arguments, arg_text)
20
+ end
21
+
22
+ def match_command?(name)
23
+ return false if none?(command)
24
+ return true if any?(command)
25
+ command.include?(name)
26
+ end
27
+
28
+ def match_options?(opt_list)
29
+ return opt_list.empty? if none?(options)
30
+ return true if any?(options)
31
+ return false if options.size != opt_list.size
32
+ opt_list.inject(true) { |match, opt| match && options.include?(opt) }
33
+ end
34
+
35
+ def match_arguments?(arg_list)
36
+ return arg_list.empty? if none?(arguments)
37
+ return true if any?(arguments)
38
+ return false if arguments.size != arg_list.size
39
+ arg_list.inject(true) { |match, arg| match && match_one_argument?(arg) }
40
+ end
41
+
42
+ def match_one_argument?(arg)
43
+ arguments.inject(false) { |match, one_arg| match || (one_arg === arg) }
44
+ end
45
+
46
+ def push(attrib, value)
47
+ send(attrib).clear if send(attrib) == [:none]
48
+ send(attrib).push(value)
49
+ end
50
+
51
+ def none?(part)
52
+ part == [:none]
53
+ end
54
+
55
+ def any?(part)
56
+ part == [:any]
57
+ end
58
+
59
+ private :push, :none?, :any?, :match_one_argument?
60
+
61
+ class << self
62
+ def allow(*cmds, &block)
63
+ create(Rule::Allow.new(cmds.flatten), &block)
64
+ end
65
+
66
+ def deny(*cmds, &block)
67
+ create(Rule::Deny.new(cmds.flatten), &block)
68
+ end
69
+
70
+ def create(rule, &block)
71
+ begin
72
+ rule.instance_eval(&block) if block_given?
73
+ rule
74
+ rescue Exception => e
75
+ false
76
+ end
77
+ end
78
+ end
79
+
80
+ class Allow < Rule
81
+ def match?(command)
82
+ match = [match_command?(command.name), match_options?(command.options),
83
+ match_arguments?(command.arguments)].inject(true) { |mem, var| var && mem }
84
+ return match, match
85
+ end
86
+ end
87
+
88
+ class Deny < Rule
89
+ def match?(command)
90
+ match = [match_command?(command.name), match_options?(command.options),
91
+ match_arguments?(command.arguments)].inject(true) { |mem, var| var && mem }
92
+ return match, !match
93
+ end
94
+ end
95
+ end
96
+
97
+ end ; end
@@ -0,0 +1,38 @@
1
+ module SSH ; module Allow
2
+
3
+ class RuleSet
4
+ attr_reader :rules
5
+
6
+ def initialize
7
+ @rules = []
8
+ end
9
+
10
+ def allow(cmd, &block)
11
+ push get_rule(:allow, cmd, block)
12
+ end
13
+
14
+ def allow!(cmd, &block)
15
+ rule = get_rule(:allow, cmd, block)
16
+ push(rule) or raise(%(Invalid rule: "#{cmd}"))
17
+ end
18
+
19
+ def read(path_to_rules)
20
+ self.instance_eval(read_rules(path_to_rules))
21
+ end
22
+
23
+ private
24
+
25
+ def push(rule)
26
+ rule ? @rules.push(rule) : false
27
+ end
28
+
29
+ def get_rule(type, cmd, block)
30
+ SSH::Allow::Rule.send(type, cmd, &block)
31
+ end
32
+
33
+ def read_rules(path_to_rules)
34
+ IO.read(path_to_rules)
35
+ end
36
+ end
37
+
38
+ end ; end