TestLink 整合 LDAP 和 CAS 单点登录

背景介绍

  • TestLink 版本:2.18
  • TestLink URL:http://devops.iamzhl.top/testlink
  • openLDAP 服务:ldap://devops.iamzhl.top:389
  • CAS 服务:http://devops.iamzhl.top:8080/cas

整合 LDAP

修改TestLink配置文件

1
# vi /var/www/html/testlink/custom_config.inc.php

添加LDAP配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$tlCfg->authentication['method'] = 'LDAP';
$tlCfg->authentication['ldap'] = array();
$tlCfg->authentication['ldap'][1]['ldap_server'] = 'devops.iamzhl.top';
$tlCfg->authentication['ldap'][1]['ldap_port'] = '389';
$tlCfg->authentication['ldap'][1]['ldap_version'] = '3';
$tlCfg->authentication['ldap'][1]['ldap_root_dn'] = 'dc=iamzhl,dc=top';
$tlCfg->authentication['ldap'][1]['ldap_bind_dn'] = 'cn=Manager,dc=iamzhl,dc=top';
$tlCfg->authentication['ldap'][1]['ldap_bind_passwd'] = '123456';
$tlCfg->authentication['ldap'][1]['ldap_tls'] = false;
$tlCfg->authentication['ldap'][1]['ldap_organization'] = '';
$tlCfg->authentication['ldap'][1]['ldap_uid_field'] = 'uid';
$tlCfg->authentication['ldap'][1]['ldap_email_field'] = 'mail';
$tlCfg->authentication['ldap'][1]['ldap_firstname_field'] = 'givenname';
$tlCfg->authentication['ldap'][1]['ldap_surname_field'] = 'sn';
$tlCfg->authentication['ldap_automatic_user_creation'] = true;
$tlCfg->authentication['ldap_email_field'] = 'mail';
$tlCfg->authentication['ldap_firstname_field'] = 'givenname';
$tlCfg->authentication['ldap_surname_field'] = 'sn';

测试

打开TestLink网址http://devops.iamzhl.top/testlink

如图,正常跳转到TestLink登录界面,输入LDAP服务器中的用户名密码后点击Log in

如图所示,登陆成功后正常的获取到了用户名,点击左上角的登出按钮,正常退出后跳转到了TestLink的登录界面

至此,TestLink整合LDAP完成。

整合CAS单点登录

添加依赖的phpCAS库文件

1
2
3
4
5
6
# wget https://github.com/apereo/phpCAS/archive/1.3.6.tar.gz
# mv 1.3.6.tar.gz phpCAS-1.3.6.tar.gz
# tar zxvf phpCAS-1.3.6.tar.gz
# chown -R apache:apache phpCAS-1.3.6
# cp -arf phpCAS-1.3.6/source/CAS.php /var/www/html/testlink/lib/functions/
# cp -arf phpCAS-1.3.6/source/CAS /var/www/html/testlink/lib/functions/

修改TestLink配置文件

1
# vi /var/www/html/testlink/custom_config.inc.php

添加CAS配置项

1
2
3
4
5
6
7
8
/** CAS server parameters */
$tlCfg->authentication['cas_enable'] = true;
$tlCfg->authentication['cas_server_name'] = 'devops.iamzhl.top';
$tlCfg->authentication['cas_server_port'] = 8080;
$tlCfg->authentication['cas_server_path'] = 'cas';
$tlCfg->authentication['cas_debug_enable'] = true;
$tlCfg->authentication['cas_debug_file'] = '/var/logs/testlink/phpCAS.log';
$tlCfg->authentication['cas_server_protocol'] = '2.0';

修改登录界面

1
# vi /var/www/html/testlink/login.php

