Files
casdoor/object/syncer_wecom.go

319 lines
8.6 KiB
Go

// Copyright 2025 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package object
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/casdoor/casdoor/util"
)
// WecomSyncerProvider implements SyncerProvider for WeCom (WeChat Work) API-based syncers
type WecomSyncerProvider struct {
Syncer *Syncer
}
// InitAdapter initializes the WeCom syncer (no database adapter needed)
func (p *WecomSyncerProvider) InitAdapter() error {
// WeCom syncer doesn't need database adapter
return nil
}
// GetOriginalUsers retrieves all users from WeCom API
func (p *WecomSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
return p.getWecomUsers()
}
// AddUser adds a new user to WeCom (not supported for read-only API)
func (p *WecomSyncerProvider) AddUser(user *OriginalUser) (bool, error) {
// WeCom syncer is typically read-only
return false, fmt.Errorf("adding users to WeCom is not supported")
}
// UpdateUser updates an existing user in WeCom (not supported for read-only API)
func (p *WecomSyncerProvider) UpdateUser(user *OriginalUser) (bool, error) {
// WeCom syncer is typically read-only
return false, fmt.Errorf("updating users in WeCom is not supported")
}
// TestConnection tests the WeCom API connection
func (p *WecomSyncerProvider) TestConnection() error {
_, err := p.getWecomAccessToken()
return err
}
// Close closes any open connections (no-op for WeCom API-based syncer)
func (p *WecomSyncerProvider) Close() error {
// WeCom syncer doesn't maintain persistent connections
return nil
}
type WecomAccessTokenResp struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}
type WecomUser struct {
UserId string `json:"userid"`
Name string `json:"name"`
Department []int `json:"department"`
Position string `json:"position"`
Mobile string `json:"mobile"`
Gender string `json:"gender"`
Email string `json:"email"`
Avatar string `json:"avatar"`
Status int `json:"status"`
Enable int `json:"enable"`
}
type WecomUserListResp struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
Userlist []*WecomUser `json:"userlist"`
}
type WecomDeptListResp struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
Department []struct {
Id int `json:"id"`
} `json:"department"`
}
// getWecomAccessToken gets access token from WeCom API
func (p *WecomSyncerProvider) getWecomAccessToken() (string, error) {
apiUrl := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s",
url.QueryEscape(p.Syncer.User), url.QueryEscape(p.Syncer.Password))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", apiUrl, nil)
if err != nil {
return "", err
}
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var tokenResp WecomAccessTokenResp
err = json.Unmarshal(data, &tokenResp)
if err != nil {
return "", err
}
if tokenResp.Errcode != 0 {
return "", fmt.Errorf("failed to get access token: errcode=%d, errmsg=%s",
tokenResp.Errcode, tokenResp.Errmsg)
}
return tokenResp.AccessToken, nil
}
// getWecomDepartments gets all department IDs from WeCom API
func (p *WecomSyncerProvider) getWecomDepartments(accessToken string) ([]int, error) {
apiUrl := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=%s",
url.QueryEscape(accessToken))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", apiUrl, nil)
if err != nil {
return nil, err
}
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var deptResp WecomDeptListResp
err = json.Unmarshal(data, &deptResp)
if err != nil {
return nil, err
}
if deptResp.Errcode != 0 {
return nil, fmt.Errorf("failed to get departments: errcode=%d, errmsg=%s",
deptResp.Errcode, deptResp.Errmsg)
}
deptIds := []int{}
for _, dept := range deptResp.Department {
deptIds = append(deptIds, dept.Id)
}
return deptIds, nil
}
// getWecomUsersFromDept gets users from a specific department
func (p *WecomSyncerProvider) getWecomUsersFromDept(accessToken string, deptId int) ([]*WecomUser, error) {
apiUrl := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/user/list?access_token=%s&department_id=%d",
url.QueryEscape(accessToken), deptId)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", apiUrl, nil)
if err != nil {
return nil, err
}
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var userResp WecomUserListResp
err = json.Unmarshal(data, &userResp)
if err != nil {
return nil, err
}
if userResp.Errcode != 0 {
return nil, fmt.Errorf("failed to get users from dept %d: errcode=%d, errmsg=%s",
deptId, userResp.Errcode, userResp.Errmsg)
}
return userResp.Userlist, nil
}
// getWecomUsers gets all users from WeCom API
func (p *WecomSyncerProvider) getWecomUsers() ([]*OriginalUser, error) {
// Get access token
accessToken, err := p.getWecomAccessToken()
if err != nil {
return nil, err
}
// Get all departments
deptIds, err := p.getWecomDepartments(accessToken)
if err != nil {
return nil, err
}
// Get users from all departments (deduplicate by userid)
userMap := make(map[string]*WecomUser)
for _, deptId := range deptIds {
users, err := p.getWecomUsersFromDept(accessToken, deptId)
if err != nil {
return nil, err
}
for _, user := range users {
// Deduplicate users by userid
if _, exists := userMap[user.UserId]; !exists {
userMap[user.UserId] = user
}
}
}
// Convert WeCom users to Casdoor OriginalUser
originalUsers := []*OriginalUser{}
for _, wecomUser := range userMap {
originalUser := p.wecomUserToOriginalUser(wecomUser)
originalUsers = append(originalUsers, originalUser)
}
return originalUsers, nil
}
// wecomUserToOriginalUser converts WeCom user to Casdoor OriginalUser
func (p *WecomSyncerProvider) wecomUserToOriginalUser(wecomUser *WecomUser) *OriginalUser {
user := &OriginalUser{
Id: wecomUser.UserId,
Name: wecomUser.UserId,
DisplayName: wecomUser.Name,
Email: wecomUser.Email,
Phone: wecomUser.Mobile,
Avatar: wecomUser.Avatar,
Title: wecomUser.Position,
Address: []string{},
Properties: map[string]string{},
Groups: []string{},
Wecom: wecomUser.UserId, // Link WeCom provider account
}
// Set gender
switch wecomUser.Gender {
case "1":
user.Gender = "Male"
case "2":
user.Gender = "Female"
default:
user.Gender = ""
}
// Set IsForbidden based on status
// status: 1=activated, 2=disabled, 4=not activated, 5=quit
// enable: 1=enabled, 0=disabled
if wecomUser.Status == 2 || wecomUser.Status == 4 || wecomUser.Status == 5 || wecomUser.Enable == 0 {
user.IsForbidden = true
} else {
user.IsForbidden = false
}
// Set CreatedTime to current time if not set
if user.CreatedTime == "" {
user.CreatedTime = util.GetCurrentTime()
}
return user
}
// GetOriginalGroups retrieves all groups from WeCom (not implemented yet)
func (p *WecomSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
// TODO: Implement WeCom group sync
return []*OriginalGroup{}, nil
}
// GetOriginalUserGroups retrieves the group IDs that a user belongs to (not implemented yet)
func (p *WecomSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
// TODO: Implement WeCom user group membership sync
return []string{}, nil
}