forked from casdoor/casdoor
430 lines
11 KiB
Go
430 lines
11 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 (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/casdoor/casdoor/util"
|
|
)
|
|
|
|
// LarkSyncerProvider implements SyncerProvider for Lark API-based syncers
|
|
type LarkSyncerProvider struct {
|
|
Syncer *Syncer
|
|
}
|
|
|
|
// InitAdapter initializes the Lark syncer (no database adapter needed)
|
|
func (p *LarkSyncerProvider) InitAdapter() error {
|
|
// Lark syncer doesn't need database adapter
|
|
return nil
|
|
}
|
|
|
|
// GetOriginalUsers retrieves all users from Lark API
|
|
func (p *LarkSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
|
|
return p.getLarkUsers()
|
|
}
|
|
|
|
// AddUser adds a new user to Lark (not supported for read-only API)
|
|
func (p *LarkSyncerProvider) AddUser(user *OriginalUser) (bool, error) {
|
|
// Lark syncer is typically read-only
|
|
return false, fmt.Errorf("adding users to Lark is not supported")
|
|
}
|
|
|
|
// UpdateUser updates an existing user in Lark (not supported for read-only API)
|
|
func (p *LarkSyncerProvider) UpdateUser(user *OriginalUser) (bool, error) {
|
|
// Lark syncer is typically read-only
|
|
return false, fmt.Errorf("updating users in Lark is not supported")
|
|
}
|
|
|
|
// TestConnection tests the Lark API connection
|
|
func (p *LarkSyncerProvider) TestConnection() error {
|
|
_, err := p.getLarkAccessToken()
|
|
return err
|
|
}
|
|
|
|
// Close closes any open connections (no-op for Lark API-based syncer)
|
|
func (p *LarkSyncerProvider) Close() error {
|
|
// Lark syncer doesn't maintain persistent connections
|
|
return nil
|
|
}
|
|
|
|
type LarkAccessTokenResp struct {
|
|
Code int `json:"code"`
|
|
Msg string `json:"msg"`
|
|
TenantAccessToken string `json:"tenant_access_token"`
|
|
Expire int `json:"expire"`
|
|
}
|
|
|
|
type LarkUser struct {
|
|
UserId string `json:"user_id"`
|
|
UnionId string `json:"union_id"`
|
|
OpenId string `json:"open_id"`
|
|
Name string `json:"name"`
|
|
EnName string `json:"en_name"`
|
|
Email string `json:"email"`
|
|
Mobile string `json:"mobile"`
|
|
Gender int `json:"gender"`
|
|
Avatar *LarkAvatar `json:"avatar"`
|
|
Status *LarkStatus `json:"status"`
|
|
DepartmentIds []string `json:"department_ids"`
|
|
JobTitle string `json:"job_title"`
|
|
}
|
|
|
|
type LarkAvatar struct {
|
|
Avatar72 string `json:"avatar_72"`
|
|
Avatar240 string `json:"avatar_240"`
|
|
Avatar640 string `json:"avatar_640"`
|
|
AvatarOrigin string `json:"avatar_origin"`
|
|
}
|
|
|
|
type LarkStatus struct {
|
|
IsFrozen bool `json:"is_frozen"`
|
|
IsResigned bool `json:"is_resigned"`
|
|
IsActivated bool `json:"is_activated"`
|
|
IsExited bool `json:"is_exited"`
|
|
}
|
|
|
|
type LarkUserListResp struct {
|
|
Code int `json:"code"`
|
|
Msg string `json:"msg"`
|
|
Data struct {
|
|
Items []*LarkUser `json:"items"`
|
|
HasMore bool `json:"has_more"`
|
|
PageToken string `json:"page_token"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
type LarkDeptListResp struct {
|
|
Code int `json:"code"`
|
|
Msg string `json:"msg"`
|
|
Data struct {
|
|
Items []struct {
|
|
DepartmentId string `json:"department_id"`
|
|
} `json:"items"`
|
|
HasMore bool `json:"has_more"`
|
|
PageToken string `json:"page_token"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
// getLarkDomain returns the Lark API domain based on whether global endpoint is used
|
|
func (p *LarkSyncerProvider) getLarkDomain() string {
|
|
// syncer.Host can be used to specify custom endpoint
|
|
// If empty, default to global endpoint (larksuite.com)
|
|
if p.Syncer.Host != "" {
|
|
return p.Syncer.Host
|
|
}
|
|
return "https://open.larksuite.com"
|
|
}
|
|
|
|
// getLarkAccessToken gets access token from Lark API
|
|
func (p *LarkSyncerProvider) getLarkAccessToken() (string, error) {
|
|
// syncer.User should be the app_id
|
|
// syncer.Password should be the app_secret
|
|
appId := p.Syncer.User
|
|
if appId == "" {
|
|
return "", fmt.Errorf("app_id (user field) is required for Lark syncer")
|
|
}
|
|
|
|
appSecret := p.Syncer.Password
|
|
if appSecret == "" {
|
|
return "", fmt.Errorf("app_secret (password field) is required for Lark syncer")
|
|
}
|
|
|
|
domain := p.getLarkDomain()
|
|
apiUrl := fmt.Sprintf("%s/open-apis/auth/v3/tenant_access_token/internal", domain)
|
|
|
|
postData := map[string]string{
|
|
"app_id": appId,
|
|
"app_secret": appSecret,
|
|
}
|
|
|
|
data, err := p.postJSON(apiUrl, postData)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var tokenResp LarkAccessTokenResp
|
|
err = json.Unmarshal(data, &tokenResp)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if tokenResp.Code != 0 {
|
|
return "", fmt.Errorf("failed to get access token: code=%d, msg=%s",
|
|
tokenResp.Code, tokenResp.Msg)
|
|
}
|
|
|
|
return tokenResp.TenantAccessToken, nil
|
|
}
|
|
|
|
// getLarkDepartments gets all department IDs from Lark API
|
|
func (p *LarkSyncerProvider) getLarkDepartments(accessToken string) ([]string, error) {
|
|
domain := p.getLarkDomain()
|
|
allDeptIds := []string{"0"} // Start with root department
|
|
pageToken := ""
|
|
|
|
for {
|
|
apiUrl := fmt.Sprintf("%s/open-apis/contact/v3/departments?parent_department_id=0&fetch_child=true&page_size=50", domain)
|
|
if pageToken != "" {
|
|
apiUrl += fmt.Sprintf("&page_token=%s", pageToken)
|
|
}
|
|
|
|
data, err := p.getWithAuth(apiUrl, accessToken)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var deptResp LarkDeptListResp
|
|
err = json.Unmarshal(data, &deptResp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if deptResp.Code != 0 {
|
|
return nil, fmt.Errorf("failed to get departments: code=%d, msg=%s",
|
|
deptResp.Code, deptResp.Msg)
|
|
}
|
|
|
|
for _, dept := range deptResp.Data.Items {
|
|
allDeptIds = append(allDeptIds, dept.DepartmentId)
|
|
}
|
|
|
|
if !deptResp.Data.HasMore {
|
|
break
|
|
}
|
|
pageToken = deptResp.Data.PageToken
|
|
}
|
|
|
|
return allDeptIds, nil
|
|
}
|
|
|
|
// getLarkUsersFromDept gets users from a specific department
|
|
func (p *LarkSyncerProvider) getLarkUsersFromDept(accessToken string, deptId string) ([]*LarkUser, error) {
|
|
domain := p.getLarkDomain()
|
|
allUsers := []*LarkUser{}
|
|
pageToken := ""
|
|
|
|
for {
|
|
apiUrl := fmt.Sprintf("%s/open-apis/contact/v3/users/find_by_department?department_id=%s&page_size=50", domain, deptId)
|
|
if pageToken != "" {
|
|
apiUrl += fmt.Sprintf("&page_token=%s", pageToken)
|
|
}
|
|
|
|
data, err := p.getWithAuth(apiUrl, accessToken)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var userResp LarkUserListResp
|
|
err = json.Unmarshal(data, &userResp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if userResp.Code != 0 {
|
|
return nil, fmt.Errorf("failed to get users from dept %s: code=%d, msg=%s",
|
|
deptId, userResp.Code, userResp.Msg)
|
|
}
|
|
|
|
allUsers = append(allUsers, userResp.Data.Items...)
|
|
|
|
if !userResp.Data.HasMore {
|
|
break
|
|
}
|
|
pageToken = userResp.Data.PageToken
|
|
}
|
|
|
|
return allUsers, nil
|
|
}
|
|
|
|
// postJSON sends a POST request with JSON body
|
|
func (p *LarkSyncerProvider) postJSON(url string, data interface{}) ([]byte, error) {
|
|
jsonData, err := json.Marshal(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonData))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respData, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return respData, nil
|
|
}
|
|
|
|
// getWithAuth sends a GET request with authorization header
|
|
func (p *LarkSyncerProvider) getWithAuth(url string, accessToken string) ([]byte, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
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
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// getLarkUsers gets all users from Lark API
|
|
func (p *LarkSyncerProvider) getLarkUsers() ([]*OriginalUser, error) {
|
|
// Get access token
|
|
accessToken, err := p.getLarkAccessToken()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get all departments
|
|
deptIds, err := p.getLarkDepartments(accessToken)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get users from all departments (deduplicate by user_id)
|
|
userMap := make(map[string]*LarkUser)
|
|
for _, deptId := range deptIds {
|
|
users, err := p.getLarkUsersFromDept(accessToken, deptId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, user := range users {
|
|
// Deduplicate users by user_id
|
|
if _, exists := userMap[user.UserId]; !exists {
|
|
userMap[user.UserId] = user
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert Lark users to Casdoor OriginalUser
|
|
originalUsers := []*OriginalUser{}
|
|
for _, larkUser := range userMap {
|
|
originalUser := p.larkUserToOriginalUser(larkUser)
|
|
originalUsers = append(originalUsers, originalUser)
|
|
}
|
|
|
|
return originalUsers, nil
|
|
}
|
|
|
|
// larkUserToOriginalUser converts Lark user to Casdoor OriginalUser
|
|
func (p *LarkSyncerProvider) larkUserToOriginalUser(larkUser *LarkUser) *OriginalUser {
|
|
// Use user_id as name, fallback to union_id or open_id
|
|
userName := larkUser.UserId
|
|
if userName == "" && larkUser.UnionId != "" {
|
|
userName = larkUser.UnionId
|
|
}
|
|
if userName == "" && larkUser.OpenId != "" {
|
|
userName = larkUser.OpenId
|
|
}
|
|
|
|
user := &OriginalUser{
|
|
Id: larkUser.UserId,
|
|
Name: userName,
|
|
DisplayName: larkUser.Name,
|
|
Email: larkUser.Email,
|
|
Phone: larkUser.Mobile,
|
|
Title: larkUser.JobTitle,
|
|
Address: []string{},
|
|
Properties: map[string]string{},
|
|
Groups: []string{},
|
|
Lark: larkUser.UserId, // Link Lark provider account
|
|
}
|
|
|
|
// Set avatar if available
|
|
if larkUser.Avatar != nil {
|
|
if larkUser.Avatar.Avatar240 != "" {
|
|
user.Avatar = larkUser.Avatar.Avatar240
|
|
} else if larkUser.Avatar.Avatar72 != "" {
|
|
user.Avatar = larkUser.Avatar.Avatar72
|
|
}
|
|
}
|
|
|
|
// Set gender
|
|
switch larkUser.Gender {
|
|
case 1:
|
|
user.Gender = "Male"
|
|
case 2:
|
|
user.Gender = "Female"
|
|
default:
|
|
user.Gender = ""
|
|
}
|
|
|
|
// Set IsForbidden based on status
|
|
// User is forbidden if frozen, resigned, not activated, or exited
|
|
if larkUser.Status != nil {
|
|
if larkUser.Status.IsFrozen || larkUser.Status.IsResigned || !larkUser.Status.IsActivated || larkUser.Status.IsExited {
|
|
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 Lark (not implemented yet)
|
|
func (p *LarkSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
|
// TODO: Implement Lark group sync
|
|
return []*OriginalGroup{}, nil
|
|
}
|
|
|
|
// GetOriginalUserGroups retrieves the group IDs that a user belongs to (not implemented yet)
|
|
func (p *LarkSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
|
// TODO: Implement Lark user group membership sync
|
|
return []string{}, nil
|
|
}
|