switch($args->action)分支选择语句段内找到case 'loginform'部分,添加CAS的认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
switch($args->action) 
{
case 'doLogin':
case 'ajaxlogin':
doSessionStart(true);

// When doing ajax login we need to skip control regarding session already open
// that we use when doing normal login.
// If we do not proceed this way we will enter an infinite loop
$options = array('doSessionExistsCheck' => ($args->action=='doLogin'));
$op = doAuthorize($db,$args->login,$args->pwd,$options);
$doAuthPostProcess = true;
$gui->draw = true;
break;

case 'ajaxcheck':
processAjaxCheck($db);
break;


case 'oauth':
//If code is empty then break
if (!isset($_GET['code'])){
renderLoginScreen($gui);
die();
}

//Switch between oauth providers
if (!include_once('lib/functions/oauth_providers/'.$_GET['oauth'].'.php')) {
die("Oauth client doesn't exist");
}

$oau = config_get('OAuthServers');
foreach ($oau as $oprov) {
if (strcmp($oprov['oauth_name'],$_GET['oauth']) == 0){
$oauth_params = $oprov;
break;
}
}

$user_token = oauth_get_token($oauth_params, $_GET['code']);
if($user_token->status['status'] == tl::OK) {
doSessionStart(true);
$op = doAuthorize($db,$user_token->options->user,'oauth',$user_token->options);
$doAuthPostProcess = true;
} else {
$gui->note = $user_token->status['msg'];
renderLoginScreen($gui);
die();
}
break;

case 'loginform':
//zhanghl start
if($authCfg['cas_enable'])
{
if($authCfg['cas_debug_enable'])
{
phpCAS::setDebug($authCfg['cas_debug_file']);
}
// Initialize phpCAS
phpCAS::client($authCfg['cas_server_protocol'], $authCfg['cas_server_name'], $authCfg['cas_server_port'], $authCfg['cas_server_path']);
// For production use set the CA certificate that is the issuer of the cert
// on the CAS server and uncomment the line below
// phpCAS::setCasServerCACert($cas_server_ca_cert_path);

// For quick testing you can disable SSL validation of the CAS server.
// THIS SETTING IS NOT RECOMMENDED FOR PRODUCTION.
// VALIDATING THE CAS SERVER IS CRUCIAL TO THE SECURITY OF THE CAS PROTOCOL!
phpCAS::setNoCasServerValidation();

// Override the validation url for any (ST and PT) CAS 2.0 validation
//phpCAS::setServerProxyValidateURL('http://devops.iamzhl.top:8080/cas/proxyValidate');

// Override the validation url for any CAS 1.0 validation
//phpCAS::setServerServiceValidateURL('http://devops.iamzhl.top:8080/cas/serviceValidate');

phpCAS::handleLogoutRequests();
phpCAS::forceAuthentication();
$options = array('doSessionExistsCheck' => ($args->action=='doLogin'));
$op = doCASAuthorize($db,$options);
$doAuthPostProcess = true;
}
else {
//zhanghl end
$doRenderLoginScreen = true;
$gui->draw = true;
$op = null;

// unfortunatelly we use $args->note in order to do some logic.
if( ($args->note=trim($args->note)) == "" )
{
if( $gui->authCfg['SSO_enabled'] )
{
doSessionStart(true);
$doAuthPostProcess = true;

switch ($gui->authCfg['SSO_method'])
{
case 'CLIENT_CERTIFICATE':
$op = doSSOClientCertificate($db,$_SERVER,$gui->authCfg);
break;

case 'WEBSERVER_VAR':
//DEBUGsyslogOnCloud('Trying to execute SSO using SAML');
$op = doSSOWebServerVar($db,$gui->authCfg);
break;
}
}
}
//zhanghl start
}
//zhanghl end
break;
}

init_gui函数内找到switch($args->note)分支语句,在expired分支下添加一行重定向调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
switch($args->note) {
case 'expired':
if(!isset($_SESSION)) {
session_start();
}
session_unset();
session_destroy();
$gui->note = lang_get('session_expired');
$gui->reqURI = null;
// 添加重定向调用
redirect(TL_BASE_HREF ."login.php?destination=".$args->destination);
break;

case 'first':
$gui->note = lang_get('your_first_login');
$gui->reqURI = null;
break;

case 'lost':
$gui->note = lang_get('passwd_lost');
$gui->reqURI = null;
break;

default:
$gui->note = '';
break;
}

修改登出界面

1
# /var/www/html/testlink/logout.php

$authCfg = config_get('authentication');语句之后添加CAS的登出处理

1
2
3
4
5
6
7
8
9
10
11
if($authCfg['cas_enable'])
{
if($authCfg['cas_debug_enable'])
{
phpCAS::setDebug($authCfg['cas_debug_file']);
}
// Initialize phpCAS
phpCAS::client($authCfg['cas_server_protocol'], $authCfg['cas_server_name'], $authCfg['cas_server_port'], $authCfg['cas_server_path']);
phpCAS::logout();
}
redirect("login.php?note=logout");

修改common.php全局引用文件

1
# vi /var/www/html/testlink/lib/functions/common.php

require_once('tlsmarty.inc.php');引用的前面增加对CAS的引用

1
2
3
4
5
6
7
/** TestLink CAS Authentication Ref */
$authCfg = config_get('authentication');
if($authCfg['cas_enable'])
{
// Load the CAS lib
require_once 'CAS.php';
}

修改认证函数

1
# vi /var/www/html/testlink/lib/functions/doAuthorize.php

