背景介绍

整合 LDAP

修改MantisBT配置文件

添加以下配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# MantisBT Authentication and LDAP Settings #
$g_login_method = LDAP;
$g_reauthentication = ON;
$g_reauthentication_expiry = TOKEN_EXPIRY_AUTHENTICATED;
$g_ldap_server = 'ldap://devops.iamzhl.top:389';
$g_ldap_root_dn = 'ou=People,dc=iamzhl,dc=top';
$g_ldap_protocol_version = 3;
$g_ldap_organization = '';
$g_ldap_bind_dn = 'cn=Manager,dc=iamzhl,dc=top';
$g_ldap_bind_passwd = '123456';
$g_ldap_uid_field = 'uid';
$g_ldap_realname_field = 'cn';
$g_use_ldap_realname = ON;
$g_use_ldap_email = ON;

打开MantisBT网址,输入用户名密码点击登录

登陆成功后,正常获取用户名

点击右上角的用户名 -> 注销,会正常退出并跳转到登录界面

至此,MantisBT整合LDAP完成。

整合 CAS 单点登录

下载phpCAS放到MantisBT

1
2
3
4
5
# 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 /var/www/html/mantis/phpCAS

修改MantisBT配置文件

1
# vi /var/www/html/mantis/config/config_inc.php

