kraut 0.5.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/.gitignore +11 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +3 -0
  4. data/README.md +175 -0
  5. data/Rakefile +10 -0
  6. data/app/controllers/kraut/sessions_controller.rb +30 -0
  7. data/app/models/kraut/session.rb +67 -0
  8. data/app/views/kraut/sessions/new.html.haml +15 -0
  9. data/autotest/discover.rb +1 -0
  10. data/config/initializers/savon.rb +12 -0
  11. data/config/locales/kraut.yml +14 -0
  12. data/config/routes.rb +5 -0
  13. data/kraut.gemspec +43 -0
  14. data/lib/kraut.rb +3 -0
  15. data/lib/kraut/application.rb +31 -0
  16. data/lib/kraut/client.rb +63 -0
  17. data/lib/kraut/kraut.rb +21 -0
  18. data/lib/kraut/mapper.rb +20 -0
  19. data/lib/kraut/principal.rb +85 -0
  20. data/lib/kraut/rails/authentication.rb +80 -0
  21. data/lib/kraut/rails/engine.rb +29 -0
  22. data/lib/kraut/rails/spec/login_helper.rb +28 -0
  23. data/lib/kraut/rails/spec/protected_action.rb +68 -0
  24. data/lib/kraut/rails/spec/user_helper.rb +27 -0
  25. data/lib/kraut/rails/spec_helper.rb +15 -0
  26. data/lib/kraut/version.rb +6 -0
  27. data/spec/controllers/application_controller_spec.rb +219 -0
  28. data/spec/controllers/sessions_controller_spec.rb +106 -0
  29. data/spec/fixtures/authenticate_application/invalid_app.xml +11 -0
  30. data/spec/fixtures/authenticate_application/invalid_password.xml +11 -0
  31. data/spec/fixtures/authenticate_application/success.xml +10 -0
  32. data/spec/fixtures/authenticate_principal/application_access_denied.xml +11 -0
  33. data/spec/fixtures/authenticate_principal/invalid_password.xml +11 -0
  34. data/spec/fixtures/authenticate_principal/invalid_user.xml +11 -0
  35. data/spec/fixtures/authenticate_principal/success.xml +7 -0
  36. data/spec/fixtures/find_principal_by_token/invalid_token.xml +11 -0
  37. data/spec/fixtures/find_principal_by_token/success.xml +39 -0
  38. data/spec/fixtures/find_principal_with_attributes_by_name/invalid_user.xml +11 -0
  39. data/spec/fixtures/find_principal_with_attributes_by_name/success.xml +69 -0
  40. data/spec/fixtures/is_group_member/not_in_group.xml +8 -0
  41. data/spec/fixtures/is_group_member/success.xml +8 -0
  42. data/spec/kraut/application_spec.rb +99 -0
  43. data/spec/kraut/client_spec.rb +101 -0
  44. data/spec/kraut/mapper_spec.rb +48 -0
  45. data/spec/kraut/principal_spec.rb +142 -0
  46. data/spec/models/session_spec.rb +148 -0
  47. data/spec/rails/engine_spec.rb +24 -0
  48. data/spec/spec_helper.rb +33 -0
  49. data/spec/views/sessions/new.html.haml_spec.rb +11 -0
  50. metadata +237 -0
