diff --git a/config/filter.php b/config/filter.php index 7d1f8fcd60..e35d666e1f 100644 --- a/config/filter.php +++ b/config/filter.php @@ -371,8 +371,8 @@ $filter->repo->diff->cookie['repoPairs'] = 'array'; $filter->repo->view->cookie['repoPairs'] = 'array'; $filter->repo->ajaxsynccommit->cookie['syncBranch'] = 'reg::any'; -$filter->webhook->bind->get['selectedDepts'] = 'reg::checked'; -$filter->webhook->bind->cookie['selectedDepts'] = 'reg::checked'; +$filter->webhook->bind->get['selectedDepts'] = 'reg::any'; +$filter->webhook->bind->cookie['selectedDepts'] = 'reg::any'; $filter->search->index->get['words'] = 'reg::any'; $filter->search->index->get['type'] = 'code'; diff --git a/config/zentaopms.php b/config/zentaopms.php index 8508db61cd..47748473ca 100644 --- a/config/zentaopms.php +++ b/config/zentaopms.php @@ -142,6 +142,9 @@ $config->openMethods[] = 'my.changepassword'; $config->openMethods[] = 'my.profile'; $config->openMethods[] = 'my.settutorialconfig'; $config->openMethods[] = 'doc.selectlibtype'; +$config->openMethods[] = 'sso.getfeishusso'; +$config->openMethods[] = 'sso.feishuauthen'; +$config->openMethods[] = 'sso.feishulogin'; /* Define the tables. */ define('TABLE_COMPANY', '`' . $config->db->prefix . 'company`'); diff --git a/lib/dingapi/dingapi.class.php b/lib/dingapi/dingapi.class.php index a9d2e2d388..dacf0a52b7 100644 --- a/lib/dingapi/dingapi.class.php +++ b/lib/dingapi/dingapi.class.php @@ -136,7 +136,7 @@ class dingapi $node['id'] = $deptID; $node['pId'] = $parentID; $node['name'] = $deptName; - if($parentID == 0) $node['open'] = true; + $node['open'] = true; $tree[] = $node; } diff --git a/lib/feishuapi/feishuapi.class.php b/lib/feishuapi/feishuapi.class.php index 31b26e85c1..a7a3daccfe 100644 --- a/lib/feishuapi/feishuapi.class.php +++ b/lib/feishuapi/feishuapi.class.php @@ -52,17 +52,23 @@ class feishuapi * @access public * @return array */ - public function getAllUsers() + public function getAllUsers($selectedDepts = '') { + $selectedDepts = trim($selectedDepts); + if(empty($selectedDepts)) return array('result' => 'fail', 'message' => 'nodept'); + set_time_limit(0); $users = array(); - $depts = $this->getDepts(); + $depts = explode(',', $selectedDepts); + $depts = array_flip($depts); + unset($depts[1]); + if(empty($depts)) return array('result' => 'fail', 'message' => 'nodept'); /* Get users by dept. */ foreach($depts as $deptID => $count) { - if($deptID and empty($count)) continue; + if(empty($deptID)) continue; $pageToken = ''; while(true) @@ -138,6 +144,46 @@ class feishuapi return $depts; } + /** + * Get department tree structure. + * + * @access public + * @return array + */ + public function getDeptTree() + { + $depts = array('result' => 'success', 'data' => array()); + + /* Gets the enterprise name. */ + $response = $this->queryAPI($this->apiUrl . "tenant/v2/tenant/query", '', array(CURLOPT_CUSTOMREQUEST => "GET")); + $company = array('id' => '1', 'pId' => '0', 'name' => $response->data->tenant->name, 'open' => 1); + $data = array($company); + + /* Get depts by parent dept. */ + $pageToken = ''; + while(true) + { + $response = $this->queryAPI($this->apiUrl . "contact/v3/departments?parent_department_id=0" . ($pageToken ? "&page_token={$pageToken}" : '') . "&fetch_child=true", '', array(CURLOPT_CUSTOMREQUEST => "GET")); + if(isset($response->data->items)) + { + foreach($response->data->items as $key => $dept) + { + $key ++; + $data[$key]['id'] = $dept->open_department_id; + $data[$key]['pId'] = empty($dept->parent_department_id) ? 1 : $dept->parent_department_id; + $data[$key]['name'] = $dept->name; + $data[$key]['open'] = 1; + } + } + + if(!isset($response->data->page_token)) break; + $pageToken = $response->data->page_token; + } + + $depts['data'] = $data; + return $depts; + } + /** * Send message * @@ -179,6 +225,11 @@ class feishuapi if(empty($response)) $this->errors = $errors; if(isset($response->code)) $this->errors[$response->code] = "Errcode:{$response->code}, Errmsg:{$response->msg}"; + if(!empty($this->errors)) + { + echo js::error(array_shift($this->errors)); + die(js::locate(helper::createLink('webhook', 'browse'))); + } return false; } diff --git a/module/sso/control.php b/module/sso/control.php index ad3cf3d03c..23cd3d59f1 100644 --- a/module/sso/control.php +++ b/module/sso/control.php @@ -305,4 +305,127 @@ class sso extends control $datas['bug'] = $this->dao->select("id, title")->from(TABLE_BUG)->where('assignedTo')->eq($account)->andWhere('status')->eq('active')->andWhere('deleted')->eq(0)->fetchPairs(); die(json_encode($datas)); } + + /** + * Get the link to the Feishu single sign-on configuration. + * + * @access public + * @return void + */ + public function getFeishuSSO() + { + $httpType = ((isset($_SERVER['HTTPS']) and $_SERVER['HTTPS'] == 'on') or (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')) ? 'https://' : 'http://'; + $applicationHome = $httpType . $_SERVER['HTTP_HOST'] . $this->createLink('sso', 'feishuAuthen'); + $redirectLink = $httpType . $_SERVER['HTTP_HOST'] . $this->createLink('sso', 'feishuLogin'); + + echo $this->lang->sso->homeURL . $applicationHome; + echo '
'; + echo $this->lang->sso->redirectURL . $redirectLink; + } + + /** + * Get the pre-authorization code for Feishu code. + * + * @access public + * @return void + */ + public function feishuAuthen() + { + $httpType = ((isset($_SERVER['HTTPS']) and $_SERVER['HTTPS'] == 'on') or (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) and $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')) ? 'https://' : 'http://'; + $redirectURI = $httpType . $_SERVER['HTTP_HOST'] . $this->createLink('sso', 'feishuLogin'); + $redirectURI = urlencode($redirectURI); + + $feishuConfig = $this->loadModel('webhook')->getByType('feishuuser'); + if(empty($feishuConfig)) $this->showError($this->lang->sso->feishuConfigEmpty); + + $appConfig = json_decode($feishuConfig->secret); + $appID = $appConfig->appId; + + $url = "https://open.feishu.cn/open-apis/authen/v1/index?redirect_uri=%s&app_id=%s"; + $url = sprintf($url, $redirectURI, $appID, $state); + header("location: $url"); + } + + /** + * Get the identity of the logged-in user. + * + * @param string $code + * @access public + * @return void + */ + public function feishuLogin($code = '') + { + if($this->config->requestType == 'PATH_INFO') + { + $params = $_SERVER["QUERY_STRING"]; + parse_str($params, $params); + if(isset($params['code'])) $code = $params['code']; + } + + $feishuConfig = $this->loadModel('webhook')->getByType('feishuuser'); + if(empty($feishuConfig)) $this->showError($this->lang->sso->feishuConfigEmpty); + $appConfig = json_decode($feishuConfig->secret); + + /* Obtain the access credentials of the Feishu app. */ + $appUrl = 'https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal'; + $appParams = array('app_id' => $appConfig->appId, 'app_secret' => $appConfig->appSecret); + $appResult = $this->sso->http($appUrl, $appParams, 'POST', 'json'); + + if(empty($appResult)) $this->showError($this->lang->sso->feishuResponseEmpty); + $appInfo = json_decode($appResult); + + if(!isset($appInfo->msg) or $appInfo->msg != 'ok') $this->showError($appResult); + $accessToken = $appInfo->app_access_token; + + /* Verify the identity of the logged in user. */ + $tokenUrl = 'https://open.feishu.cn/open-apis/authen/v1/refresh_access_token'; + $tokenHeaders = array('Authorization: Bearer ' . $accessToken); + $tokenParams = array('grant_type' => 'authorization_code', 'code' => $code); + $tokenResult = $this->sso->http($tokenUrl, $tokenParams, 'POST', 'json', array(), $tokenHeaders); + + if(empty($tokenResult)) $this->showError($this->lang->sso->feishuResponseEmpty); + $tokenInfo = json_decode($tokenResult); + + if(!isset($tokenInfo->msg) or $tokenInfo->msg != 'success') $this->showError($tokenResult); + $userToken = $tokenInfo->data->access_token; + + /* Get login user information. */ + $userUrl = 'https://open.feishu.cn/open-apis/authen/v1/user_info'; + $userHeaders = array('Authorization: Bearer ' . $userToken); + $userResult = $this->sso->http($userUrl, array(), 'GET', 'json', array(), $userHeaders); + + if(empty($userResult)) $this->showError($this->lang->sso->feishuResponseEmpty); + $userInfo = json_decode($userResult); + + if(!isset($userInfo->msg) or $userInfo->msg != 'success') $this->showError($userResult); + $openID = $userInfo->data->open_id; + + /* Get the user relationship bound in webhook. */ + $account = $this->loadModel('webhook')->getBindAccount($feishuConfig->id, 'webhook', $openID); + if(empty($account)) $this->showError($this->lang->sso->unbound); + + $user = $this->loadModel('user')->getById($account); + $password = $user->password; + $this->session->set('rand', ''); + $user = $this->user->identify($account, $password); + $this->user->login($user); + + $indexUrl = $this->createLink('my', 'index'); + header("location: $indexUrl"); + } + + /** + * Display the error message. + * + * @param string $message + * @access public + * @return void + */ + public function showError($message = '') + { + $this->view->title = $this->lang->sso->deny; + $this->view->message = $message; + $this->display('sso', 'error'); + die(); + } } diff --git a/module/sso/lang/zh-cn.php b/module/sso/lang/zh-cn.php index c8c3fbd11b..2cad5b1681 100644 --- a/module/sso/lang/zh-cn.php +++ b/module/sso/lang/zh-cn.php @@ -33,7 +33,14 @@ $lang->sso->help = <<1、接口地址的填写,如果是PATH_INFO :http://ZDOO网址/sys/sso-check.html,如果是GET:http://ZDOO网址/sys/index.php?m=sso&f=check

