openam_auth 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9cf4acae2226611b478dd5bf2f992b71372f94e9
4
+ data.tar.gz: 98d65914ba32096233b213bbcc80501aa1478429
5
+ SHA512:
6
+ metadata.gz: 6d102e557c3cf6fa977581859e7ef67a4a7f5d48dc0af78af711eb45f652aac57c22ff8738beda6b30a1fca9c1dd75db5440bdc94104416958cc19bed3bfce94
7
+ data.tar.gz: 5df3aeaf18e671355783c058e3c1666c784e5e9dee2d4a52ef6dc44187b56c6474a72d1dc821ff6c20a91381bb3c12ed9d6f229180f885b24b3cb63d56c188ef
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in openam_auth.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 sameera207
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # OpenamAuth
2
+
3
+ ruby authentication client for forgerock OpenAM server (http://forgerock.com/products/open-identity-stack/openam/), this ruby client will work with OpenAM Policy Agent for Nginx https://bitbucket.org/hamano/nginx-mod-am
4
+
5
+ Read more about OpenAM REST api
6
+
7
+ https://wikis.forgerock.org/confluence/display/openam/Use+OpenAM+RESTful+Services
8
+
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ gem 'openam_auth'
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install openam_auth
23
+
24
+ ## Running tests
25
+
26
+ rspec
27
+
28
+ ## Usage
29
+
30
+ ### Using with Devise
31
+
32
+ 1. install the gem (as described above)
33
+ 2. create a file in `config/initializers`
34
+
35
+ ```ruby
36
+ #config/initializers/openam_config.rb
37
+ OpenamConfig.config do
38
+ openam_url <Path to your openam server>
39
+ end
40
+ ```
41
+ 3. **openam_auth assumes** you have a `User` model in you project and it has the following two methods implemented
42
+
43
+
44
+ ```ruby
45
+
46
+ class User
47
+
48
+ # sends the token (from openam server) as a parameter
49
+ def self.existing_user_by_token(token)
50
+ # this method should return a user object, if matching record found
51
+ # or nil if there are not matching record found
52
+ end
53
+
54
+ # authentication token from OpenAm server
55
+ # user hash
56
+ # ex: { "sn" => ["admin"] }
57
+ # NOTE: hash will have the key and value array
58
+ def self.update_openam_user(token, hash)
59
+ #this method should either update the existing user token, if user found
60
+ #or create a new user and return that user
61
+ end
62
+
63
+ end
64
+
65
+ ```
66
+
67
+ **Note**, you may want to add some more columns for the existing `users` table to accomodate the values passed by the hash. Read the section *3.5. Token Validation, Attribute Retrieval* http://openam.forgerock.org/openam-documentation/openam-doc-source/doc/dev-guide/#rest-api-auth for more info.
68
+
69
+
70
+
71
+
72
+ 4. in your controller , `Ex: ApplicationController`, include the `OpenamAuth::Authenticate` module
73
+
74
+ ```ruby
75
+ class ApplicationController < ActionController::Base
76
+ include OpenamAuth::Authenticate
77
+ end
78
+ ```
79
+
80
+ 5 . Finally implement the before_filter for `authenticate_user!`
81
+
82
+ ```ruby
83
+ before_filter :authenticate_user!
84
+ ```
85
+ 6 . For `logout` you could use `openam_logout` with current user token from your controller
86
+
87
+ ```ruby
88
+ openam_logout(<current user token>)
89
+ ```
90
+
91
+ ### Special thanks :)
92
+
93
+ https://github.com/tychobrailleur/openam-sample
94
+
95
+ http://speakmy.name/2011/05/29/simple-configuration-for-ruby-apps/
96
+
97
+ http://say26.com/rspec-testing-controllers-outside-of-a-rails-application
98
+
99
+
100
+ ## Contributing
101
+
102
+ 1. Fork it
103
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
104
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
105
+ 4. Push to the branch (`git push origin my-new-feature`)
106
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,46 @@
1
+ module OpenamAuth
2
+ module Authenticate
3
+ module ClassMethods ; end
4
+
5
+ module InstanceMethods
6
+
7
+ def authenticate_user!
8
+ status = false
9
+ openam = OpenamAuth::Openam.new
10
+ cookie_name = openam.cookie_name
11
+ token = openam.token_cookie(request, cookie_name)
12
+
13
+ #The following class method shold be implemented by the parent application
14
+ user = User.existing_user_by_token(token)
15
+
16
+ unless user
17
+ if openam.valid_token?(token)
18
+ response = openam.openam_user(cookie_name, token)
19
+ user_hash = openam.user_hash(response.body)
20
+ #following class method should be implemented by the parent application
21
+ user = User.update_openam_user(token, user_hash)
22
+ end
23
+ end
24
+ if user
25
+ session[:user_id] = user.id
26
+ status = true
27
+ end
28
+ status ? true : (redirect_to openam.login_url)
29
+ end
30
+ end
31
+
32
+ def openam_logout(token)
33
+ OpenamAuth::Openam.new.logout(token)
34
+ end
35
+
36
+ def current_user
37
+ User.where(id: session[:user_id]).first
38
+ end
39
+
40
+ def self.included(receiver)
41
+ receiver.extend ClassMethods
42
+ receiver.send :include, InstanceMethods
43
+ end
44
+ end
45
+ end
46
+
@@ -0,0 +1,64 @@
1
+ require 'httparty'
2
+
3
+ module OpenamAuth
4
+ class Openam
5
+ include HTTParty
6
+
7
+ COOKIE_NAME_FOR_TOKEN = "/identity/getCookieNameForToken"
8
+ IS_TOKEN_VALID = "/identity/isTokenValid"
9
+ USER_ATTRIBUTES = "/identity/attributes"
10
+ LOGIN_URL = "/UI/Login?goto="
11
+ LOGOUT_URL = "/identity/logout"
12
+
13
+
14
+ def initialize
15
+ @base_url = OpenamConfig.openam_url
16
+ end
17
+
18
+ def cookie_name
19
+ response = self.class.post("#{@base_url}#{COOKIE_NAME_FOR_TOKEN}", {})
20
+ response.body.split('=').last.strip
21
+ end
22
+
23
+ def token_cookie(request, cookie_name)
24
+ token_cookie = CGI.unescape(request.cookies.fetch(cookie_name, nil).to_s.gsub('+', '%2B'))
25
+ token_cookie != '' ? token_cookie : nil
26
+ end
27
+
28
+ def valid_token?(token)
29
+ response = self.class.get("#{@base_url}#{IS_TOKEN_VALID}?tokenid=#{token}", {})
30
+ response.body.split('=').last.strip == 'true'
31
+ end
32
+
33
+ def openam_user(token_cookie_name, token)
34
+ self.class.cookies({ token_cookie_name => token })
35
+ self.class.post("#{@base_url}#{USER_ATTRIBUTES}", {:subjectid => token})
36
+ end
37
+
38
+ def login_url
39
+ "#{@base_url}#{LOGIN_URL}"
40
+ end
41
+
42
+ def logout(token)
43
+ self.class.get("#{@base_url}#{LOGOUT_URL}?subjectid=#{token}", {})
44
+ end
45
+
46
+ def user_hash(response)
47
+ opensso_user = Hash.new{ |h,k| h[k] = Array.new }
48
+ attribute_name = ''
49
+ lines = response.split(/\n/)
50
+ lines.each do |line|
51
+ line = line.strip
52
+ if line.match(/^userdetails.attribute.name=/)
53
+ attribute_name = line.gsub(/^userdetails.attribute.name=/, '').strip
54
+ elsif line.match(/^userdetails.attribute.value=/)
55
+ opensso_user[attribute_name] << line.gsub(/^userdetails.attribute.value=/, '').strip
56
+ end
57
+ end
58
+ opensso_user
59
+ end
60
+
61
+
62
+ end
63
+ end
64
+
@@ -0,0 +1,23 @@
1
+ module OpenamConfig
2
+ extend self
3
+
4
+ def parameter(*names)
5
+ names.each do |name|
6
+ attr_accessor name
7
+
8
+ define_method name do |*values|
9
+ value = values.first
10
+ value ? self.send("#{name}=", value) : instance_variable_get("@#{name}")
11
+ end
12
+ end
13
+ end
14
+
15
+ def config(&block)
16
+ instance_eval &block
17
+ end
18
+
19
+ end
20
+
21
+ OpenamConfig.config do
22
+ parameter :openam_url
23
+ end
@@ -0,0 +1,3 @@
1
+ module OpenamAuth
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,5 @@
1
+ require "openam_auth/version"
2
+ require "openam_auth/openam"
3
+ require "openam_auth/authenticate"
4
+ require "openam_auth/openam_config"
5
+
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'openam_auth/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "openam_auth"
8
+ spec.version = OpenamAuth::VERSION
9
+ spec.authors = ["sameera207"]
10
+ spec.email = ["sameera207@gmail.com"]
11
+ spec.description = %q{ruby authentication client for OpenAm}
12
+ spec.summary = %q{ruby authentication client for forgerock OpenAM}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rspec-rails"
23
+ spec.add_development_dependency "webmock"
24
+ spec.add_development_dependency "actionpack"
25
+ spec.add_development_dependency "activesupport"
26
+ spec.add_development_dependency "httparty"
27
+ end
@@ -0,0 +1,22 @@
1
+ require 'active_support/all'
2
+ require 'action_controller'
3
+ require 'action_dispatch'
4
+
5
+ module Rails
6
+ class App
7
+ def env_config; {} end
8
+ def routes
9
+ return @routes if defined?(@routes)
10
+ @routes = ActionDispatch::Routing::RouteSet.new
11
+ @routes.draw do
12
+ resources :posts
13
+ end
14
+ @routes
15
+ end
16
+ end
17
+
18
+ def self.application
19
+ @app ||= App.new
20
+ end
21
+ end
22
+
@@ -0,0 +1,29 @@
1
+ require 'openam_auth'
2
+
3
+ class User
4
+
5
+ attr_accessor :id, :name
6
+
7
+ def self.existing_user_by_token(token)
8
+ nil
9
+ end
10
+
11
+ def self.update_openam_user(token, user_hash)
12
+ User.new
13
+ end
14
+
15
+ end
16
+
17
+ class ApplicationController < ActionController::Base
18
+ include Rails.application.routes.url_helpers
19
+
20
+ def render(*attributes); end
21
+ end
22
+
23
+ class PostsController < ApplicationController
24
+ include OpenamAuth::Authenticate
25
+
26
+ before_filter :authenticate_user!
27
+
28
+ def index; end
29
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+ require 'openam_auth'
3
+
4
+ describe OpenamAuth do
5
+
6
+ let(:token) { '121212121' }
7
+ let!(:openam_url) { OpenamConfig.config do
8
+ openam_url 'http://server.openam.com'
9
+ end }
10
+
11
+ describe OpenamAuth, "#get cookie name" do
12
+ before do
13
+ stub_request(:post, "#{openam_url}/identity/getCookieNameForToken").
14
+ to_return(:status => 200, :body => "string=iPlanetDirectoryPro\n", :headers => {})
15
+ end
16
+
17
+ it "should return the correct cookie name" do
18
+ OpenamAuth::Openam.new.cookie_name.should eq("iPlanetDirectoryPro")
19
+ end
20
+ end
21
+
22
+ describe OpenamAuth, "#validating token" do
23
+
24
+ before do
25
+ stub_request(:get, "#{openam_url}/identity/isTokenValid?tokenid=#{token}").
26
+ to_return(:status => 200, :body => "boolean=true\n", :headers => {})
27
+ end
28
+
29
+ it "should be a valid token" do
30
+ OpenamAuth::Openam.new.valid_token?(token).should be_true
31
+ end
32
+ end
33
+
34
+ describe OpenamAuth, "#openam user" do
35
+ let(:response) { <<EOF
36
+ userdetails.token.id=#{token}
37
+ userdetails.attribute.name=mail
38
+ userdetails.attribute.name=sunidentitymsisdnnumber
39
+ userdetails.attribute.name=sn
40
+ userdetails.attribute.value=amAdmin
41
+ EOF
42
+ }
43
+
44
+ before do
45
+ stub_request(:post, "#{openam_url}/identity/attributes").
46
+ with(:headers => {"Cookie"=>"iPlanetDirectoryPro=#{token}"}).
47
+ to_return(:status => 200, :body => response, :headers => {})
48
+ end
49
+
50
+ it "should return openam user string" do
51
+ OpenamAuth::Openam.new.openam_user('iPlanetDirectoryPro',token).to_s.should eq(response)
52
+ end
53
+
54
+ it "should parse response" do
55
+ OpenamAuth::Openam.new.user_hash(response).should eq({"sn" => ["amAdmin"] })
56
+ end
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,72 @@
1
+ require 'fixtures/application'
2
+ require 'fixtures/controllers'
3
+ require 'rspec/rails'
4
+ require 'spec_helper'
5
+
6
+ describe PostsController, type: :controller do
7
+
8
+ let(:token) { '123456' }
9
+ let!(:openam_url) { OpenamConfig.config do
10
+ openam_url 'http://server.openam.com'
11
+ end }
12
+ let(:response) { <<EOF
13
+ userdetails.token.id=#{token}
14
+ userdetails.attribute.name=mail
15
+ userdetails.attribute.name=sunidentitymsisdnnumber
16
+ userdetails.attribute.name=sn
17
+ userdetails.attribute.value=amAdmin
18
+ EOF
19
+ }
20
+
21
+ before { OpenamAuth::Openam.any_instance.stub(:token_cookie).and_return(token) }
22
+
23
+ describe "#index, authenticate by the openam server" do
24
+
25
+ before do
26
+ stub_request(:post, "#{openam_url}/identity/getCookieNameForToken").
27
+ to_return(:status => 200, :body => "string=iPlanetDirectoryPro\n", :headers => {})
28
+ end
29
+
30
+ subject { OpenamAuth::Openam.new }
31
+
32
+ describe PostsController, "#when the token is valid" do
33
+
34
+ before do
35
+ stub_request(:get, "#{openam_url}/identity/isTokenValid?tokenid=#{token}").
36
+ to_return(:status => 200, :body => "boolean=true\n", :headers => {})
37
+ stub_request(:post, "#{openam_url}/identity/attributes").
38
+ with(:headers => {"Cookie"=>"iPlanetDirectoryPro=#{token}"}).
39
+ to_return(:status => 200, :body => response, :headers => {})
40
+ end
41
+
42
+ it "should authenticate the request" do
43
+ subject.cookie_name.should eq('iPlanetDirectoryPro')
44
+ subject.token_cookie.should eq(token)
45
+ User.existing_user_by_token(token).should be_nil
46
+ subject.valid_token?(token).should be_true
47
+ subject.openam_user('iPlanetDirectoryPro', token).to_s.should eq(response)
48
+ subject.user_hash(subject.openam_user('iPlanetDirectoryPro', token)).should eq({ "sn" => ["amAdmin"] })
49
+
50
+ get :index
51
+ response.should be_true
52
+ end
53
+
54
+ end
55
+
56
+ describe PostsController, "#when the token is invalid" do
57
+
58
+ before do
59
+ stub_request(:get, "#{openam_url}/identity/isTokenValid?tokenid=#{token}").
60
+ to_return(:status => 200, :body => "boolean=false\n", :headers => {})
61
+ end
62
+
63
+ it "should authenticate the request" do
64
+ get :index
65
+ response.should redirect_to "#{openam_url}/UI/Login?goto="
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+ end
72
+
@@ -0,0 +1,4 @@
1
+ require 'webmock/rspec'
2
+
3
+ WebMock.disable_net_connect!(allow_localhost: true)
4
+
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: openam_auth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - sameera207
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-03-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec-rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: webmock
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: actionpack
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: activesupport
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: httparty
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: ruby authentication client for OpenAm
98
+ email:
99
+ - sameera207@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - .gitignore
105
+ - Gemfile
106
+ - LICENSE.txt
107
+ - README.md
108
+ - Rakefile
109
+ - lib/openam_auth.rb
110
+ - lib/openam_auth/authenticate.rb
111
+ - lib/openam_auth/openam.rb
112
+ - lib/openam_auth/openam_config.rb
113
+ - lib/openam_auth/version.rb
114
+ - openam_auth.gemspec
115
+ - spec/fixtures/application.rb
116
+ - spec/fixtures/controllers.rb
117
+ - spec/openam_auth_spec.rb
118
+ - spec/posts_controller_spec.rb
119
+ - spec/spec_helper.rb
120
+ homepage: ''
121
+ licenses:
122
+ - MIT
123
+ metadata: {}
124
+ post_install_message:
125
+ rdoc_options: []
126
+ require_paths:
127
+ - lib
128
+ required_ruby_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - '>='
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ required_rubygems_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - '>='
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ requirements: []
139
+ rubyforge_project:
140
+ rubygems_version: 2.1.11
141
+ signing_key:
142
+ specification_version: 4
143
+ summary: ruby authentication client for forgerock OpenAM
144
+ test_files:
145
+ - spec/fixtures/application.rb
146
+ - spec/fixtures/controllers.rb
147
+ - spec/openam_auth_spec.rb
148
+ - spec/posts_controller_spec.rb
149
+ - spec/spec_helper.rb