kraut 0.5.6

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.
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