kennedy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.document +5 -0
  2. data/.gitignore +21 -0
  3. data/.yardoc +0 -0
  4. data/LICENSE +20 -0
  5. data/MAIN.rdoc +23 -0
  6. data/README.markdown +17 -0
  7. data/Rakefile +52 -0
  8. data/VERSION +1 -0
  9. data/bin/kennedy-gen +13 -0
  10. data/doc/Kennedy.html +98 -0
  11. data/doc/Kennedy/Backends.html +94 -0
  12. data/doc/Kennedy/Backends/LDAP.html +471 -0
  13. data/doc/Kennedy/BadTicketException.html +92 -0
  14. data/doc/Kennedy/Granter.html +570 -0
  15. data/doc/Kennedy/Server.html +258 -0
  16. data/doc/Kennedy/Ticket.html +875 -0
  17. data/doc/_index.html +170 -0
  18. data/doc/class_list.html +97 -0
  19. data/doc/css/common.css +1 -0
  20. data/doc/css/full_list.css +23 -0
  21. data/doc/css/style.css +261 -0
  22. data/doc/file.README.html +69 -0
  23. data/doc/file_list.html +29 -0
  24. data/doc/index.html +69 -0
  25. data/doc/js/app.js +91 -0
  26. data/doc/js/full_list.js +39 -0
  27. data/doc/js/jquery.js +19 -0
  28. data/doc/method_list.html +152 -0
  29. data/doc/top-level-namespace.html +80 -0
  30. data/kennedy.gemspec +114 -0
  31. data/lib/kennedy.rb +2 -0
  32. data/lib/kennedy/backends/ldap.rb +35 -0
  33. data/lib/kennedy/generator.rb +69 -0
  34. data/lib/kennedy/granter.rb +52 -0
  35. data/lib/kennedy/instance_configuration.rb +58 -0
  36. data/lib/kennedy/server.rb +164 -0
  37. data/lib/kennedy/ticket.rb +97 -0
  38. data/logo.png +0 -0
  39. data/template/config.ru.erb +22 -0
  40. data/template/config/api_keys.yml.erb +1 -0
  41. data/template/config/backend.rb +0 -0
  42. data/template/config/encryption.yml.erb +2 -0
  43. data/template/config/sessions.yml.erb +1 -0
  44. data/test/granter_test.rb +93 -0
  45. data/test/ldap_backend_test.rb +66 -0
  46. data/test/server_test.rb +285 -0
  47. data/test/teststrap.rb +34 -0
  48. data/test/ticket_test.rb +84 -0
  49. metadata +177 -0