添加CAS认证需要的变量(请按照自己的LDAP服务器进行修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# MantisBT Authentication With CAS Settings #
$g_login_method = CAS;
$g_cas_server = 'devops.iamzhl.top';
$g_cas_port = 8080;
$g_cas_uri = '/cas';
$g_cas_validate = '';
$g_cas_version = '2.0';
$g_cas_debug = '/var/www/html/mantis/cas.log';
$g_cas_saml_attributes = OFF;
$g_cas_saml_map = array( 'name' => '', 'mail' => '' );
$g_cas_use_ldap = ON;
$g_ldap_mantis_uid = 'uid';
$g_cas_ldap_update = OFF;
$g_cas_ldap_update_fields = '';
$g_cas_ldap_update_map = '';
$g_ldap_language_field = '';
$g_ldap_language_keys = '';

修改登录页面

1
# vi /var/www/html/mantis/login_page.php
1
2
3
4
5
6
// 在文件开头的 require_once 部分增加对 phpCAS 的引入
require_once( '/var/www/html/mantis/phpCAS/login_cas.php' );
// 在 $f_username 变量的定义之前添加判断语句,当检测到用户已经认证时,跳转到主页
if( auth_is_user_authenticated() && !current_user_is_anonymous() ) {
print_header_redirect( config_get( 'default_home_page' ) );
}
1
# vi /var/www/html/mantis/login.php
1
2
3
4
5
6
7
// 在判断变量 f_install 的判断语句之后添加下面的判断语句来判断验证方式,若为 CAS ,则利用 auth_cas_get_name 函数来处理
if ( CAS == config_get( 'login_method' ) ) {
# This will detour to the CAS login page if needed
$f_password = '';
$f_username = auth_cas_get_name();
# User is always authenticated by this point
}
1
# vi /var/www/html/mantis/login_password_page.php
1
2
3
4
5
6
7
8
9
10
// 在 $f_username 变量的定义之前添加判断语句,当检测到用户已经认证时,跳转到主页
if( auth_is_user_authenticated() && !current_user_is_anonymous() ) {
print_header_redirect( config_get( 'default_home_page' ) );
}
$f_username = gpc_get_string( 'username', '' );
# zhanghl start
if( $f_username == '' ) {
$f_username = $staffid;
}
# zhanghl end

修改登出页面

1
# vi /var/www/html/mantis/logout_page.php
1
2
3
4
5
6
7
8
9
10
11
12
13
// 在文件开头的 require_once 部分增加对 phpCAS 的引入
require_once( '/var/www/html/mantis/phpCAS/login_cas.php' );
# Cache the current logout redirect page as it will be cleared by auth_logout()
//$t_logout_redirect = auth_logout_redirect_page();

//auth_logout();
phpCAS::setDebug();
phpCAS::setVerbose(true);
phpCAS::handleLogoutRequests();
phpCAS::logout();

//print_header_redirect( $t_logout_redirect, true, false );
print_header_redirect( config_get( 'logout_redirect_page' ), true, false );

修改验证逻辑

1
# vi /var/www/html/mantis/core/authentication_api.php
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
// 在变量 g_cache_current_user_id 的定义后面添加以下函数,定义 CAS 的登录逻辑
/**
* Initialize phpCAS.
*/
function auth_cas_init() {
# phpCAS must be installed in the include path
# or in the Mantis directory.
require_once('/var/www/html/mantis/phpCAS/CAS.php');

static $s_initialized=false;

if (! $s_initialized ) {
phpCAS::setDebug( config_get( 'cas_debug' ) );
## These should be set in config_inc.php
$t_server_version = config_get( 'cas_version' );
$t_server_cas_server = config_get( 'cas_server' );
$t_server_port = config_get( 'cas_port' );
$t_server_uri = config_get( 'cas_uri' );
$t_start_session = (boolean)FALSE; # Mantis takes care of its own session

phpCAS::client($t_server_version, $t_server_cas_server, $t_server_port, $t_server_uri, $t_start_session);
if ($t_server_version == "S1")
phpCAS::setServerSamlValidateURL( config_get( 'cas_validate' ) );
else
phpCAS::setServerProxyValidateURL( config_get( 'cas_validate' ) );
if (method_exists('phpCAS', 'setNoCasServerValidation')) {
// no SSL validation for the CAS server
phpCAS::setNoCasServerValidation();
}

$s_initialized = true;
}

}

/**
* Fetches the user's CAS name, authenticating if needed.
* Can translate CAS login name to Mantis username through LDAP.
*/
function auth_cas_get_name()
{
# Get CAS username from phpCAS
auth_cas_init();
phpCAS::forceAuthentication();
$t_cas_id = phpCAS::getUser();
$t_cas_attribs = phpCAS::getAttributes();

# If needed, translate the CAS username through LDAP
$t_username = $t_cas_id;
if (config_get( 'cas_use_ldap', false )) {
$t_username = auth_cas_ldap_translate( $t_cas_id );
}
elseif (config_get( 'cas_saml_attributes', false )) {
$t_cas_attribmap = config_get( 'cas_saml_map', array() );
$t_cas_attrib_name = $t_cas_attribs[$t_cas_attribmap['name']];
$t_cas_attrib_mail = $t_cas_attribs[$t_cas_attribmap['mail']];
if ( user_get_id_by_name($t_cas_id) == false ) {
user_create( $t_cas_id, '', $t_cas_attrib_mail, null, false, true, $t_cas_attrib_name );
}
}

return $t_username;
}

/**
* Takes an ID string, and looks up the LDAP directory to find
* the matching username for Mantis.
*
* Optionally, also update the user information in the Mantis user
* table.
*
* @param $p_cas_id string Typically, the username given by phpCAS.
* @param $p_update_user bool Whether or not to update user details from LDAP.
*/
function auth_cas_ldap_translate( $p_cas_id, $p_update_user='' )
{

# Please make sure the Mantis CAS and LDAP settings are set in config_inc.php

$t_ldap_organization = config_get( 'ldap_organization' );
$t_ldap_root_dn = config_get( 'ldap_root_dn' );

# Required fields in LDAP for CAS
$t_ldap_language_field = config_get( 'ldap_language_field', '' );
$t_ldap_uid_field = config_get( 'ldap_uid_field', 'uid' ) ;
$t_ldap_mantis_uid = config_get( 'ldap_mantis_uid', 'uid' );
$t_ldap_required = array( $t_ldap_uid_field, $t_ldap_mantis_uid, 'dn' );
if ($t_ldap_language_field) {
// Add language field to attributes list only if it is configured.
$t_ldap_required[] = $t_ldap_language_field;
}
$t_ldap_required = array_combine( $t_ldap_required, $t_ldap_required );

# User-defined fields to fetch from LDAP...
$t_ldap_fields = explode( ',', config_get( 'cas_ldap_update_fields' ) );
$t_ldap_fields = array_combine( $t_ldap_fields, $t_ldap_fields );
# ...which are mapped to Mantis user fields
$t_ldap_map = explode( ',', config_get( 'cas_ldap_update_map' ) );
$t_ldap_map = array_combine( $t_ldap_map, $t_ldap_map );

# Build LDAP search filter, attribute list from CAS ID
$t_search_filter = "(&$t_ldap_organization($t_ldap_uid_field=$p_cas_id))";
$t_search_attrs = array_values($t_ldap_required + $t_ldap_fields); # array union

# Use Mantis ldap_api to connect to LDAP
$t_ds = ldap_connect_bind();
$t_sr = ldap_search( $t_ds, $t_ldap_root_dn, $t_search_filter, $t_search_attrs );
$t_info = ldap_get_entries( $t_ds, $t_sr );
# Parse the LDAP entry to find the Mantis username
if ( $t_info ) {
# Get Mantis username
$t_username = $t_info[0][$t_ldap_mantis_uid][0];

# @@@ The fact that we got here means the user is authenticated
# @@@ by CAS, and has an LDAP entry.
# @@@ We might as well update other user details since we are here.

# If no argument given, check settings
if ( '' == $p_update_user ) {
$p_update_user = config_get( 'cas_ldap_update', FALSE );
}
# If there's a user record, then update it
if ( $p_update_user ) {
# Only proceed if the field map arrays are the same length
$t_field_map = array_combine( $t_ldap_fields, $t_ldap_map );
if ($t_field_map) {
# If user is new, then we must create their account before updating it
# @@@ ( make sure $g_allow_blank_email == ON )
$t_userid = user_get_id_by_name($t_username);
if ( false == $t_userid ) {
user_create( $t_username, '' );
# @@@ Wow, this is pretty lame
$t_userid = user_get_id_by_name($t_username);
}
# @@@ maybe we can optimize this to write all fields at once?
foreach ( $t_field_map as $key=>$t_userfield ) {
if (isset($t_info[0][$key][0])) {
user_set_field( $t_userid, $t_userfield, $t_info[0][$key][0] );
}
}
}

// Update user's overall language preference
if ($t_ldap_language_field) {
$t_language = $t_info[0][$t_ldap_language_field][0];
// Map the LDAP language field to Mantis' language field if needed
$t_language_keys = config_get( 'ldap_language_keys', '');
$t_language_values = config_get( 'ldap_language_values', '');
$t_language_map = array_combine(
explode(',', $t_language_keys),
explode(',', $t_language_values)
);
if (isset($t_language_map[$t_language])) {
$t_language = $t_language_map[$t_language];
}
user_pref_set_pref($t_userid, 'language', $t_language);
}
}
}
ldap_free_result( $t_sr );
ldap_unbind( $t_ds );

return $t_username;
}

/**
* Logs out of CAS, redirecting to Mantis on re-login.
* User should already be logged out of Mantis by the time this is called.
* @see auth_logout()
*/
function auth_cas_logout()
{
$t_path = config_get('path');
auth_cas_init();

if (method_Exists('phpCAS', 'logoutWithUrl')) {
phpCAS::logoutWithUrl($t_path);
} else {
phpCAS::logout($t_path);
}
}
// zhanghl end
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
// 修改 auth_auto_create_user 函数实现 CAS 自动创建用户
function auth_auto_create_user( $p_username, $p_password ) {
$t_login_method = config_get_global( 'login_method' );

// if( $t_login_method == BASIC_AUTH ) {
if ( in_array( $t_login_method, array( BASIC_AUTH, CAS ) ) ) {
# attempt to create the user if using BASIC_AUTH or CAS
$t_auto_create = true;
} else if( $t_login_method == LDAP && ldap_authenticate_by_username( $p_username, $p_password ) ) {
$t_auto_create = true;
} else {
$t_auto_create = false;
}

if ( CAS == config_get( 'login_method' ) ) {
# Redirect to CAS page to logout
auth_cas_logout();
}

if( $t_auto_create ) {
# attempt to create the user
$t_cookie_string = user_create( $p_username, md5( $p_password ) );
if( $t_cookie_string === false ) {
# it didn't work
return false;
}

# ok, we created the user, get the row again
return user_get_id_by_name( $p_username );
}

session_clean();

return false;
}
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
function auth_attempt_login( $p_username, $p_password, $p_perm_login = false ) {
$t_user_id = auth_get_user_id_from_login_name( $p_username );
$t_login_method = config_get( 'login_method' );

if( $t_user_id === false ) {
if ( in_array( $t_login_method, array( BASIC_AUTH, CAS ) ) ) {
# attempt to create the user if using BASIC_AUTH or CAS
# ( note: CAS must have $g_allow_blank_email == ON )
$t_auto_create = true;
}
$t_user_id = auth_auto_create_user( $p_username, $p_password );
if( $t_user_id === false ) {
return false;
}
}

# max. failed login attempts achieved...
if( !user_is_login_request_allowed( $t_user_id ) ) {
return false;
}

# check for anonymous login
if( !user_is_anonymous( $t_user_id ) ) {
# anonymous login didn't work, so check the password
if( !auth_does_password_match( $t_user_id, $p_password ) ) {
user_increment_failed_login_count( $t_user_id );
return false;
}
}

return auth_login_user( $t_user_id, $p_perm_login );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function auth_logout() {
global $g_cache_current_user_id, $g_cache_cookie_valid;

# clear cached userid
user_clear_cache( $g_cache_current_user_id );
current_user_set( null );
$g_cache_cookie_valid = null;

# clear cookies, if they were set
if( auth_clear_cookies() ) {
helper_clear_pref_cookies();
}

if( HTTP_AUTH == config_get_global( 'login_method' ) ) {
auth_http_set_logout_pending( true );
}

elseif ( CAS == config_get( 'login_method' ) ) {
# Redirect to CAS page to logout
auth_cas_logout();
}

session_clean();
}
1
2
3
4
5
6
7
8
9
10
function auth_automatic_logon_bypass_form() {
switch( config_get( 'login_method' ) ) {
case HTTP_AUTH:
return true;
case CAS:
return true;
}
return false;
//return config_get_global( 'login_method' ) == HTTP_AUTH;
}
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
function auth_does_password_match( $p_user_id, $p_test_password ) {
$t_configured_login_method = config_get_global( 'login_method' );

if( LDAP == $t_configured_login_method ) {
return ldap_authenticate( $p_user_id, $p_test_password );
}

elseif ( CAS == $t_configured_login_method ) {
return true;
}

if( !auth_can_use_standard_login( $p_user_id ) ) {
return false;
}

$t_password = user_get_field( $p_user_id, 'password' );
$t_login_methods = array(
MD5,
CRYPT,
PLAIN,
BASIC_AUTH,
CAS,
);

foreach( $t_login_methods as $t_login_method ) {
# pass the stored password in as the salt
if( auth_process_plain_password( $p_test_password, $t_password, $t_login_method ) == $t_password ) {
# Do not support migration to PLAIN, since this would be a crazy thing to do.
# Also if we do, then a user will be able to login by providing the MD5 value
# that is copied from the database. See #8467 for more details.
if( ( $t_configured_login_method != PLAIN && $t_login_method == PLAIN ) ||
( $t_configured_login_method != BASIC_AUTH && $t_login_method == BASIC_AUTH ) ) {
continue;
}

# Check for migration to another login method and test whether the password was encrypted
# with our previously insecure implementation of the CRYPT method
if( ( $t_login_method != $t_configured_login_method ) || (( CRYPT == $t_configured_login_method ) && mb_substr( $t_password, 0, 2 ) == mb_substr( $p_test_password, 0, 2 ) ) ) {
user_set_password( $p_user_id, $p_test_password, true );
}

return true;
}
}

return false;
}
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
function auth_reauthenticate() {
//if( !auth_reauthentication_enabled() || BASIC_AUTH == config_get_global( 'login_method' ) || HTTP_AUTH == config_get_global( 'login_method' ) ) {
if( !auth_reauthentication_enabled() || in_array(config_get('login_method'), array(BASIC_AUTH, HTTP_AUTH, CAS)) ) {
return true;
}

$t_auth_token = token_get( TOKEN_AUTHENTICATED );
if( null != $t_auth_token ) {
token_touch( $t_auth_token['id'], auth_reauthentication_expiry() );
return true;
} else {
$t_anon_account = auth_anonymous_account();
$t_anon_allowed = auth_anonymous_enabled();

$t_user_id = auth_get_current_user_id();
$t_username = user_get_username( $t_user_id );

# check for anonymous login
if( ON == $t_anon_allowed && $t_anon_account == $t_username ) {
return true;
}

$t_request_uri = string_url( $_SERVER['REQUEST_URI'] );

$t_query_params = http_build_query(
array(
'reauthenticate' => 1,
'username' => $t_username,
'return' => $t_request_uri,
),
'', '&'
);

# redirect to login page
print_header_redirect( auth_credential_page( $t_query_params ) );
}
}

新建login_cas.php处理拦截利用CAS认证登录

1
#  vi /var/www/html/mantis/phpCAS/login_cas.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

require_once( 'CAS.php' );
define('CAS_ENABLE', true);
$cas_host = 'devops.iamzhl.top';
$cas_context = '/cas';
$cas_port = 8080;
$cas_real_hosts = array (
'devops.iamzhl.top'
);

phpCAS::setDebug();
phpCAS::setVerbose(true);
phpCAS::client(CAS_VERSION_2_0, $cas_host, $cas_port, $cas_context);
phpCAS::setNoCasServerValidation();
phpCAS::handleLogoutRequests(true, $cas_real_hosts);
phpCAS::forceAuthentication();
?>

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

1
# vi /var/www/html/mantis/phpCAS/source/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;
}

测试

新建log目录

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

打开MantisBT网址,正常跳转至CAS登录界面,网址是http://devops.iamzhl.top:8080/cas/login?service=http%3A%2F%2Fdevops.iamzhl.top%2Fmantis%2Flogin_page.php

如图,输入用户名密码后点击登录,正常登陆后跳转至MantisBT主页,并且正常获取用户名

点击右上角的用户名 -> 注销,会正常退出并跳转到CAS的登出界面