lib/puavo/authentication.rb in puavo_authentication-0.2.0 vs lib/puavo/authentication.rb in puavo_authentication-0.2.1
- old
+ new
@@ -1,93 +1,260 @@
module Puavo
- module Authentication
- def self.included(base)
- base.send :extend, ClassMethods
+ mattr_accessor :available_languages
+
+ class AuthenticationError < UserError
+ def code
+ "authentication_error"
end
+ end
+ class AuthenticationFailed < AuthenticationError
+ def code
+ "bad_credentials"
+ end
+ end
- module ClassMethods
+ class AuthorizationFailed < AuthenticationError
+ def code
+ "no_permissions"
+ end
+ end
- def dn_cache_key(login_uid)
- "user_dn:#{ login_uid }"
- end
- def delete_caches(login_uid)
- Rails.cache.delete dn_cache_key login_uid
- end
+ # For User model
+ module AuthenticationMixin
+ # FIXME Observer?
+ def delete_dn_cache
+ organisation_key = LdapOrganisation.first.cn.to_s
+ Rails.cache.delete Puavo::Authentication.dn_cache_key organisation_key, uid
+ end
+ end
- # Authenticate user with login username and password.
- # Returns user dn string on successful login or false on invalid login
- def authenticate(login, password)
+ class Authentication
- # To authenticate an user we need to make a LDAP bind with user's dn
- # and password. Lets look it up from cache:
- user_dn = Rails.cache.fetch dn_cache_key(login) do
- # On cache miss we need to use the Puavo credentials from config/ldap.yml
- # to fetch the user object which contains the user dn.
+ attr_accessor :authenticated, :authorized
- # This find call actually initializes the LDAP connection under the
- # hood with Puavo credentials.
- user = self.find(:first, :attribute => "uid", :value => login)
+ def self.dn_cache_key(organisation_key, uid)
+ "user_dn:#{ organisation_key }:#{ uid }"
+ end
- # Remove connection made with Puavo credentials
- self.remove_connection
+ def initialize
+ @credentials = {}
+ end
- if user.nil?
- return nil
- end
+ [:dn, :organisation_key, :scope].each do |attr|
+ define_method attr do
+ @credentials[attr]
+ end
+ end
- user.dn
- end
+ def puavo_configuration
+ ActiveLdap::Base.ensure_configuration
+ end
- if user_dn.nil?
- logger.info "Login failed for #{ login }: Unknown username"
- return false
- end
+ def base
+ return current_organisation.ldap_base
+ end
- # Setup new ActiveLdap connections to use user's credentials
- LdapBase.ldap_setup_connection(
- LdapBase.configuration[:host],
- LdapBase.base.to_s,
- user_dn,
- password)
+ def ldap_host
+ @credentials[:ldap_host] || puavo_configuration["host"]
+ end
- # Do not never ever allow anonymous connections in Puavo. Should be
- # false in config/ldap.yml, but we just make sure here.
- self.connection.instance_variable_set :@allow_anonymous, false
+ def self.remove_connection
+ ActiveLdap::Base.active_connections.keys.each do |connection_name|
+ ActiveLdap::Base.remove_connection(connection_name)
+ end
+ end
- # This is the first time when LDAP connection is used with the user's
- # credentials. So this search call will initialize the connection and
- # will raise ActiveLdap::AuthenticationError if user supplied a
- # bad password.
- begin
- admin_permissions = School.search(
- :filter => "(puavoSchoolAdmin=#{user_dn})",
- :scope => :one, :attributes => ["puavoId"],
- :limit => 1 )
- rescue ActiveLdap::AuthenticationError
- logger.info "Login failed for #{ login } (#{ user_dn }): Bad password"
- return false
- end
+ def configure_ldap_connection(credentials)
- # Allow authentication if user is a school admin in the some school.
- if not admin_permissions.empty?
- return user_dn
+ @credentials = credentials
+
+ if current_organisation.nil?
+ raise Puavo::AuthenticationError, "Bad organisation"
+ end
+
+ if uid = @credentials[:uid]
+ if uid.nil? || uid.empty?
+ raise AuthenticationFailed, "Cannot get dn from empty or nil uid"
end
- # Allow authentication if user is an organisation owner
- organisation = LdapOrganisation.first
- if organisation && organisation.owner.include?(user_dn)
- return user_dn
+ if uid.match(/^service\//)
+ uid = uid.match(/^service\/(.*)/)[1]
+ user_class = ExternalService
+ else
+ user_class = User
end
- # Allow authentication always if logged in user an external service
- if user_dn.rdns[1]["ou"] == "System Accounts"
- return user_dn
+ user_dn = Rails.cache.fetch self.class.dn_cache_key(organisation_key, uid) do
+ # Remove previous connection
+ self.class.remove_connection
+ LdapBase.ldap_setup_connection( ldap_host,
+ base.to_s,
+ puavo_configuration["bind_dn"],
+ puavo_configuration["password"] )
+
+ user = user_class.find(:first, :attribute => "uid", :value => uid)
+
+ if user
+ user.dn.to_s
+ else
+ nil
+ end
end
+
+ raise AuthenticationFailed, "Cannot get dn for UID '#{ uid }'" if not user_dn
+ logger.debug "Found #{ dn } for #{ uid }"
+ @credentials[:dn] = ActiveLdap::DistinguishedName.parse user_dn
+ end
- logger.info "Login failed for #{ login } (#{ user_dn }): Not school admin or organisation owner"
- return false
+ # Reset attributes on new configuration
+ @current_user = nil
+ @authenticated = false
+ @authorized = false
+
+ # Remove previous connection
+ self.class.remove_connection
+
+
+
+ logger.info "Configuring ActiveLdap to use #{ @credentials.map { |k,v| "#{ k }: #{ v }" }.join ", " }"
+ logger.debug "PW: #{ @credentials[:password] }" if ENV["LOG_LDAP_PASSWORD"]
+ # Setup new ActiveLdap connections to use user's credentials
+ LdapBase.ldap_setup_connection ldap_host, base.to_s, @credentials[:dn], @credentials[:password]
+
+ # Do not never ever allow anonymous connections in Puavo. Should be
+ # false in config/ldap.yml, but we just make sure here.
+ LdapBase.connection.instance_variable_set :@allow_anonymous, false
+
+ end
+
+ # Test dn&password bind to LDAP without actually configuring ActiveLdap to
+ # use them
+ def test_bind(dn, password)
+ ldap = Net::LDAP.new(
+ :host => ldap_host,
+ :port => 389,
+ :encryption => {
+ :method => :start_tls
+ },
+ :auth => {
+ :method => :simple,
+ :username => dn.to_s,
+ :password => password
+ })
+
+ if not ldap.bind
+ raise AuthenticationFailed, "Test bind failed: Bad dn or password"
end
end
+
+ # Authenticate configured connection to LDAP.
+ #
+ # Raises AuthenticationFailed if connection could not be made.
+ # Returns possible admin permissions on successful connect
+ def authenticate
+
+ # This is the first time when LDAP connection is used with the user's
+ # credentials. So this search call will initialize the connection and
+ # will raise ActiveLdap::AuthenticationError if user supplied a
+ # bad password.
+ begin
+
+ @admin_permissions = School.search(
+ :filter => "(puavoSchoolAdmin=#{ dn })",
+ :scope => :one, :attributes => ["puavoId"],
+ :limit => 1 )
+
+ AccessToken.validate @credentials if oauth_access_token?
+
+ rescue ActiveLdap::AuthenticationError
+ raise AuthenticationFailed, "Bad dn or password"
+ rescue AccessToken::Expired
+ raise AuthenticationFailed, "OAuth Access Token expired"
+ end
+
+
+ @authenticated = true
+
+ end
+
+ def external_service?
+ dn.rdns[1]["ou"] == "System Accounts"
+ end
+
+ def oauth_client_server?
+ dn.rdns.first.keys.first == "puavoOAuthClientId"
+ end
+
+ def oauth_access_token?
+ dn.rdns.first.keys.first == "puavoOAuthTokenId"
+ end
+
+ # User is authenticated with real password
+ def user_password?
+ return false if oauth_access_token?
+ current_user.classes.include? "puavoEduPerson"
+ end
+
+ # Authorize that user has permissions to use Puavo
+ def authorize
+
+ raise AuthorizationFailed, "Cannot authorize before authenticating" unless @authenticated
+
+ # Authorize school admins
+ if not @admin_permissions.empty?
+ logger.info "Authorization ok: Admin #{ dn }"
+ return @authorized = true
+ end
+
+ # Authorize External Services
+ if external_service?
+ logger.info "Authorization ok: External Service #{ dn }"
+ return @authorized = true
+ end
+
+ # Authorize OAuth Access Tokens
+ if oauth_access_token?
+ return @authorized = true
+ end
+
+ # Authorize organisation owners
+ organisation = LdapOrganisation.first
+ if organisation && organisation.owner && organisation.owner.include?(dn)
+ logger.info "Authorization ok: Organisation owner #{ dn }"
+ return @authorized = true
+ end
+
+ raise AuthorizationFailed, "Unauthorized access for #{ dn }"
+ end
+
+ def current_user
+
+ raise "Cannot get current user before authentication" if not @authenticated
+
+ return @current_user if @current_user
+
+
+ if external_service?
+ @current_user = ExternalService.find dn
+ elsif oauth_access_token?
+ access_token = AccessToken.find dn
+ @current_user = User.find access_token.puavoOAuthEduPerson
+ else
+ @current_user = User.find dn
+ end
+
+ raise "Failed get User object for #{ dn }" if @current_user.nil?
+ return @current_user
+ end
+
+ def current_organisation
+ Puavo::Organisation.find organisation_key
+ end
+
+ def logger
+ RAILS_DEFAULT_LOGGER
+ end
+
end
end