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