diff --git a/airtime_mvc/application/auth/adapters/FreeIpa.php b/airtime_mvc/application/auth/adapters/FreeIpa.php new file mode 100644 index 000000000..17217e275 --- /dev/null +++ b/airtime_mvc/application/auth/adapters/FreeIpa.php @@ -0,0 +1,117 @@ +username = $username; + return $this; + } + + /** + * password from form + * + * This is ignored by FreeIPA but needs to get passed for completeness + * + * @return self + */ + function setCredential($password) { + $this->password = $password; + return $this; + } + + /** + * Check if apache logged the user and get data from ldap + * + * @return Zend_Auth_Result + */ + function authenticate() + { + if (array_key_exists('EXTERNAL_AUTH_ERROR', $_SERVER)) { + return new Zend_Auth_Result(Zend_Auth_Result::FAILURE, null, array($_SERVER['EXTERNAL_AUTH_ERROR'])); + } + if (!array_key_exists('REMOTE_USER', $_SERVER)) { + return new Zend_Auth_Result(Zend_Auth_Result::FAILURE, null); + } + // success, the user is good since the service populated the REMOTE_USER + $remoteUser = $_SERVER['REMOTE_USER']; + + $subj = CcSubjsQuery::create()->findOneByDbLogin($remoteUser); + $subjId =null; + if ($subj) { + $subjId = $subj->getDBId(); + } + + if ($subjId) { + $user = new Application_Model_User($subjId); + } else { + // upsert the user on login for first time users + $user = new Application_Model_User(''); + } + + // Always zap any local info with new info from ipa + $user->setLogin($remoteUser); + + // Use a random password for IPA users, reset on each login... I may change this to get set to the IPA pass but hate that it is being stored as md5 behind the scenes + // gets rescrambled on each succeful login for security purposes + $ipaDummyPass = bin2hex(openssl_random_pseudo_bytes(10)); + $user->setPassword($ipaDummyPass); + + // grab user info from LDAP + $userParts = explode('@', $remoteUser); + $userInfo = LibreTime_Model_FreeIpa::GetUserInfo($userParts[0]); + + $user->setType($userInfo['type']); + $user->setFirstName($userInfo['first_name']); + $user->setLastName($userInfo['last_name']); + $user->setEmail($userInfo['email']); + $user->setCellPhone($userInfo['cell_phone']); + $user->setSkype($userInfo['skype']); + $user->setJabber($userInfo['jabber']); + $user->save(); + $this->user = $user; + try { + return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $user); + } catch (Exception $e) { + // exception occured + return new Zend_Auth_Result(Zend_Auth_Result::FAILURE, null); + } + } + + /** + * return dummy object for internal auth handling + * + * we need to build a dummpy object since the auth layer knows nothing about the db + * + * @return stdClass + */ + public function getResultRowObject() { + $o = new \stdClass; + $o->id = $this->user->getId(); + $o->username = $this->user->getLogin(); + $o->password = $this->user->getPassword(); + $o->real_name = implode(' ', array($this->user->getFirstName(), $this->user->getLastName())); + $o->type = $this->user->getType(); + $o->login = $this->user->getLogin(); + return $o; + } +} diff --git a/airtime_mvc/application/common/GoogleAnalytics.php b/airtime_mvc/application/common/GoogleAnalytics.php index 2a8d0184b..77a36f44a 100644 --- a/airtime_mvc/application/common/GoogleAnalytics.php +++ b/airtime_mvc/application/common/GoogleAnalytics.php @@ -76,6 +76,10 @@ class Application_Common_GoogleAnalytics /** Return true if the user used to be on a trial plan and was just converted to a paid plan. */ public static function didPaidConversionOccur($request) { + if (LIBRETIME_ENABLE_GOOGLE_ANALYTICS !== true) { + return false; + } + $userInfo = Zend_Auth::getInstance()->getStorage()->read(); if ($userInfo) { $user = new Application_Model_User($userInfo->id); diff --git a/airtime_mvc/application/configs/conf.php b/airtime_mvc/application/configs/conf.php index 0a0339047..d51be7f50 100644 --- a/airtime_mvc/application/configs/conf.php +++ b/airtime_mvc/application/configs/conf.php @@ -37,6 +37,11 @@ class Config { $CC_CONFIG['dev_env'] = 'production'; } + $CC_CONFIG['auth'] = 'local'; + if (isset($values['general']['auth'])) { + $CC_CONFIG['auth'] = $values['general']['auth']; + } + //Backported static_base_dir default value into saas for now. if (array_key_exists('static_base_dir', $values['general'])) { $CC_CONFIG['staticBaseDir'] = $values['general']['static_base_dir']; @@ -64,7 +69,7 @@ class Config { $CC_CONFIG['cache_ahead_hours'] = $values['general']['cache_ahead_hours']; - // Database config + // Database config $CC_CONFIG['dsn']['username'] = $values['database']['dbuser']; $CC_CONFIG['dsn']['password'] = $values['database']['dbpass']; $CC_CONFIG['dsn']['hostspec'] = $values['database']['host']; @@ -92,6 +97,19 @@ class Config { $CC_CONFIG['facebook-app-api-key'] = $globalAirtimeConfigValues['facebook']['facebook_app_api_key']; } + // ldap config + $CC_CONFIG['ldap_hostname'] = $values['ldap']['hostname']; + $CC_CONFIG['ldap_binddn'] = $values['ldap']['binddn']; + $CC_CONFIG['ldap_password'] = $values['ldap']['password']; + $CC_CONFIG['ldap_account_domain'] = $values['ldap']['account_domain']; + $CC_CONFIG['ldap_basedn'] = $values['ldap']['basedn']; + $CC_CONFIG['ldap_groupmap_guest'] = $values['ldap']['groupmap_guest']; + $CC_CONFIG['ldap_groupmap_host'] = $values['ldap']['groupmap_host']; + $CC_CONFIG['ldap_groupmap_program_manager'] = $values['ldap']['groupmap_program_manager']; + $CC_CONFIG['ldap_groupmap_admin'] = $values['ldap']['groupmap_admin']; + $CC_CONFIG['ldap_groupmap_superadmin'] = $values['ldap']['groupmap_superadmin']; + $CC_CONFIG['ldap_filter_field'] = $values['ldap']['filter_field']; + if(isset($values['demo']['demo'])){ $CC_CONFIG['demo'] = $values['demo']['demo']; } diff --git a/airtime_mvc/application/models/Auth.php b/airtime_mvc/application/models/Auth.php index 03bd7f48b..7fc9b1ea5 100644 --- a/airtime_mvc/application/models/Auth.php +++ b/airtime_mvc/application/models/Auth.php @@ -76,7 +76,10 @@ class Application_Model_Auth public static function getAuthAdapter() { $CC_CONFIG = Config::getConfig(); - + if ($CC_CONFIG['auth'] !== 'local') { + return self::getCustomAuthAdapter($CC_CONFIG['auth']); + } + // Database config $db = Zend_Db::factory('PDO_' . $CC_CONFIG['dsn']['phptype'], array( 'host' => $CC_CONFIG['dsn']['hostspec'], @@ -95,6 +98,15 @@ class Application_Model_Auth return $authAdapter; } + /** + * Gets an alternative Adapter that does not need to auth agains a databse table + * + * @return object + */ + public static function getCustomAuthAdapter($adaptor) { + return new $adaptor(); + } + /** * Get random string * diff --git a/airtime_mvc/application/models/FreeIpa.php b/airtime_mvc/application/models/FreeIpa.php new file mode 100644 index 000000000..01937cb48 --- /dev/null +++ b/airtime_mvc/application/models/FreeIpa.php @@ -0,0 +1,74 @@ +search(sprintf('%s=%s', $config['ldap_filter_field'], $username, $config['ldap_basedn'])); + + if ($ldapResults->count() !== 1) { + throw new Exception('Could not find logged user in LDAP'); + } + $ldapUser = $ldapResults->getFirst(); + + $groupMap = array( + UTYPE_GUEST => $config['ldap_groupmap_guest'], + UTYPE_HOST => $config['ldap_groupmap_host'], + UTYPE_PROGRAM_MANAGER => $config['ldap_groupmap_program_manager'], + UTYPE_ADMIN => $config['ldap_groupmap_admin'], + UTYPE_SUPERADMIN => $config['ldap_groupmap_superadmin'], + ); + $type = UTYPE_GUEST; + foreach ($groupMap as $groupType => $group) { + if (in_array($group, $ldapUser['memberof'])) { + $type = $groupType; + } + } + + // grab first value for multivalue field + $firstName = $ldapUser['givenname'][0]; + $lastName = $ldapUser['sn'][0]; + $mail = $ldapUser['mail'][0]; + + // return full user info for auth adapter + return array( + 'type' => $type, + 'first_name' => $firstName, + 'last_name' => $lastName, + 'email' => $mail, + 'cell_phone' => '', # empty since I did not find it in ldap + 'skype' => '', # empty until we decide on a field + 'jabber' => '' # empty until we decide on a field + ); + } + + /** + * Bind to ldap so we can fetch additional user info + * + * @return Zend_Ldap + */ + private static function _getLdapConnection() + { + $config = Config::getConfig(); + + $options = array( + 'host' => $config['ldap_hostname'], + 'username' => $config['ldap_binddn'], + 'password' => $config['ldap_password'], + 'bindRequiresDn' => true, + 'accountDomainName' => $config['ldap_account_domain'], + 'baseDn' => $config['ldap_basedn'] + ); + $conn = new Zend_Ldap($options); + $conn->connect(); + return $conn; + } +} diff --git a/airtime_mvc/build/airtime.example.conf b/airtime_mvc/build/airtime.example.conf index 7dc2f2cd2..a4459338b 100644 --- a/airtime_mvc/build/airtime.example.conf +++ b/airtime_mvc/build/airtime.example.conf @@ -42,6 +42,11 @@ # station_id: The Airtime station name. # Only used in saas, needed for compatibility. # +# auth: Auth adaptor to user +# Set to local to use the default db auth or specifiy +# a class like LibreTime_Auth_Adaptor_FreeIpa to replace +# the built-in adaptor +# [general] api_key = web_server_user = www-data @@ -51,6 +56,7 @@ base_dir = / cache_ahead_hours = 1 airtime_dir = station_id = +auth = local # # ---------------------------------------------------------------------- @@ -308,3 +314,38 @@ soundcloud_redirect_uri = http://libretime.example.org/soundcloud_callback.php facebook_app_id = 0 facebook_app_url = http://example.org facebook_app_api_key = 0 + +# +# ---------------------------------------------------------------------- +# L D A P +# ---------------------------------------------------------------------- +# +# hostname: Hostname of LDAP server +# +# binddn: Complete DN of user used to bind to LDAP +# +# password: Password for binddn user +# +# account_domain: Domain part of username +# +# basedn: base search DN +# +# filter_field: Name of the uid field for searching +# Usually uid, may be cn +# +# groupmap_*: Map LibreTime user types to LDAP groups +# Lets LibreTime assign user types based on the +# group a given user is in. +# +[ldap] +hostname = ldap.example.org +binddn = 'uid=libretime,cn=sysaccounts,cn=etc,dc=int,dc=example,dc=org' +password = hackme +account_domain = INT.EXAMPLE.ORG +basedn = 'cn=users,cn=accounts,dc=int,dc=example,dc=org' +filter_field = uid +groupmap_guest = 'cn=guest,cn=groups,cn=accounts,dc=int,dc=example,dc=org' +groupmap_host = 'cn=host,cn=groups,cn=accounts,dc=int,dc=example,dc=org' +groupmap_program_manager = 'cn=program_manager,cn=groups,cn=accounts,dc=int,dc=example,dc=org' +groupmap_admin = 'cn=admins,cn=groups,cn=accounts,dc=int,dc=example,dc=org' +groupmap_superadmin = 'cn=superadmin,cn=groups,cn=accounts,dc=int,dc=example,dc=org' diff --git a/docs/freeipa.md b/docs/freeipa.md new file mode 100644 index 000000000..837ef620d --- /dev/null +++ b/docs/freeipa.md @@ -0,0 +1,102 @@ +You can configure LibreTime to delegate all authentication to a FreeIPA server. + +This allows you users to use their existing FreeIPA credentials. For this to +work you need to configure Apache to use `mod_authnz_pam` and `mod_intercept_form_submit`. + +## Apache configuration + +After installing the needed modules you can set up Apache to intercept form logins and +check them against pam. + +```apache + + InterceptFormPAMService http-libretime + InterceptFormLogin username + InterceptFormPassword password + InterceptFormLoginSkip admin + InterceptFormPasswordRedact on + InterceptFormLoginRealms INT.RABE.CH + Require pam-account http-libretime + + + + + + Require pam-account http-libretime + Require all granted + + + Require expr %{REQUEST_URI} =~ /(index.php|login|favicon.ico|js|css|locale)/ + Require all granted + + + +``` + +## PAM configuration + +The above configuration expects a PAM configuration for the `http-libretime` service. + +To confiure this you need to create the file `/etc/pam.d/http-libretime` with the following contents. + +``` +auth required pam_sss.so +account required pam_sss.so +``` + +## LDAP configuration + +LibreTime needs direct access to LDAP so it can fetch additional information. It does so with +a [system account](https://www.freeipa.org/page/HowTo/LDAP#System_Accounts) that you need to +set up beforehand. + +You can configure everything pertaining to how LibreTime accesses LDAP in +`/etc/airtime/airtime.conf`. The default file has the following values you need to change. + +```ini +# +# ---------------------------------------------------------------------- +# L D A P +# ---------------------------------------------------------------------- +# +# hostname: Hostname of LDAP server +# +# binddn: Complete DN of user used to bind to LDAP +# +# password: Password for binddn user +# +# account_domain: Domain part of username +# +# basedn: base search DN +# +# filter_field: Name of the uid field for searching +# Usually uid, may be cn +# +# groupmap_*: Map LibreTime user types to LDAP groups +# Lets LibreTime assign user types based on the +# group a given user is in. +# +[ldap] +hostname = ldap.example.org +binddn = 'uid=libretime,cn=sysaccounts,cn=etc,dc=int,dc=example,dc=org' +password = hackme +account_domain = INT.EXAMPLE.ORG +basedn = 'cn=users,cn=accounts,dc=int,dc=example,dc=org' +filter_field = uid +groupmap_guest = 'cn=guest,cn=groups,cn=accounts,dc=int,dc=example,dc=org' +groupmap_host = 'cn=host,cn=groups,cn=accounts,dc=int,dc=example,dc=org' +groupmap_program_manager = 'cn=program_manager,cn=groups,cn=accounts,dc=int,dc=example,dc=org' +groupmap_admin = 'cn=admins,cn=groups,cn=accounts,dc=int,dc=example,dc=org' +groupmap_superadmin = 'cn=superadmin,cn=groups,cn=accounts,dc=int,dc=example,dc=org' +``` + +## Enable FreeIPA auth + +After everything is set up properly you can enable FreeIPA auth in `airtime.conf`: + +``` +[general] +auth = LibreTime_Auth_Adaptor_FreeIpa +``` + +You should now be able to use your FreeIPA credentials to log in to LibreTime. diff --git a/mkdocs.yml b/mkdocs.yml index cec423ee4..5ffdb54f0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -69,6 +69,7 @@ pages: - 'LibreTime API authentication': manual/airtime-api-authentication/index.md - 'Secure login with SSL or TLS': manual/secure-login-with-ssl/index.md - 'Icecast statistics with Piwik': manual/icecast-statistics-with-piwik/index.md + - 'FreeIPA Authentication': freeipa.md - 'Development': - 'Testing': testing.md - 'Vagrant': vagrant.md