在开头require_once语句的后面添加CAS认证函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// zhanghl start
function doCASAuthorize(&$db,$options=null)
{
global $g_tlLogger;
$result = array('status' => tl::ERROR, 'msg' => null);
$user = new tlUser();
$user->login = $_SESSION['phpCAS']['user'];
$login_exists = ($user->readFromDB($db,tlUser::USER_O_SEARCH_BYLOGIN) >= tl::OK);

if(!$login_exists)
{
$user = new tlUser();
$user->login = $_SESSION['phpCAS']['user'];
$user->isActive = true;
$user->authentication = 'LDAP'; // force for auth_does_password_match
$user->setPassword($user->login); // write password on DB anyway
}
//$user->emailAddress = $_SESSION['phpCAS']['attributes']['mail'];
//$user->firstName = $_SESSION['phpCAS']['attributes']['sn'];
//$user->lastName = $_SESSION['phpCAS']['attributes']['givenName'];
$doLogin = ($user->writeToDB($db) == tl::OK);

if( $doLogin )
{
// Need to do set COOKIE following Mantis model
$auth_cookie_name = config_get('auth_cookie');
$expireOnBrowserClose=false;
setcookie($auth_cookie_name,$user->getSecurityCookie(),$expireOnBrowserClose,'/');

// Disallow two sessions within one browser
if (isset($_SESSION['currentUser']) && !is_null($_SESSION['currentUser']))
{
$result['msg'] = lang_get('login_msg_session_exists1') .
' <a style="color:white;" href="logout.php">' .
lang_get('logout_link') . '</a>' . lang_get('login_msg_session_exists2');
}
else
{
// Setting user's session information
$_SESSION['currentUser'] = $user;
$_SESSION['lastActivity'] = time();

$g_tlLogger->endTransaction();
$g_tlLogger->startTransaction();
setUserSession($db,$user->login, $user->dbID,$user->globalRoleID,$user->emailAddress,$user->locale,null);

$result['status'] = tl::OK;
}
}
return $result;
}
// zhanghl end

修改全局配置文件 (可选)

1
# vi /var/www/html/testlink/config.inc.php

增加CAS认证属性

1
2
3
4
5
6
7
8
/** CAS server properties */
$tlCfg->authentication['cas_enable'] = false;
$tlCfg->authentication['cas_server_name'] = '';
$tlCfg->authentication['cas_server_port'] = 8080;
$tlCfg->authentication['cas_server_path'] = 'cas';
$tlCfg->authentication['cas_debug_enable'] = true;
$tlCfg->authentication['cas_debug_file'] = '';
$tlCfg->authentication['cas_server_protocol'] = '';

Note:此选项用以设置默认属性值,主要用来日后查阅,可以不写,/var/www/html/testlink/custom_config.inc.php文件相同的属性配置会覆盖生效。

修改CASClient.php启用http连接(根据个人CAS服务器来定)

1
# vi /var/www/html/testlink/lib/functions/CAS/Client.php

将如下几个函数中的https改为http即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private function _getServerBaseURL()
{
// the URL is build only when needed
if ( empty($this->_server['base_url']) ) {
// $this->_server['base_url'] = 'https://' . $this->_getServerHostname();
$this->_server['base_url'] = 'http://' . $this->_getServerHostname();
if ($this->_getServerPort()!=443) {
$this->_server['base_url'] .= ':'
.$this->_getServerPort();
}
$this->_server['base_url'] .= $this->_getServerURI();
}
return $this->_server['base_url'];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private function _getCallbackURL()
{
// the URL is built when needed only
if ( empty($this->_callback_url) ) {
$final_uri = '';
// remove the ticket if present in the URL
// $final_uri = 'https://';
$final_uri = 'http://';
$final_uri .= $this->_getClientUrl();
$request_uri = $_SERVER['REQUEST_URI'];
$request_uri = preg_replace('/\?.*$/', '', $request_uri);
$final_uri .= $request_uri;
$this->_callback_url = $final_uri;
}
return $this->_callback_url;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public function getURL()
{
phpCAS::traceBegin();
// the URL is built when needed only
if ( empty($this->_url) ) {
$final_uri = '';
// remove the ticket if present in the URL
// $final_uri = ($this->_isHttps()) ? 'https' : 'http';
$final_uri = ($this->_isHttps()) ? 'http' : 'http';
$final_uri .= '://';

$final_uri .= $this->_getClientUrl();
$request_uri = explode('?', $_SERVER['REQUEST_URI'], 2);
$final_uri .= $request_uri[0];

if (isset($request_uri[1]) && $request_uri[1]) {
$query_string= $this->_removeParameterFromQueryString('ticket', $request_uri[1]);

// If the query string still has anything left,
// append it to the final URI
if ($query_string !== '') {
$final_uri .= "?$query_string";
}
}

phpCAS::trace("Final URI: $final_uri");
$this->setURL($final_uri);
}
phpCAS::traceEnd($this->_url);
return $this->_url;
}

测试

新建debug调试目录

1
2
# mkdir /var/log/testlink
# chown -R apache:apache /var/log/testlink

打开TestLink网址http://devops.iamzhl.top/testlink

如图所示,正常跳转到CAS的登录界面,地址变成了http://devops.iamzhl.top:8080/cas/login?service=http%3A%2F%2Fdevops.iamzhl.top%2Ftestlink%2Flogin.php,输入用户名密码后点击登录

如图登陆成功后正常获取用户名,点击左上角的登出按钮后,正常退出到CAS登出页面

image-20181205173735167

至此,TestLink整合CAS单点登录完成。

-------------本文结束感谢您的阅读-------------
请站长喝杯咖啡吧´◡`