2、代号和密钥必须与ZDOO后台设置的一致。

EOD; +$lang->sso->deny = '访问受限'; $lang->sso->bindNotice = '添加的新用户暂时没有权限,需要联系禅道管理员,给该用户分配权限。'; $lang->sso->bindNoPassword = '密码不能为空'; $lang->sso->bindNoUser = '该用户的登录密码错误,或该用户不存在!'; $lang->sso->bindHasAccount = '该用户名已经存在,请更换用户名,或直接绑定到该用户。'; + +$lang->sso->homeURL = '飞书主页配置URL:'; +$lang->sso->redirectURL = '飞书重定向配置URL:'; +$lang->sso->feishuConfigEmpty = '请在[后台][通知][Webhook]功能中配置(飞书工作消息通知)'; +$lang->sso->feishuResponseEmpty = '请求响应信息为空'; +$lang->sso->unbound = '当前飞书用户未在禅道webhook功能中进行用户关系绑定'; diff --git a/module/sso/model.php b/module/sso/model.php index 80a73e9938..851972e18b 100644 --- a/module/sso/model.php +++ b/module/sso/model.php @@ -105,4 +105,60 @@ class ssoModel extends model if(dao::isError()) return array('status' => 'fail', 'data' => dao::getError()); return array('status' => 'success', 'id' => $this->dao->lastInsertId()); } + + /** + * Initiate a request. + * + * @param string $url + * @param array $data + * @param string $mehtod GET|POST|PATCH + * @param string $dataType data|json + * @param array $options This is option and value pair, like CURLOPT_HEADER => true. Use curl_setopt function to set options. + * @param array $headers Set request headers. + * @static + * @access public + * @return string + */ + public function http($url, $data = array(), $method = 'POST', $dataType = 'data', $options = array(), $headers = array()) + { + global $lang, $app; + if(!extension_loaded('curl')) die($this->lang->error->noCurlExt); + + if(!is_array($headers)) $headers = (array)$headers; + $headers[] = "API-RemoteIP: " . zget($_SERVER, 'REMOTE_ADDR', ''); + if($dataType == 'json') + { + $headers[] = 'Content-Type: application/json;charset=utf-8'; + if(!empty($data)) $data = json_encode($data); + } + + $curl = curl_init(); + curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); + curl_setopt($curl, CURLOPT_USERAGENT, 'Sae T OAuth2 v0.1'); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 30); + curl_setopt($curl, CURLOPT_TIMEOUT, 30); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE); + curl_setopt($curl, CURLOPT_ENCODING, ""); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE); + curl_setopt($curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); + curl_setopt($curl, CURLOPT_HEADER, FALSE); + curl_setopt($curl, CURLINFO_HEADER_OUT, TRUE); + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + curl_setopt($curl, CURLOPT_URL, $url); + + if(!empty($data)) + { + if($method == 'POST') curl_setopt($curl, CURLOPT_POST, true); + if($method == 'PATCH') curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'PATCH'); + curl_setopt($curl, CURLOPT_POSTFIELDS, $data); + } + + if($options) curl_setopt_array($curl, $options); + + $response = curl_exec($curl); + curl_close($curl); + + return $response; + } } diff --git a/module/sso/view/error.html.php b/module/sso/view/error.html.php new file mode 100644 index 0000000000..cd6e33b62c --- /dev/null +++ b/module/sso/view/error.html.php @@ -0,0 +1,26 @@ + + * @package ZenTaoPMS + * @version $Id: deny.html.php 4129 2013-01-18 01:58:14Z wwccss $ + */ +include '../../common/view/header.lite.html.php'; +?> +
+ +
+ + diff --git a/module/webhook/control.php b/module/webhook/control.php index fcc4d174a7..d9e9c14268 100644 --- a/module/webhook/control.php +++ b/module/webhook/control.php @@ -211,14 +211,14 @@ class webhook extends control elseif($webhook->type == 'wechatuser') { $this->app->loadClass('wechatapi', true); - $wechatApi = new wechatapi($webhook->secret->appKey, $webhook->secret->appSecret, $webhook->secret->agentId); - $response = $wechatApi->getAllUsers(); + $wechatApi = new wechatapi($webhook->secret->appKey, $webhook->secret->appSecret, $webhook->secret->agentId); + $response = $wechatApi->getAllUsers(); } elseif($webhook->type == 'feishuuser') { $this->app->loadClass('feishuapi', true); - $feishuApi = new feishuapi($webhook->secret->appId, $webhook->secret->appSecret); - $response = $feishuApi->getAllUsers(); + $feishuApi = new feishuapi($webhook->secret->appId, $webhook->secret->appSecret); + $response = $feishuApi->getAllUsers($selectedDepts); } if($response['result'] == 'fail') @@ -275,7 +275,7 @@ class webhook extends control public function chooseDept($id) { $webhook = $this->webhook->getById($id); - if($webhook->type != 'dinguser' && $webhook->type != 'wechatuser') + if($webhook->type != 'dinguser' && $webhook->type != 'wechatuser' && $webhook->type != 'feishuuser') { echo js::alert($this->lang->webhook->note->bind); die(js::locate($this->createLink('webhook', 'browse'))); @@ -289,6 +289,13 @@ class webhook extends control $response = $dingapi->getDeptTree(); } + if($webhook->type == 'feishuuser') + { + $this->app->loadClass('feishuapi', true); + $feishuApi = new feishuapi($webhook->secret->appId, $webhook->secret->appSecret); + $response = $feishuApi->getDeptTree(); + } + if($response['result'] == 'fail') { echo js::error($response['message']); diff --git a/module/webhook/model.php b/module/webhook/model.php index 54852bf529..bfc9b6d640 100644 --- a/module/webhook/model.php +++ b/module/webhook/model.php @@ -24,6 +24,32 @@ class webhookModel extends model return $webhook; } + /** + * Get a webhook by type. + * + * @param int $type + * @access public + * @return object + */ + public function getByType($type) + { + $webhook = $this->dao->select('*')->from(TABLE_WEBHOOK)->where('type')->eq($type)->fetch(); + return $webhook; + } + + /** + * Get a webhook by type. + * + * @param int $type + * @access public + * @return object + */ + public function getBindAccount($webhookID, $webhookType, $openID) + { + $account = $this->dao->select('account')->from(TABLE_OAUTH)->where('providerID')->eq($webhookID)->andWhere('providerType')->eq($webhookType)->andWhere('openID')->eq($openID)->fetch('account'); + return $account; + } + /** * Get webhook list. * diff --git a/module/webhook/view/browse.html.php b/module/webhook/view/browse.html.php index f3fef5b1eb..e297af5fa7 100644 --- a/module/webhook/view/browse.html.php +++ b/module/webhook/view/browse.html.php @@ -34,8 +34,8 @@ url;?> type == 'dinguser') common::printIcon('webhook', 'chooseDept', "webhookID=$id", '', 'list', 'link'); - if($webhook->type == 'wechatuser' or $webhook->type == 'feishuuser') common::printIcon('webhook', 'bind', "webhookID=$id", '', 'list', 'link'); + if($webhook->type == 'dinguser' or $webhook->type == 'feishuuser') common::printIcon('webhook', 'chooseDept', "webhookID=$id", '', 'list', 'link'); + if($webhook->type == 'wechatuser') common::printIcon('webhook', 'bind', "webhookID=$id", '', 'list', 'link'); common::printIcon('webhook', 'log', "webhookID=$id", '', 'list', 'file-text'); common::printIcon('webhook', 'edit', "webhookID=$id", '', 'list'); if(common::hasPriv('webhook', 'delete')) diff --git a/module/webhook/view/choosedept.html.php b/module/webhook/view/choosedept.html.php index ff5bff4f80..20df0198d0 100644 --- a/module/webhook/view/choosedept.html.php +++ b/module/webhook/view/choosedept.html.php @@ -28,9 +28,13 @@