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.
- data/.gitignore +11 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/README.md +175 -0
- data/Rakefile +10 -0
- data/app/controllers/kraut/sessions_controller.rb +30 -0
- data/app/models/kraut/session.rb +67 -0
- data/app/views/kraut/sessions/new.html.haml +15 -0
- data/autotest/discover.rb +1 -0
- data/config/initializers/savon.rb +12 -0
- data/config/locales/kraut.yml +14 -0
- data/config/routes.rb +5 -0
- data/kraut.gemspec +43 -0
- data/lib/kraut.rb +3 -0
- data/lib/kraut/application.rb +31 -0
- data/lib/kraut/client.rb +63 -0
- data/lib/kraut/kraut.rb +21 -0
- data/lib/kraut/mapper.rb +20 -0
- data/lib/kraut/principal.rb +85 -0
- data/lib/kraut/rails/authentication.rb +80 -0
- data/lib/kraut/rails/engine.rb +29 -0
- data/lib/kraut/rails/spec/login_helper.rb +28 -0
- data/lib/kraut/rails/spec/protected_action.rb +68 -0
- data/lib/kraut/rails/spec/user_helper.rb +27 -0
- data/lib/kraut/rails/spec_helper.rb +15 -0
- data/lib/kraut/version.rb +6 -0
- data/spec/controllers/application_controller_spec.rb +219 -0
- data/spec/controllers/sessions_controller_spec.rb +106 -0
- data/spec/fixtures/authenticate_application/invalid_app.xml +11 -0
- data/spec/fixtures/authenticate_application/invalid_password.xml +11 -0
- data/spec/fixtures/authenticate_application/success.xml +10 -0
- data/spec/fixtures/authenticate_principal/application_access_denied.xml +11 -0
- data/spec/fixtures/authenticate_principal/invalid_password.xml +11 -0
- data/spec/fixtures/authenticate_principal/invalid_user.xml +11 -0
- data/spec/fixtures/authenticate_principal/success.xml +7 -0
- data/spec/fixtures/find_principal_by_token/invalid_token.xml +11 -0
- data/spec/fixtures/find_principal_by_token/success.xml +39 -0
- data/spec/fixtures/find_principal_with_attributes_by_name/invalid_user.xml +11 -0
- data/spec/fixtures/find_principal_with_attributes_by_name/success.xml +69 -0
- data/spec/fixtures/is_group_member/not_in_group.xml +8 -0
- data/spec/fixtures/is_group_member/success.xml +8 -0
- data/spec/kraut/application_spec.rb +99 -0
- data/spec/kraut/client_spec.rb +101 -0
- data/spec/kraut/mapper_spec.rb +48 -0
- data/spec/kraut/principal_spec.rb +142 -0
- data/spec/models/session_spec.rb +148 -0
- data/spec/rails/engine_spec.rb +24 -0
- data/spec/spec_helper.rb +33 -0
- data/spec/views/sessions/new.html.haml_spec.rb +11 -0
- metadata +237 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -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).
|
data/Rakefile
ADDED
@@ -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."
|
data/config/routes.rb
ADDED
data/kraut.gemspec
ADDED
@@ -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
|
data/lib/kraut.rb
ADDED
@@ -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
|
data/lib/kraut/client.rb
ADDED
@@ -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
|