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