Files
casdoor/object/syncer_lark.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
}