@@ -0,0 +1,11 @@
1
+ .DS_Store
2
+ .yardoc
3
+ doc
4
+ coverage
5
+ tmp
6
+ *~
7
+ *.gem
8
+ .bundle
9
+ Gemfile.lock
10
+ spec/reports
11
+ *.log
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ # -*- encoding : utf-8 -*-
2
+ source :rubygems
3
+ gemspec
@@ -0,0 +1,175 @@
1
+ Kraut
2
+ =====
3
+
4
+ Interface for the [Atlassian Crowd](http://www.atlassian.com/software/crowd/) SOAP service.
5
+
6
+ Crowd endpoint
7
+ --------------
8
+
9
+ Kraut needs to know the SOAP endpoint of your Crowd installation. Set it via:
10
+
11
+ ```ruby
12
+ Kraut.endpoint = "http://example.com/crowd/services/SecurityServer"
13
+ ```
14
+
15
+ Kraut::Application
16
+ ------------------
17
+
18
+ Crowd manages principals and applications. `Kraut::Application` obviously represents the latter.
19
+ To authenticate your application with Crowd, you need to provide its name and password:
20
+
21
+ ```ruby
22
+ Kraut::Application.authenticate "my_app", "secret"
23
+ ```
24
+
25
+ After being authenticated, you can access the following attributes:
26
+
27
+ ```ruby
28
+ Kraut::Application.name => "my_app"
29
+ Kraut::Application.password => "secret"
30
+ Kraut::Application.token => "Dem7p7Ns97uRV92so4IE1h10"
31
+ ```
32
+
33
+ Kraut stores the time of the latest authentication:
34
+
35
+ ```ruby
36
+ Kraut::Application.authenticated_at # => Mon Jan 10 16:35:58 +0100 2011
37
+ ```
38
+
39
+ To check whether the application needs to (re-)authenticate itself, you can use the following method:
40
+
41
+ ```ruby
42
+ Kraut::Application.authentication_required?(timeout = 10) # defaults to 10 minutes
43
+ ```
44
+
45
+ Kraut::Principal
46
+ ----------------
47
+
48
+ Represents a Crowd principal. To authenticate a principal:
49
+
50
+ ```ruby
51
+ Kraut::Principal.authenticate "user", "password"
52
+ ```
53
+
54
+ The `.authenticate` method returns a `Kraut::Principal` instance with basic attributes:
55
+
56
+ * #name => "user"
57
+ * #password => "password"
58
+ * #token => "3p7Xs3dIuTVb2pO4II1h8A"
59
+
60
+ It also contains the following attributes:
61
+
62
+ * #display_name => "Chuck Norris"
63
+ * #email => "chuck.norris@gmail.com"
64
+ * #attributes => { :display_name => "Chuck Norris", ... }
65
+
66
+ Make sure to verify whether a principal's password is expired. Principal's with an expired password are
67
+ still able to authenticate and access your application.
68
+
69
+ ```ruby
70
+ Kraut::Principal.requires_password_change?
71
+ ```
72
+
73
+ ### Groups
74
+
75
+ To verify whether a principal belongs to a certain group:
76
+
77
+ ```ruby
78
+ Kraut::Principal#member_of?(group)
79
+ ```
80
+
81
+ Kraut stores all positive and negative group-requests in a Hash:
82
+
83
+ ```ruby
84
+ Kraut::Principal#groups => { "staff" => true, "supervisor" => false }
85
+ ```
86
+
87
+ Login
88
+ -----
89
+
90
+ In order to provide easy login to your apps, just require 'kraut/rails/engine' instead of just 'kraut':
91
+
92
+ ```ruby
93
+ gem "kraut", :require => "kraut/rails/engine"
94
+ ```
95
+
96
+ Then, you'll have a login controller unter '/sessions/new'. To configure its behaviour, add it in 'config/initializers/kraut.rb':
97
+
98
+ ```ruby
99
+ Kraut.endpoint = AppConfig.webservices.crowd.baseaddress
100
+ # the layout to use for the login page
101
+ Kraut::Rails::Engine.config.layout = "application"
102
+ # hash containing user and password for authenticatin the crowd app
103
+ Kraut::Rails::Engine.config.webservice = AppConfig.webservices.crowd
104
+ # hash containing :action => [crowd_group1, crowd_group2] pairs
105
+ Kraut::Rails::Engine.config.authorizations = AppConfig.authorizations
106
+ # starting url after authentication
107
+ Kraut::Rails::Engine.config.entry_url = "/"
108
+ ```
109
+
110
+ In your controllers, you have three methods to use as before_filter:
111
+
112
+ * `check_for_crowd_token` => checks for `params[:crowd_token]` and logs in with that token
113
+ * `verify_login` => checks whether a user is logged in and redirects to the login page if necessary
114
+ * `verify_access` => checks whether the logged in user has access to the current action
115
+
116
+ `verify_access` uses the `Kraut::Rails::Engine.config.authorizations` hash. It checks for controller-action actions (eg :orders_show). If a controller action protected by `verify_access` isn't listed there, no one can access this action!
117
+
118
+ In your controllers and views, you can access user specific methods:
119
+
120
+ * `logged_in?` => checks whether someone is logged in
121
+ * `user` => returns the currently logged in user (or nil)
122
+ * `allowed_to?` => checks whether someone is logged in and this user has access to the given action (see `Kraut::Rails::Engine.config.authorizations` above)
123
+
124
+ Testing authentication/authorization behaviour
125
+ ----------------------------------------------
126
+
127
+ In your spec_helper.rb:
128
+
129
+ ```ruby
130
+ require "kraut/rails/spec_helper"
131
+ ```
132
+
133
+ Then you have in all your specs:
134
+
135
+ * `create_user` => creates a new user to spec against
136
+
137
+ And in your controller/view/helper specs:
138
+
139
+ * `login!` => log in with a newly created user
140
+ * `logout!` => log out again
141
+ * `user` => user you're logged in with
142
+
143
+ And finally in your controller specs:
144
+
145
+ * `describe_protected_action` => tests an action protected by `verify_login`/`verify_access`
146
+
147
+ Example:
148
+
149
+ ```ruby
150
+ describe_protected_action "GET :show", :orders_index do
151
+ unauthorized_request { get :show, :id => "5" }
152
+
153
+ before do
154
+ @order = Order.create
155
+ end
156
+
157
+ it "should be successful" do
158
+ get :index, :id => @order.id
159
+ response.should be_success
160
+ assigns(:order).should == @order
161
+ end
162
+ end
163
+ ```
164
+
165
+ This runs three tests:
166
+
167
+ * the test written in the block above that checks whether the response is a success when logged with a user allowed to do :orders_index
168
+ * an automatically generated test that checks that you're redirected to the login page when logged in with a user not allowed to do :orders_index
169
+ * an automatically generated test that checks that you're redirected to the login page when not logged in
170
+
171
+ If you leave out the `action` parameter (:orders_index in the example), the first test only checks with a logged in user and the second test is omitted.
172
+
173
+ If you leave out the `unauthorized_request`, the second and third test are omitted and only the successful tests are executed.
174
+
175
+ `unauthorized_request` is run outside the scope of the `describe_protected_action` block, so you can't access stuff initialized within it's before block (eg the @order above).
@@ -0,0 +1,10 @@
1
+ require "rake"
2
+
3
+ require "rspec/core/rake_task"
4
+ require "ci/reporter/rake/rspec"
5
+
6
+ RSpec::Core::RakeTask.new do |t|
7
+ t.rspec_opts = %w(-fd -c)
8
+ end
9
+
10
+ task :default => %w(ci:setup:rspec spec)
@@ -0,0 +1,30 @@
1
+ module Kraut
2
+
3
+ class SessionsController < ActionController::Base
4
+
5
+ layout Kraut::Rails::Engine.config.layout
6
+
7
+ def new
8
+ @session = Kraut::Session.new
9
+ end
10
+
11
+ def create
12
+ @session = Kraut::Session.new params[:kraut_session]
13
+
14
+ authenticate_application
15
+ if @session.valid?
16
+ switch_user(@session)
17
+ redirect_to stored_location! || Kraut::Rails::Engine.config.entry_url
18
+ else
19
+ render :new
20
+ end
21
+ end
22
+
23
+ def destroy
24
+ reset_session
25
+ redirect_to Kraut::Rails::Engine.config.entry_url
26
+ end
27
+
28
+ end
29
+
30
+ end
@@ -0,0 +1,67 @@
1
+ module Kraut
2
+
3
+ class Session
4
+
5
+ include ActiveModel::Validations
6
+ include ActiveModel::Conversion
7
+ include Mapper
8
+
9
+ attr_accessor :username, :password, :principal
10
+ validates :username, :password, :presence => true
11
+
12
+ def name
13
+ principal.name
14
+ end
15
+
16
+ def token
17
+ principal.token
18
+ end
19
+
20
+ def allowed_to?(action)
21
+ in_group? Kraut::Rails::Engine.config.authorizations[action]
22
+ end
23
+
24
+ def in_group?(groups)
25
+ Array.wrap(groups).any? { |group| principal.member_of? group }
26
+ end
27
+
28
+ def valid?
29
+ return unless super
30
+ if self.principal.nil?
31
+ login!
32
+ else
33
+ true
34
+ end
35
+ end
36
+
37
+ def self.find_by_token(token)
38
+ self.new(:principal => Kraut::Principal.find_by_token(token))
39
+ end
40
+
41
+ def persisted?
42
+ false
43
+ end
44
+
45
+ private
46
+
47
+ def login!
48
+ self.principal = Kraut::Principal.authenticate(username, password)
49
+ valid_password?
50
+ rescue Kraut::InvalidAuthentication, Kraut::InvalidAuthorization
51
+ errors[:base] << I18n.t("errors.kraut.invalid_credentials")
52
+ false
53
+ rescue Kraut::ApplicationAccessDenied
54
+ errors[:base] << I18n.t("errors.kraut.application_access_denied")
55
+ false
56
+ end
57
+
58
+ def valid_password?
59
+ return true unless principal.requires_password_change?
60
+
61
+ errors[:base] << I18n.t("errors.kraut.password_expired")
62
+ false
63
+ end
64
+
65
+ end
66
+
67
+ end
@@ -0,0 +1,15 @@
1
+ %section
2
+ %h1 Anmelden
3
+ - if flash[:alert]
4
+ .flash.alert= flash[:alert]
5
+
6
+ = form_for @session do |f|
7
+ = f.error_messages :id => nil, :class => "errors"
8
+
9
+ %fieldset
10
+ = f.label :username
11
+ = f.text_field :username, :class => "width m"
12
+ = f.label :password
13
+ = f.password_field :password, :class => "width m"
14
+
15
+ %button(type="submit" id="session_submit") Anmelden
@@ -0,0 +1 @@
1
+ Autotest.add_discovery { "rspec2" }
@@ -0,0 +1,12 @@
1
+ # -*- encoding : utf-8 -*-
2
+ Savon::Model.handle_response = Proc.new do |response|
3
+ if response.soap_fault?
4
+ begin
5
+ if response.to_hash[:fault][:detail][:voucher_exception][:error_code] == "NOT_AUTHORIZED"
6
+ raise SecurityError
7
+ end
8
+ rescue NoMethodError
9
+ end
10
+ end
11
+ response
12
+ end if defined?(Savon::Model)
@@ -0,0 +1,14 @@
1
+ de:
2
+ activemodel:
3
+ attributes:
4
+ kraut/session:
5
+ username: "Benutzername"
6
+ password: "Passwort"
7
+ errors:
8
+ kraut:
9
+ invalid_credentials: "Benutzername und/oder Passwort ungültig"
10
+ application_access_denied: "Sie haben keinen Zugriff auf diese Anwendung"
11
+ password_expired: "Ihr Passwort ist abgelaufen"
12
+ session_expired: "Die Sitzung ist abgelaufen. Bitte melde dich erneut an."
13
+ token_not_found: "Das Authentifizierungs-Token wurde nicht gefunden! Bitte melden Sie sich neu an."
14
+ access_denied: "Auf diesen Bereich haben Sie keinen Zugriff."
@@ -0,0 +1,5 @@
1
+ Rails.application.routes.draw do
2
+
3
+ resource :sessions, :controller => "kraut/sessions", :only => [:new, :create, :destroy], :as => "kraut_sessions"
4
+
5
+ end
@@ -0,0 +1,43 @@
1
+ # -*- encoding : utf-8 -*-
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $:.unshift lib unless $:.include? lib
4
+
5
+ require "kraut/version"
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "kraut"
9
+ s.version = Kraut::VERSION
10
+ s.authors = ["Daniel Harrington", "Thilko Richter"]
11
+ s.email = "blaulabs@blau.de"
12
+ s.homepage = "http://github.com/blaulabs/#{s.name}"
13
+ s.summary = "Crowd Interface"
14
+ s.description = "Interface for the Atlassian Crowd SOAP API"
15
+
16
+ s.rubyforge_project = s.name
17
+
18
+ #savon 0.9.8 ships with Savon::Model, which does not support handle_response method used in savon initializer [mw-21.02.12]
19
+ #<= 0.9.7 is broken due to invalid dependencies [aj-18.04.12]
20
+ s.add_dependency "savon", "= 0.9.7"
21
+
22
+ # nail down gyoku until savon_spec 1.0.0 is on rubygems
23
+ # NoMethodError:
24
+ # undefined method `lower_camelcase' for "send_sms_to_any_provider":String
25
+ s.add_development_dependency "gyoku", "= 0.4.4"
26
+
27
+ s.add_development_dependency "ci_reporter", "~> 1.6.5"
28
+ s.add_development_dependency "rspec", "~> 2.5.0"
29
+ s.add_development_dependency "autotest", "~> 4.4.2"
30
+ s.add_development_dependency "mocha", "~> 0.9.9"
31
+ s.add_development_dependency "webmock", "~> 1.3.5"
32
+ s.add_development_dependency "savon_spec", "~> 0.1.6"
33
+ s.add_development_dependency "rake", "0.8.7"
34
+ s.add_development_dependency "rails", "3.0.7"
35
+ s.add_development_dependency "rspec-rails", "~> 2.5.0"
36
+ s.add_development_dependency "haml", "~> 3.0"
37
+
38
+ # ZenTest 4.6 requires RubyGems version ~> 1.8 [dh, 2011-08-19]
39
+ s.add_development_dependency "ZenTest", "4.5.0"
40
+
41
+ s.files = `git ls-files`.split("\n")
42
+ s.require_path = "lib"
43
+ end
@@ -0,0 +1,3 @@
1
+ require "kraut/version"
2
+ require "kraut/application"
3
+ require "kraut/principal"
@@ -0,0 +1,31 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require "kraut/client"
3
+
4
+ module Kraut
5
+
6
+ # = Kraut::Application
7
+ #
8
+ # Represents an application registered with Crowd.
9
+ class Application
10
+ class << self
11
+
12
+ # Authenticates an application with a given +name+ and +password+.
13
+ def authenticate(name, password)
14
+ response = Client.request :authenticate_application,
15
+ :in0 => { "aut:credential" => { "aut:credential" => password }, "aut:name" => name }
16
+
17
+ self.authenticated_at = Time.now
18
+ self.name, self.password, self.token = name, password, response[:out][:token]
19
+ end
20
+
21
+ attr_accessor :name, :password, :token, :authenticated_at
22
+
23
+ # Returns whether the application needs to (re-)authenticate itself.
24
+ # Defaults to a +timeout+ of 10 minutes.
25
+ def authentication_required?(timeout = 10)
26
+ !authenticated_at || authenticated_at < Time.now - (60 * timeout)
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,63 @@
1
+ require "savon"
2
+ require "kraut/kraut"
3
+
4
+ module Kraut
5
+
6
+ autoload :Application, "kraut/application"
7
+
8
+ # = Kraut::Client
9
+ #
10
+ # Wraps a <tt>Savon::Client</tt> and executes SOAP requests.
11
+ module Client
12
+ class << self
13
+
14
+ # Executes a SOAP request to a given +method+ with an optional +body+ Hash.
15
+ # Ensures to always raise SOAP faults if they happen and returns a response Hash.
16
+ def request(method, body = {})
17
+ response = client.request :wsdl, method do
18
+ soap.namespaces["xmlns:aut"] = Kraut.namespace
19
+ soap.body = body
20
+ end
21
+
22
+ if response.soap_fault?
23
+ handle_soap_fault response.soap_fault
24
+ else
25
+ response.to_hash["#{method}_response".to_sym]
26
+ end
27
+ rescue Savon::SOAP::Fault => soap_fault
28
+ handle_soap_fault soap_fault
29
+ end
30
+
31
+ # Executes a SOAP request to a given +method+ with an optional +body+ Hash.
32
+ # Adds application authentication credentials and delegates to the +request+ method.
33
+ def auth_request(method, body = {})
34
+ body[:in0] = { "aut:name" => Application.name, "aut:token" => Application.token }
35
+ body[:order!] = body.keys.sort_by { |key| key.to_s }
36
+ request method, body
37
+ end
38
+
39
+ # Returns a memoized <tt>Savon::Client</tt> for executing SOAP requests.
40
+ def client
41
+ @client ||= Savon::Client.new do
42
+ wsdl.endpoint = Kraut.endpoint
43
+ wsdl.namespace = "urn:SecurityServer"
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def handle_soap_fault(soap_fault)
50
+ error = case soap_fault.to_hash[:fault][:detail].keys.first
51
+ when :invalid_authentication_exception then InvalidAuthentication
52
+ when :invalid_authorization_exception then InvalidAuthorization
53
+ when :application_access_denied_exception then ApplicationAccessDenied
54
+ when :invalid_token_exception then InvalidPrincipalToken
55
+ else UnknownError
56
+ end
57
+
58
+ raise error, soap_fault.to_s
59
+ end
60
+
61
+ end
62
+ end
63
+ end