This gem enables authentication for Ruby on Rails applications against Openstack Keystone Service using the Identity API v3. Further it implements a Policy Engine to allow permission cheks.
class DashboardController < ::ScopeController
authentication_required domain: :get_domain_id,
project: :get_project_id,
except: :terms_of_use
def index
end
def terms_of_use
end
protected
def get_domain_id
params[:domain_id]
end
def get_project_id
params[:project_id]
end
$ [sudo] gem install monsoon-openstack-auth
gem 'monsoon-openstack-auth', git: 'git://github.com/sapcc/monsoon-openstack-auth.git'
rails generate monsoon_openstack_auth:setup
File: config/initializers/monsoon_openstack_auth.rb
MonsoonOpenstackAuth.configure do |config|
############# Authentication ################
# connection driver, default MonsoonOpenstackAuth::Driver::Default (Fog)
# config.connection_driver = DriverClass
# api auth endpoint
config.connection_driver.api_endpoint = ENV['MONSOON_OPENSTACK_AUTH_API_ENDPOINT']
# optional, default=true
config.token_auth_allowed = true
# optional, default=true
config.basic_auth_allowed = true
# optional, default=true
config.sso_auth_allowed = true
# optional, default=true
config.form_auth_allowed = true
# optional, default=false
config.access_key_auth_allowed = false
# optional, default= last url before redirected to form
# config.login_redirect_url = '/'
########## Authorization #########
# policy_file_path: path to policy file
config.authorization.policy_file_path = "config/policy.json"
# context: Default is name of main app, e.g. dashboard.
# If you overwrite context, rules in policy file should begin with that context.
config.authorization.context = "identity"
# controller_action_map: default action mappings for controller actions. Can be overwritten inside controller
config.authorization.controller_action_map {
:index => 'read',
:show => 'read',
:new => 'create',
:create => 'create',
:edit => 'update',
:update => 'update',
:destroy => 'delete'
}
# config.authorization.security_violation_handler: Error handler method which is called when MonsoonOpenstackAuth::Authorization::SecurityViolation appears.
# Default setting is :authorization_forbidden.
# You can specify another handler or overwrite "authorization_forbidden" method in controller.
security_violation_handler = :authorization_forbidden
# enable or disable two factor authentication
config.two_factor_enabled = false
# two factor auth method should return a Proc with params username and passcode
config.two_factor_authentication_method = -> username,passcode { }
########## Plugin ##########
# optional, default=false
# config.debug = true
# optional Excon request and response debug, default=false
# config.debug_api_calls = true (Deprecated, use environment variable EXCON_DEBUG = true)
end
If this gem should support the form based login then the session store must be anything but cookie_store.
Example of setting up a ActiveRecord session_store
File: Gemfile
gem 'activerecord-session_store'
rails generate active_record:session_migration
File: config/initializers/session_store.rb
Rails.application.config.session_store :active_record_store, :key => '_monsoon_app_session'
ActionController::API does not include http basic functionality. So you have to include it manually if you want to support http basic.
include ActionController::HttpAuthentication::Basic::ControllerMethods
ActionController::API does not include MonsoonOpenstackAuth::Authentication. So you have to include it manually.
include MonsoonOpenstackAuth::Authentication
Class method which is called in controllers. This method is based on the before_action method and therefore it accepts the common options such as :except, :only, and :if
authentication_required options
options:
- rescope, boolean (optional). Default is true. If rescope is true the user token will be scoped to domain or/and project.
- domain, symbol, value or Proc (optional?). Provides user domain id. This option is used for both the user domain and the scope domain (if rescope = false). One of options domain_id or domain_name should be provided!
- domain_name, string or Proc (optional?). Use this option instead of domain_id. One of options domain_id or domain_name should be provided!
- project, symbol, value or Proc (optional). Provides project id. This option is used for scope project (if rescope = true).
- only, array (optional). Example only: [:index,:show]
- except, optional. Example except: [:index,:show]
- if, optional. Example if: -> c {c.params[:region_id].nil?}
- unless, optional
Example for unscoped token authentication. User is redirected to the login page, where he/she is prompted to enter his/her credentials.
DashboardController < ApplicationController
authentication_required domain: 'DOMAIN_ID', rescope: false
def index
end
end
Example for project scoped token authentication.
DashboardController < ApplicationController
authentication_required domain_name: DOMAIN_NAME, project: PROJECT_ID, rescope: true
def index
end
end
Example for only option
DashboardController < ApplicationController
authentication_required only: [:index], project: :get_project, domain: :get_domain
def index
end
def get_domain
@domain_id = params[:domain_id]
end
def get_project
@project_id = params[:project_id]
end
end
Class method which is called in controllers. This method allows you to skip authentication in subclasses.
skip_authentication options
options:
- only, optional. Example only: [:index,:show]
- except, optional. Example except: [:index,:show]
- if, optional. Example if: -> c {!c.params[:domain].nil?}
- unless, optional
It is possible to prevent automatic rescoping.
DashboardController < ApplicationController
authentication_required domain_name: -> c {c.params[:domain_id]}, project: project_id, rescope: false
before_action do
# user is authenticated with unscoped token. Check if user has read permission for project_id
user_projects = service_user.user_projects(current_user.id)
redirect_to not_allowed_url unless user_projects.collect{|project| project.id}.include?(project_id)
end
before_action do
authentication_rescope_token
# now current_user is rescoped to project_id
end
def index
end
protected
def project_id
@project_id ||= params[:project_id]
end
end
Instance method, available in controller instances and views. Returns current logged in user or nil. To get current_user the authentication_required method should be called first.
Instance method, available in controller instances and views. Returns true if current logged in user is presented.
Instance methods:
- context, returns the token received by API
- enabled?, true if user is active (enabled)
- token, returns the token value (auth_token)
- id, user id (obtained through the token)
- name, user name (obtained through the token)
- description, user description (obtained through the token)
- user_domain_id, received by scoped token
- user_domain_name, received by scoped token
- domain_id, scope (obtained through the token)
- domain_name, scope (obtained through the token)
- project_id, scope (obtained through the token)
- project_name, scope (obtained through the token)
- project_domain_id, scope (obtained through the token)
- project_domain_name, scope (obtained through the token)
- project_scoped, returns a hash (scope)
- domain_scoped, returns a hash (scope)
- token_expires_at, returns datetime
- token_expired?, true if token expired
- token_issued_at, returns datetime
- service_catalog, returns an array of hashes (services)
- has_service?(type), returns true if service_catalog contains the given type
- roles, returns an array of hashes
- role_names, returns an array of roles
- has_role?(name), returns true if user has the given role
- admin?, true if user is a superuser (can do anything)
- default_services_region, returns the first endpoint region for first non-identity service in the service catalog
- available_services_regions, returns list of unique region name values found in service catalog
- is_allowed?(RULE_NAMES), returns true or false
- required_roles(RULE_NAMES), returns an array of required roles
Authorization is inspired by the authority gem from nathanl. In contrast to it's origin, authorization uses policy files for
the authorization checks and not authorizer classes.
So you now have to implement a policy file in json format for you application. The file has to be located under config.authorization.policy_file_path
.
An example could look like:
{
"default": "rule:admin_required",
"p_member" : "project_id:%(project.id)s",
"d_member" : "domain_id:%(domain.id)s",
"admin_required": "role:admin or is_admin:True",
"admin_or_project_member": "rule:admin_required or rule:p_member",
"admin_or_domain_member": "rule:admin_required or rule:d_member",
"identity:domain_list": "rule:test or rule:admin_required or rule:is_service or rule:admin_or_domain_member",
"identity:domain_show": "rule:admin_required or rule:d_member",
"identity:domain_create": "",
"identity:domain_change": "rule:admin_required or rule:d_member",
"identity:domain_delete": "rule:admin_required",
"identity:project_list": "rule:admin_or_domain_member",
"identity:project_create": "rule:admin_or_domain_member",
"identity:project_change": "rule:admin_or_domain_member or rule:p_member"
}
The policy syntax is described at openstack olso policies.
Some explanations on that:
"default": "rule:admin_required"
defines the default rule which is used in case that an authorization request with an undefined rule is made.
"admin_required": "role:admin or is_admin:True"
defines a rule which can later be referenced in other rules. It uses the role and is_admin attributes from the current user for whom a authorization request is made.
"d_member" : "domain_id:%(domain.id)s"
when the user domain_id from his current authentication scope is the same as the one given to the auth. request. The auth. request could be made with an Domain object which has an id attribute.
"identity:project_list": "rule:admin_or_domain_member"
defines a application specific rule where the context is identity (might come from config.authorization.context) and the action to be checked is project_list.
You've to use the policy_engine to do a policy enforcement. The engine is always available
policy_engine = MonsoonOpenstackAuth.policy_engine
Afterwards you can do a policy check for a user with
action = "identity:project_list"
policy_engine.policy(@current_user).enforce(action)
You get a true or false
as an result.
Similar to the above but more convinient you can check authorizations for a user with the is_allowed?
method. So you can ask
action = "identity:project_list"
@current_user.is_allowed?(action, @domain)
and get a boolean response.
Controllers get some additional class methods for authorization purpose automatically through a railtie.
You can check authorization in your controllers in one of two ways:
authorization_actions_for ModelClass [, :name => 'ModelNameUsedInPolicy', :actions => {:action_name => 'policy_action_name'}, <StandardBeforeFilterOptions> ]
protects multiple controller actions with a before_action, which performs a class-level check. If the current user is never allowed to delete a ModelClass, he'll never even get to the controller's destroy method.
authorization_action_for @model [, :name => 'ModelNameUsedInPolicy' ]
can be called inside a single controller action, and performs an instance-level check. If called inside update, it will check whether the current user is allowed to update this particular @model instance.
If either method finds a user attempting something they're not authorized to do, a Security Violation will result.
How does authorization_actions_for
know to check deletable_by? before the controller's destroy action? It checks your configuration from config.authorization.controller_action_map configured in the initializer file.
The mappings are also configurable per controller with
authorization_actions :index => 'list', :update => 'change'
Alternatively you can call an authorization check by it's rule directly with an
if_allowed?(PolicyFileRule [, {key: value, ...}])
Authorizations for a user can be checked by the is_allowed?
method. So you can ask
@current_user.is_allowed?(PolicyFileRule, params)
Example:
@current_user.is_allowed?("identity:project_create", {domain_id: 1})
@current_user.is_allowed?(["identity:project_create","identity:project_change"], {domain_id: 1})
git clone https://github.com/sapcc/monsoon-openstack-auth.git
cd monsoon-openstack-auth
bundle install
cd spec/dummy
bundle exec rake db:migrate
bundle exec rails s