ssh-allow 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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