@@ -0,0 +1,80 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
4
+ <head>
5
+ <meta name="Content-Type" content="text/html; charset=UTF-8" />
6
+ <title>Top Level Namespace</title>
7
+ <link rel="stylesheet" href="css/style.css" type="text/css" media="screen" charset="utf-8" />
8
+ <link rel="stylesheet" href="css/common.css" type="text/css" media="screen" charset="utf-8" />
9
+
10
+ <script type="text/javascript" charset="utf-8">
11
+ relpath = '';
12
+ if (relpath != '') relpath += '/';
13
+ </script>
14
+ <script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
15
+ <script type="text/javascript" charset="utf-8" src="js/app.js"></script>
16
+
17
+ </head>
18
+ <body>
19
+ <div id="header">
20
+ <div id="menu">
21
+
22
+ <a href="_index.html">Index</a> &raquo;
23
+
24
+
25
+ <span class="title">Top Level Namespace</span>
26
+
27
+ </div>
28
+
29
+ <div id="search">
30
+ <a id="class_list_link" href="#">Namespace List</a>
31
+ <a id="method_list_link" href="#">Method List</a>
32
+ <a id ="file_list_link" href="#">File List</a>
33
+ </div>
34
+
35
+ <div class="clear"></div>
36
+ </div>
37
+
38
+ <iframe id="search_frame"></iframe>
39
+
40
+ <div id="content"><h1>Top Level Namespace
41
+
42
+
43
+ </h1>
44
+
45
+ <dl class="box">
46
+
47
+
48
+
49
+
50
+
51
+
52
+
53
+ </dl>
54
+ <div class="clear"></div>
55
+
56
+ <h2>Defined Under Namespace</h2>
57
+ <p class="children">
58
+
59
+
60
+ <strong class="modules">Modules:</strong> <a title="Kennedy" href="Kennedy.html">Kennedy</a>
61
+
62
+
63
+
64
+
65
+ </p>
66
+
67
+
68
+
69
+
70
+
71
+ </div>
72
+
73
+ <div id="footer">
74
+ Generated on Tue Dec 8 16:39:39 2009 by
75
+ <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool">yard</a>
76
+ 0.4.0 (ruby-1.8.7).
77
+ </div>
78
+
79
+ </body>
80
+ </html>
@@ -0,0 +1,114 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{kennedy}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["gabrielg"]
12
+ s.date = %q{2009-12-09}
13
+ s.description = %q{Kennedy is out for Castronaut. A simple single-sign-on client and server library.}
14
+ s.email = %q{gabriel.gironda@gmail.com}
15
+ s.executables = ["kennedy-gen", "kennedy-gen"]
16
+ s.extra_rdoc_files = [
17
+ "LICENSE",
18
+ "README.markdown"
19
+ ]
20
+ s.files = [
21
+ ".document",
22
+ ".gitignore",
23
+ ".yardoc",
24
+ "LICENSE",
25
+ "MAIN.rdoc",
26
+ "README.markdown",
27
+ "Rakefile",
28
+ "VERSION",
29
+ "bin/kennedy-gen",
30
+ "doc/Kennedy.html",
31
+ "doc/Kennedy/Backends.html",
32
+ "doc/Kennedy/Backends/LDAP.html",
33
+ "doc/Kennedy/BadTicketException.html",
34
+ "doc/Kennedy/Granter.html",
35
+ "doc/Kennedy/Server.html",
36
+ "doc/Kennedy/Ticket.html",
37
+ "doc/_index.html",
38
+ "doc/class_list.html",
39
+ "doc/css/common.css",
40
+ "doc/css/full_list.css",
41
+ "doc/css/style.css",
42
+ "doc/file.README.html",
43
+ "doc/file_list.html",
44
+ "doc/index.html",
45
+ "doc/js/app.js",
46
+ "doc/js/full_list.js",
47
+ "doc/js/jquery.js",
48
+ "doc/method_list.html",
49
+ "doc/top-level-namespace.html",
50
+ "kennedy.gemspec",
51
+ "lib/kennedy.rb",
52
+ "lib/kennedy/backends/ldap.rb",
53
+ "lib/kennedy/generator.rb",
54
+ "lib/kennedy/granter.rb",
55
+ "lib/kennedy/instance_configuration.rb",
56
+ "lib/kennedy/server.rb",
57
+ "lib/kennedy/ticket.rb",
58
+ "logo.png",
59
+ "template/config.ru.erb",
60
+ "template/config/api_keys.yml.erb",
61
+ "template/config/backend.rb",
62
+ "template/config/encryption.yml.erb",
63
+ "template/config/sessions.yml.erb",
64
+ "test/granter_test.rb",
65
+ "test/ldap_backend_test.rb",
66
+ "test/server_test.rb",
67
+ "test/teststrap.rb",
68
+ "test/ticket_test.rb"
69
+ ]
70
+ s.homepage = %q{http://github.com/gabrielg/kennedy}
71
+ s.rdoc_options = ["--charset=UTF-8"]
72
+ s.require_paths = ["lib"]
73
+ s.rubygems_version = %q{1.3.5}
74
+ s.summary = %q{A simple single-sign-on client and server library.}
75
+ s.test_files = [
76
+ "test/granter_test.rb",
77
+ "test/ldap_backend_test.rb",
78
+ "test/server_test.rb",
79
+ "test/teststrap.rb",
80
+ "test/ticket_test.rb"
81
+ ]
82
+
83
+ if s.respond_to? :specification_version then
84
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
85
+ s.specification_version = 3
86
+
87
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
88
+ s.add_development_dependency(%q<riot>, [">= 0"])
89
+ s.add_development_dependency(%q<maruku>, [">= 0"])
90
+ s.add_development_dependency(%q<yard>, [">= 0"])
91
+ s.add_runtime_dependency(%q<ruby-net-ldap>, [">= 0"])
92
+ s.add_runtime_dependency(%q<json>, [">= 0"])
93
+ s.add_runtime_dependency(%q<sinatra>, [">= 0"])
94
+ s.add_runtime_dependency(%q<rack>, [">= 0"])
95
+ else
96
+ s.add_dependency(%q<riot>, [">= 0"])
97
+ s.add_dependency(%q<maruku>, [">= 0"])
98
+ s.add_dependency(%q<yard>, [">= 0"])
99
+ s.add_dependency(%q<ruby-net-ldap>, [">= 0"])
100
+ s.add_dependency(%q<json>, [">= 0"])
101
+ s.add_dependency(%q<sinatra>, [">= 0"])
102
+ s.add_dependency(%q<rack>, [">= 0"])
103
+ end
104
+ else
105
+ s.add_dependency(%q<riot>, [">= 0"])
106
+ s.add_dependency(%q<maruku>, [">= 0"])
107
+ s.add_dependency(%q<yard>, [">= 0"])
108
+ s.add_dependency(%q<ruby-net-ldap>, [">= 0"])
109
+ s.add_dependency(%q<json>, [">= 0"])
110
+ s.add_dependency(%q<sinatra>, [">= 0"])
111
+ s.add_dependency(%q<rack>, [">= 0"])
112
+ end
113
+ end
114
+
@@ -0,0 +1,2 @@
1
+ require 'kennedy/granter'
2
+ require 'kennedy/backends/ldap'
@@ -0,0 +1,35 @@
1
+ module Kennedy
2
+ module Backends
3
+ class LDAP
4
+ attr_writer :filter
5
+
6
+ # Creates a new LDAP auth backend with the given arguments
7
+ # @param [Hash] args The arguments to construct the backend with
8
+ # @option args [String] :host The LDAP server host to connect to
9
+ # @option args [Hash] :auth The auth method ruby-net-ldap should use
10
+ # @option args [String] :base The treebase to check against
11
+ def initialize(args = {})
12
+ @host = args[:host] || raise(ArgumentError, "Host must be given as :host")
13
+ @auth = args[:auth] || raise(ArgumentError, "Auth must be given as :auth")
14
+ @base = args[:base] || raise(ArgumentError, "Base must be given as :base")
15
+ @filter = lambda { raise(ArgumentError, "Set a filter block on this object using the 'filter' writer") }
16
+ end
17
+
18
+ # Authenticates the given credentials against LDAP
19
+ # @param [String] identifier The identifier to filter on
20
+ # @param [String] password The password to use
21
+ # @return [true, false] A boolean indicating authentication success
22
+ def authenticate(identifier, password)
23
+ filter_string = @filter.call(identifier)
24
+ !!ldap_conn.bind_as(:filter => filter_string, :password => password)
25
+ end
26
+
27
+ private
28
+
29
+ def ldap_conn
30
+ @ldap_conn ||= Net::LDAP.new(:host => @host, :auth => @auth, :base => @base)
31
+ end
32
+
33
+ end # LDAP
34
+ end # Backends
35
+ end # Kennedy
@@ -0,0 +1,69 @@
1
+ require 'pathname'
2
+ require 'erb'
3
+ require 'fileutils'
4
+ require 'digest/sha1'
5
+
6
+ module Kennedy
7
+ class Generator
8
+ TemplateDir = (Pathname(__FILE__).parent.parent.parent + "template").expand_path
9
+ TemplateType = ".erb"
10
+
11
+ def run(arguments)
12
+ @app_name = arguments.first
13
+ raise ArgumentError, "An app name must be given" if @app_name.nil? || @app_name.empty?
14
+ create_destination_directory
15
+ copy_files
16
+ end
17
+
18
+ private
19
+
20
+ def create_destination_directory
21
+ @dest = Pathname(@app_name)
22
+ log_create(@dest)
23
+ @dest.mkpath
24
+ end
25
+
26
+ def copy_files
27
+ Pathname.glob("#{template_dir}/**/*").each do |pn|
28
+ next if pn.directory?
29
+ relative = pn.relative_path_from(TemplateDir)
30
+ dest_path = @dest + relative
31
+ unless dest_path.dirname.exist?
32
+ log_create(dest_path.dirname)
33
+ dest_path.dirname.mkpath
34
+ end
35
+ is_template?(pn) ? evaluate_and_write_template(pn, dest_path) : copy_file(pn, dest_path)
36
+ end
37
+ end
38
+
39
+ def is_template?(path)
40
+ Pathname(path).extname == template_type
41
+ end
42
+
43
+ def evaluate_and_write_template(path, dest)
44
+ dest = dest.sub(/#{Regexp.escape(dest.extname)}$/, "")
45
+ log_create(dest)
46
+ dest.open('w') do |f|
47
+ f << ERB.new(path.read).result(binding)
48
+ end
49
+ end
50
+
51
+ def copy_file(path, dest)
52
+ log_create(dest)
53
+ FileUtils.cp_r(path.expand_path.to_s, dest.expand_path.to_s)
54
+ end
55
+
56
+ def template_type
57
+ TemplateType
58
+ end
59
+
60
+ def template_dir
61
+ TemplateDir
62
+ end
63
+
64
+ def log_create(path)
65
+ puts "Creating '#{path}'"
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,52 @@
1
+ require 'kennedy/ticket'
2
+
3
+ module Kennedy
4
+ # Granter is used to authenticate credentials and grant tickets to services
5
+ # once a client has been authenticated.
6
+ class Granter
7
+
8
+ # @param [Hash] args The arguments to create the granter with
9
+ # @option args [String] :iv The AES-256 initialization vector to use for encryption and decryption
10
+ # @option args [String] :passphrase The AES-256 passphrase to use for encryption and decryption
11
+ # @option args [Object] :backend An instance of a backend to use for authentication
12
+ def initialize(args = {})
13
+ @iv = args[:iv] || raise(ArgumentError, "Encryption IV must be given as :iv")
14
+ @passphrase = args[:passphrase] || raise(ArgumentError, "Encryption passphrase must be given as :passphrase")
15
+ @backend = args[:backend] || raise(ArgumentError, "Authentication backend must be given as :backend")
16
+ end
17
+
18
+ # Authenticates the given credentials against the current backend
19
+ # @param [Hash] args The arguments to authenticate with
20
+ # @option args [String] :identifier The identifier (email address, for example) to use for authentication
21
+ # @option args [String] :password The password to use for authentication
22
+ # @return [true, false] A boolean indication of whether authentication was successful or not
23
+ def authenticate(args = {})
24
+ !!@backend.authenticate(args[:identifier], args[:password])
25
+ end
26
+
27
+ # Generates a ticket object to pass back to clients requesting authentication
28
+ # @param [Hash] args The arguments to generate the ticket with
29
+ # @option args [String] :identifier The identifier (email address, for example) the ticket grants access for
30
+ # @return [Kennedy::Ticket] A Kennedy::Ticket object
31
+ def generate_ticket(args = {})
32
+ identifier = args[:identifier] || raise(ArgumentError, "An identifier must be given as :identifier")
33
+ new_ticket(identifier)
34
+ end
35
+
36
+ def read_ticket(args = {})
37
+ data = args[:data] || raise(ArgumentError, "Data must be given as :data")
38
+ decrypt_ticket(args[:data])
39
+ end
40
+
41
+ private
42
+
43
+ def decrypt_ticket(data)
44
+ Kennedy::Ticket.from_encrypted(:data => data, :iv => @iv, :passphrase => @passphrase)
45
+ end
46
+
47
+ def new_ticket(identifier)
48
+ Kennedy::Ticket.create(:identifier => identifier, :iv => @iv, :passphrase => @passphrase)
49
+ end
50
+
51
+ end # Granter
52
+ end
@@ -0,0 +1,58 @@
1
+ require 'yaml'
2
+ require 'pathname'
3
+
4
+ module Kennedy
5
+ class InstanceConfiguration
6
+ attr_reader :backend, :encryption, :api_keys
7
+
8
+ RequiredFiles = %w[backend.rb
9
+ encryption.yml
10
+ sessions.yml
11
+ api_keys.yml]
12
+
13
+ class << self
14
+ private :new
15
+ end
16
+
17
+ def initialize(config_dir)
18
+ @config_dir = config_dir
19
+ load_backend
20
+ load_encryption
21
+ load_sessions
22
+ load_api_keys
23
+ end
24
+
25
+ def self.load_config(config_dir)
26
+ config_dir = Pathname(config_dir)
27
+ raise ArgumentError, "Config dir '#{config_dir}' does not exist" unless config_dir.exist?
28
+ RequiredFiles.each do |rf|
29
+ expected = config_dir + rf
30
+ raise ArgumentError, "Expected config file '#{expected}' to exist" unless expected.exist?
31
+ end
32
+ new(config_dir)
33
+ end
34
+
35
+ def session_secret
36
+ @sessions['secret']
37
+ end
38
+
39
+ private
40
+
41
+ def load_backend
42
+ @backend = eval(File.read(@config_dir + "backend.rb"))
43
+ end
44
+
45
+ def load_encryption
46
+ @encryption = YAML.load_file(@config_dir + "encryption.yml")
47
+ end
48
+
49
+ def load_sessions
50
+ @sessions = YAML.load_file(@config_dir + "sessions.yml")
51
+ end
52
+
53
+ def load_api_keys
54
+ @api_keys = YAML.load_file(@config_dir + "api_keys.yml")
55
+ end
56
+
57
+ end # InstanceConfiguration
58
+ end # Kennedy
@@ -0,0 +1,164 @@
1
+ require 'sinatra/base'
2
+ require 'json'
3
+ require 'base64'
4
+ require 'rack/session/cookie'
5
+ require 'kennedy'
6
+
7
+ module Kennedy
8
+ class Server < Sinatra::Base
9
+ disable :session
10
+
11
+ # Creates a new subclass of Kennedy::Server with the given options
12
+ # @param [Hash] opts The options to use when building the subclass
13
+ # @option opts [Hash] :encryption The IV and passphrase to use when generating tickets, given as
14
+ # :iv and :passphrase keys in a Hash.
15
+ # @option opts [Object] :backend An instance of a backend to use for authentication.
16
+ # @option opts [String] :session_secret A secret for Rack::Session::Cookie to use when generating session
17
+ # cookies.
18
+ # @option opts [Hash] :api_keys A hash of key-value pairs to use as API users/keys with HTTP basic
19
+ # authentication
20
+ #
21
+ def self.create(opts = {})
22
+ sc = Class.new(self)
23
+ sc.instance_eval do
24
+ opts.each { |k,v| set k.to_sym, v }
25
+ raise ArgumentError, "A session secret must be set with :session_secret" unless defined?(session_secret)
26
+ add_cookie_middleware
27
+ set :api_keys, (defined?(api_keys) ? api_keys : {})
28
+ set :require_ssl, (defined?(require_ssl) ? require_ssl : true)
29
+ end
30
+ end
31
+
32
+ # Ensures all connections come in over SSL
33
+ before do
34
+ next unless require_ssl?
35
+ unless (request.env['HTTP_X_FORWARDED_PROTO'] || request.env['rack.url_scheme']) == 'https'
36
+ halt 403, "Only SSL connections are accepted."
37
+ end
38
+ end
39
+
40
+ # Ensures all connections come in requesting JSON
41
+ before do
42
+ unless request.content_type == 'application/json'
43
+ halt 415, "Only JSON requests are accepted."
44
+ end
45
+ end
46
+
47
+ # Parses request body as JSON
48
+ before do
49
+ begin
50
+ @json = JSON.parse(request.body.read)
51
+ rescue
52
+ @json = {}
53
+ end
54
+ end
55
+
56
+ # Takes incoming requests with a 'ticket' property in the JSON body and decrypts the ticket,
57
+ # returning an identifier as the 'identifier' property in the JSON response body if the
58
+ # ticket is valid and unexpired.
59
+ post "/validation_request" do
60
+ content_type "application/json"
61
+ require_api_authentication
62
+ begin
63
+ encrypted_ticket = Base64.decode64(@json['ticket'])
64
+ ticket = granter.read_ticket(:data => encrypted_ticket)
65
+ if ticket.expired?
66
+ [406, {'error' => 'expired_ticket'}.to_json]
67
+ else
68
+ [200, {'identifier' => ticket.identifier}.to_json]
69
+ end
70
+ rescue => e
71
+ [406, {'error' => 'bad_ticket'}.to_json]
72
+ end
73
+ end
74
+
75
+ # Takes incoming requests and generates an encrypted and Base64 encoded ticket in the 'ticket'
76
+ # property of the JSON response if the user has a valid session.
77
+ get '/session' do
78
+ content_type "application/json"
79
+ if session['identifier']
80
+ ticket = granter.generate_ticket(:identifier => session['identifier'])
81
+ [200, {'ticket' => Base64.encode64(ticket.to_encrypted)}.to_json]
82
+ else
83
+ [401, {'error' => 'authentication_required'}.to_json]
84
+ end
85
+ end
86
+
87
+ # Creates a session if authentication with the given credentials passed as 'identifier' and
88
+ # 'password' in the JSON body is successful.
89
+ post '/session' do
90
+ credentials = @json['credentials']
91
+ content_type "application/json"
92
+ if credentials.nil? || !granter.authenticate(:identifier => credentials['identifier'], :password => credentials['password'])
93
+ [406, {'error' => 'bad_credentials'}.to_json]
94
+ else
95
+ session['identifier'] = credentials['identifier']
96
+ [201, {'success' => 'session_created'}.to_json]
97
+ end
98
+ end
99
+
100
+ # Destroys an existing session.
101
+ delete '/session' do
102
+ request.session.clear
103
+ [200, {'success' => 'session_destroyed'}.to_json]
104
+ end
105
+
106
+ private
107
+
108
+ def auth
109
+ @auth ||= Rack::Auth::Basic::Request.new(request.env)
110
+ end
111
+
112
+ def authorized?
113
+ request.env['REMOTE_USER']
114
+ end
115
+
116
+ def unauthorized!(realm = "Kennedy")
117
+ headers 'WWW-Authenticate' => %(Basic realm="#{realm}")
118
+ throw :halt, [401, {'error' => 'authentication_required'}.to_json]
119
+ end
120
+
121
+ def bad_request!
122
+ throw :halt, [400, {'error' => 'bad_request'}.to_json]
123
+ end
124
+
125
+ def authorize(username, password)
126
+ api_keys.has_key?(username) && api_keys[username] == password
127
+ end
128
+
129
+ def require_api_authentication
130
+ return if authorized?
131
+ unauthorized! unless auth.provided?
132
+ bad_request! unless auth.basic?
133
+ unauthorized! unless authorize(*auth.credentials)
134
+ request.env['REMOTE_USER'] = auth.username
135
+ end
136
+
137
+ def self.add_cookie_middleware
138
+ use Rack::Session::Cookie, :secret => session_secret
139
+ end
140
+
141
+ def granter
142
+ @granter ||= Kennedy::Granter.new(:iv => encryption[:iv], :passphrase => encryption[:passphrase],
143
+ :backend => backend)
144
+ end
145
+
146
+ def encryption
147
+ self.class.encryption
148
+ end
149
+
150
+ def backend
151
+ self.class.backend
152
+ end
153
+
154
+ def api_keys
155
+ self.class.api_keys
156
+ end
157
+
158
+ def require_ssl?
159
+ !!options.require_ssl
160
+ end
161
+
162
+ end # Server
163
+ end # Kennedy
164
+