forked from casdoor/casdoor
feat: support group sync in Google Workspace syncer (#4962)
This commit is contained in:
@@ -370,3 +370,15 @@ func (p *ActiveDirectorySyncerProvider) adEntryToOriginalUser(entry *goldap.Entr
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// GetOriginalGroups retrieves all groups from Active Directory (not implemented yet)
|
||||
func (p *ActiveDirectorySyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
||||
// TODO: Implement Active Directory group sync
|
||||
return []*OriginalGroup{}, nil
|
||||
}
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to (not implemented yet)
|
||||
func (p *ActiveDirectorySyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
||||
// TODO: Implement Active Directory user group membership sync
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
@@ -274,3 +274,15 @@ func (p *AzureAdSyncerProvider) getAzureAdOriginalUsers() ([]*OriginalUser, erro
|
||||
|
||||
return originalUsers, nil
|
||||
}
|
||||
|
||||
// GetOriginalGroups retrieves all groups from Azure AD (not implemented yet)
|
||||
func (p *AzureAdSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
||||
// TODO: Implement Azure AD group sync
|
||||
return []*OriginalGroup{}, nil
|
||||
}
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to (not implemented yet)
|
||||
func (p *AzureAdSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
||||
// TODO: Implement Azure AD user group membership sync
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
@@ -60,9 +60,19 @@ func addSyncerJob(syncer *Syncer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Sync groups as well
|
||||
err = syncer.syncGroups()
|
||||
if err != nil {
|
||||
// Log error but don't fail the entire sync
|
||||
fmt.Printf("Warning: syncGroups() error: %s\n", err.Error())
|
||||
}
|
||||
|
||||
schedule := fmt.Sprintf("@every %ds", syncer.SyncInterval)
|
||||
cron := getCronMap(syncer.Name)
|
||||
_, err = cron.AddFunc(schedule, syncer.syncUsersNoError)
|
||||
_, err = cron.AddFunc(schedule, func() {
|
||||
syncer.syncUsersNoError()
|
||||
syncer.syncGroupsNoError()
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -164,3 +164,15 @@ func (t dsnConnector) Connect(ctx context.Context) (driver.Conn, error) {
|
||||
func (t dsnConnector) Driver() driver.Driver {
|
||||
return t.driver
|
||||
}
|
||||
|
||||
// GetOriginalGroups retrieves all groups from Database (not implemented yet)
|
||||
func (p *DatabaseSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
||||
// TODO: Implement Database group sync
|
||||
return []*OriginalGroup{}, nil
|
||||
}
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to (not implemented yet)
|
||||
func (p *DatabaseSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
||||
// TODO: Implement Database user group membership sync
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
@@ -384,3 +384,15 @@ func (p *DingtalkSyncerProvider) dingtalkUserToOriginalUser(dingtalkUser *Dingta
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// GetOriginalGroups retrieves all groups from DingTalk (not implemented yet)
|
||||
func (p *DingtalkSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
||||
// TODO: Implement DingTalk group sync
|
||||
return []*OriginalGroup{}, nil
|
||||
}
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to (not implemented yet)
|
||||
func (p *DingtalkSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
||||
// TODO: Implement DingTalk user group membership sync
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ func (p *GoogleWorkspaceSyncerProvider) getAdminService() (*admin.Service, error
|
||||
PrivateKey: []byte(serviceAccount.PrivateKey),
|
||||
Scopes: []string{
|
||||
admin.AdminDirectoryUserReadonlyScope,
|
||||
admin.AdminDirectoryGroupReadonlyScope,
|
||||
},
|
||||
TokenURL: google.JWTTokenURL,
|
||||
Subject: adminEmail, // Impersonate the admin user
|
||||
@@ -202,12 +203,189 @@ func (p *GoogleWorkspaceSyncerProvider) getGoogleWorkspaceOriginalUsers() ([]*Or
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get all groups and their members to build a user-to-groups mapping
|
||||
// This avoids N+1 queries by fetching group memberships upfront
|
||||
userGroupsMap, err := p.buildUserGroupsMap(service)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: failed to fetch group memberships: %v. Users will have no groups assigned.\n", err)
|
||||
userGroupsMap = make(map[string][]string)
|
||||
}
|
||||
|
||||
// Convert Google Workspace users to Casdoor OriginalUser
|
||||
originalUsers := []*OriginalUser{}
|
||||
for _, gwUser := range gwUsers {
|
||||
originalUser := p.googleWorkspaceUserToOriginalUser(gwUser)
|
||||
|
||||
// Assign groups from the pre-built map
|
||||
if groups, exists := userGroupsMap[gwUser.PrimaryEmail]; exists {
|
||||
originalUser.Groups = groups
|
||||
} else {
|
||||
originalUser.Groups = []string{}
|
||||
}
|
||||
|
||||
originalUsers = append(originalUsers, originalUser)
|
||||
}
|
||||
|
||||
return originalUsers, nil
|
||||
}
|
||||
|
||||
// buildUserGroupsMap builds a map of user email to group emails by iterating through all groups
|
||||
// and their members. This is more efficient than querying groups for each user individually.
|
||||
func (p *GoogleWorkspaceSyncerProvider) buildUserGroupsMap(service *admin.Service) (map[string][]string, error) {
|
||||
userGroupsMap := make(map[string][]string)
|
||||
|
||||
// Get all groups
|
||||
groups, err := p.getGoogleWorkspaceGroups(service)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch groups: %v", err)
|
||||
}
|
||||
|
||||
// For each group, get its members and populate the user-to-groups map
|
||||
for _, group := range groups {
|
||||
members, err := p.getGroupMembers(service, group.Id)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: failed to get members for group %s: %v\n", group.Email, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Add this group to each member's group list
|
||||
for _, member := range members {
|
||||
userGroupsMap[member.Email] = append(userGroupsMap[member.Email], group.Email)
|
||||
}
|
||||
}
|
||||
|
||||
return userGroupsMap, nil
|
||||
}
|
||||
|
||||
// getGroupMembers retrieves all members of a specific group
|
||||
func (p *GoogleWorkspaceSyncerProvider) getGroupMembers(service *admin.Service, groupId string) ([]*admin.Member, error) {
|
||||
allMembers := []*admin.Member{}
|
||||
pageToken := ""
|
||||
|
||||
for {
|
||||
call := service.Members.List(groupId).MaxResults(500)
|
||||
if pageToken != "" {
|
||||
call = call.PageToken(pageToken)
|
||||
}
|
||||
|
||||
resp, err := call.Do()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list members: %v", err)
|
||||
}
|
||||
|
||||
allMembers = append(allMembers, resp.Members...)
|
||||
|
||||
// Handle pagination
|
||||
if resp.NextPageToken == "" {
|
||||
break
|
||||
}
|
||||
pageToken = resp.NextPageToken
|
||||
}
|
||||
|
||||
return allMembers, nil
|
||||
}
|
||||
|
||||
// GetOriginalGroups retrieves all groups from Google Workspace
|
||||
func (p *GoogleWorkspaceSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
||||
// Get Admin SDK service
|
||||
service, err := p.getAdminService()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get all groups from Google Workspace
|
||||
gwGroups, err := p.getGoogleWorkspaceGroups(service)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert Google Workspace groups to Casdoor OriginalGroup
|
||||
originalGroups := []*OriginalGroup{}
|
||||
for _, gwGroup := range gwGroups {
|
||||
originalGroup := p.googleWorkspaceGroupToOriginalGroup(gwGroup)
|
||||
originalGroups = append(originalGroups, originalGroup)
|
||||
}
|
||||
|
||||
return originalGroups, nil
|
||||
}
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to
|
||||
func (p *GoogleWorkspaceSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
||||
// Get Admin SDK service
|
||||
service, err := p.getAdminService()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get groups for the user
|
||||
groupIds := []string{}
|
||||
pageToken := ""
|
||||
|
||||
for {
|
||||
call := service.Groups.List().UserKey(userId)
|
||||
if pageToken != "" {
|
||||
call = call.PageToken(pageToken)
|
||||
}
|
||||
|
||||
resp, err := call.Do()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list user groups: %v", err)
|
||||
}
|
||||
|
||||
for _, group := range resp.Groups {
|
||||
groupIds = append(groupIds, group.Email)
|
||||
}
|
||||
|
||||
// Handle pagination
|
||||
if resp.NextPageToken == "" {
|
||||
break
|
||||
}
|
||||
pageToken = resp.NextPageToken
|
||||
}
|
||||
|
||||
return groupIds, nil
|
||||
}
|
||||
|
||||
// getGoogleWorkspaceGroups gets all groups from Google Workspace using Admin SDK API
|
||||
func (p *GoogleWorkspaceSyncerProvider) getGoogleWorkspaceGroups(service *admin.Service) ([]*admin.Group, error) {
|
||||
allGroups := []*admin.Group{}
|
||||
pageToken := ""
|
||||
|
||||
// Get the customer ID (use "my_customer" for the domain)
|
||||
customer := "my_customer"
|
||||
|
||||
for {
|
||||
call := service.Groups.List().Customer(customer).MaxResults(500)
|
||||
if pageToken != "" {
|
||||
call = call.PageToken(pageToken)
|
||||
}
|
||||
|
||||
resp, err := call.Do()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list groups: %v", err)
|
||||
}
|
||||
|
||||
allGroups = append(allGroups, resp.Groups...)
|
||||
|
||||
// Handle pagination
|
||||
if resp.NextPageToken == "" {
|
||||
break
|
||||
}
|
||||
pageToken = resp.NextPageToken
|
||||
}
|
||||
|
||||
return allGroups, nil
|
||||
}
|
||||
|
||||
// googleWorkspaceGroupToOriginalGroup converts Google Workspace group to Casdoor OriginalGroup
|
||||
func (p *GoogleWorkspaceSyncerProvider) googleWorkspaceGroupToOriginalGroup(gwGroup *admin.Group) *OriginalGroup {
|
||||
group := &OriginalGroup{
|
||||
Id: gwGroup.Id,
|
||||
Name: gwGroup.Email,
|
||||
DisplayName: gwGroup.Name,
|
||||
Description: gwGroup.Description,
|
||||
Email: gwGroup.Email,
|
||||
}
|
||||
|
||||
return group
|
||||
}
|
||||
|
||||
204
object/syncer_googleworkspace_test.go
Normal file
204
object/syncer_googleworkspace_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
// 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 (
|
||||
"testing"
|
||||
|
||||
admin "google.golang.org/api/admin/directory/v1"
|
||||
)
|
||||
|
||||
func TestGoogleWorkspaceUserToOriginalUser(t *testing.T) {
|
||||
provider := &GoogleWorkspaceSyncerProvider{
|
||||
Syncer: &Syncer{},
|
||||
}
|
||||
|
||||
// Test case 1: Full Google Workspace user with all fields
|
||||
gwUser := &admin.User{
|
||||
Id: "user-123",
|
||||
PrimaryEmail: "john.doe@example.com",
|
||||
Name: &admin.UserName{
|
||||
FullName: "John Doe",
|
||||
GivenName: "John",
|
||||
FamilyName: "Doe",
|
||||
},
|
||||
ThumbnailPhotoUrl: "https://example.com/avatar.jpg",
|
||||
Suspended: false,
|
||||
IsAdmin: true,
|
||||
CreationTime: "2024-01-01T00:00:00Z",
|
||||
}
|
||||
|
||||
originalUser := provider.googleWorkspaceUserToOriginalUser(gwUser)
|
||||
|
||||
// Verify basic fields
|
||||
if originalUser.Id != "user-123" {
|
||||
t.Errorf("Expected Id to be 'user-123', got '%s'", originalUser.Id)
|
||||
}
|
||||
if originalUser.Name != "john.doe@example.com" {
|
||||
t.Errorf("Expected Name to be 'john.doe@example.com', got '%s'", originalUser.Name)
|
||||
}
|
||||
if originalUser.Email != "john.doe@example.com" {
|
||||
t.Errorf("Expected Email to be 'john.doe@example.com', got '%s'", originalUser.Email)
|
||||
}
|
||||
if originalUser.DisplayName != "John Doe" {
|
||||
t.Errorf("Expected DisplayName to be 'John Doe', got '%s'", originalUser.DisplayName)
|
||||
}
|
||||
if originalUser.FirstName != "John" {
|
||||
t.Errorf("Expected FirstName to be 'John', got '%s'", originalUser.FirstName)
|
||||
}
|
||||
if originalUser.LastName != "Doe" {
|
||||
t.Errorf("Expected LastName to be 'Doe', got '%s'", originalUser.LastName)
|
||||
}
|
||||
if originalUser.Avatar != "https://example.com/avatar.jpg" {
|
||||
t.Errorf("Expected Avatar to be 'https://example.com/avatar.jpg', got '%s'", originalUser.Avatar)
|
||||
}
|
||||
if originalUser.IsForbidden != false {
|
||||
t.Errorf("Expected IsForbidden to be false for non-suspended user, got %v", originalUser.IsForbidden)
|
||||
}
|
||||
if originalUser.IsAdmin != true {
|
||||
t.Errorf("Expected IsAdmin to be true, got %v", originalUser.IsAdmin)
|
||||
}
|
||||
|
||||
// Test case 2: Suspended Google Workspace user
|
||||
suspendedUser := &admin.User{
|
||||
Id: "user-456",
|
||||
PrimaryEmail: "jane.doe@example.com",
|
||||
Name: &admin.UserName{
|
||||
FullName: "Jane Doe",
|
||||
},
|
||||
Suspended: true,
|
||||
}
|
||||
|
||||
suspendedOriginalUser := provider.googleWorkspaceUserToOriginalUser(suspendedUser)
|
||||
if suspendedOriginalUser.IsForbidden != true {
|
||||
t.Errorf("Expected IsForbidden to be true for suspended user, got %v", suspendedOriginalUser.IsForbidden)
|
||||
}
|
||||
|
||||
// Test case 3: User with no Name object (should not panic)
|
||||
minimalUser := &admin.User{
|
||||
Id: "user-789",
|
||||
PrimaryEmail: "bob@example.com",
|
||||
}
|
||||
|
||||
minimalOriginalUser := provider.googleWorkspaceUserToOriginalUser(minimalUser)
|
||||
if minimalOriginalUser.DisplayName != "" {
|
||||
t.Errorf("Expected DisplayName to be empty for minimal user, got '%s'", minimalOriginalUser.DisplayName)
|
||||
}
|
||||
|
||||
// Test case 4: Display name construction from first/last name when FullName is empty
|
||||
noFullNameUser := &admin.User{
|
||||
Id: "user-101",
|
||||
PrimaryEmail: "alice@example.com",
|
||||
Name: &admin.UserName{
|
||||
GivenName: "Alice",
|
||||
FamilyName: "Jones",
|
||||
},
|
||||
}
|
||||
|
||||
noFullNameOriginalUser := provider.googleWorkspaceUserToOriginalUser(noFullNameUser)
|
||||
if noFullNameOriginalUser.DisplayName != "Alice Jones" {
|
||||
t.Errorf("Expected DisplayName to be constructed as 'Alice Jones', got '%s'", noFullNameOriginalUser.DisplayName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoogleWorkspaceGroupToOriginalGroup(t *testing.T) {
|
||||
provider := &GoogleWorkspaceSyncerProvider{
|
||||
Syncer: &Syncer{},
|
||||
}
|
||||
|
||||
// Test case 1: Full Google Workspace group with all fields
|
||||
gwGroup := &admin.Group{
|
||||
Id: "group-123",
|
||||
Email: "team@example.com",
|
||||
Name: "Engineering Team",
|
||||
Description: "All engineering staff",
|
||||
}
|
||||
|
||||
originalGroup := provider.googleWorkspaceGroupToOriginalGroup(gwGroup)
|
||||
|
||||
// Verify all fields
|
||||
if originalGroup.Id != "group-123" {
|
||||
t.Errorf("Expected Id to be 'group-123', got '%s'", originalGroup.Id)
|
||||
}
|
||||
if originalGroup.Name != "team@example.com" {
|
||||
t.Errorf("Expected Name to be 'team@example.com', got '%s'", originalGroup.Name)
|
||||
}
|
||||
if originalGroup.DisplayName != "Engineering Team" {
|
||||
t.Errorf("Expected DisplayName to be 'Engineering Team', got '%s'", originalGroup.DisplayName)
|
||||
}
|
||||
if originalGroup.Description != "All engineering staff" {
|
||||
t.Errorf("Expected Description to be 'All engineering staff', got '%s'", originalGroup.Description)
|
||||
}
|
||||
if originalGroup.Email != "team@example.com" {
|
||||
t.Errorf("Expected Email to be 'team@example.com', got '%s'", originalGroup.Email)
|
||||
}
|
||||
|
||||
// Test case 2: Minimal group
|
||||
minimalGroup := &admin.Group{
|
||||
Id: "group-456",
|
||||
Email: "minimal@example.com",
|
||||
}
|
||||
|
||||
minimalOriginalGroup := provider.googleWorkspaceGroupToOriginalGroup(minimalGroup)
|
||||
if minimalOriginalGroup.DisplayName != "" {
|
||||
t.Errorf("Expected DisplayName to be empty for minimal group, got '%s'", minimalOriginalGroup.DisplayName)
|
||||
}
|
||||
if minimalOriginalGroup.Description != "" {
|
||||
t.Errorf("Expected Description to be empty for minimal group, got '%s'", minimalOriginalGroup.Description)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSyncerProviderGoogleWorkspace(t *testing.T) {
|
||||
syncer := &Syncer{
|
||||
Type: "Google Workspace",
|
||||
Host: "admin@example.com",
|
||||
}
|
||||
|
||||
provider := GetSyncerProvider(syncer)
|
||||
|
||||
if _, ok := provider.(*GoogleWorkspaceSyncerProvider); !ok {
|
||||
t.Errorf("Expected GoogleWorkspaceSyncerProvider for type 'Google Workspace', got %T", provider)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoogleWorkspaceSyncerProviderEmptyMethods(t *testing.T) {
|
||||
provider := &GoogleWorkspaceSyncerProvider{
|
||||
Syncer: &Syncer{},
|
||||
}
|
||||
|
||||
// Test AddUser returns error
|
||||
_, err := provider.AddUser(&OriginalUser{})
|
||||
if err == nil {
|
||||
t.Error("Expected AddUser to return error for read-only syncer")
|
||||
}
|
||||
|
||||
// Test UpdateUser returns error
|
||||
_, err = provider.UpdateUser(&OriginalUser{})
|
||||
if err == nil {
|
||||
t.Error("Expected UpdateUser to return error for read-only syncer")
|
||||
}
|
||||
|
||||
// Test Close returns no error
|
||||
err = provider.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Expected Close to return nil, got error: %v", err)
|
||||
}
|
||||
|
||||
// Test InitAdapter returns no error
|
||||
err = provider.InitAdapter()
|
||||
if err != nil {
|
||||
t.Errorf("Expected InitAdapter to return nil, got error: %v", err)
|
||||
}
|
||||
}
|
||||
121
object/syncer_group.go
Normal file
121
object/syncer_group.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
func (syncer *Syncer) getOriginalGroups() ([]*OriginalGroup, error) {
|
||||
provider := GetSyncerProvider(syncer)
|
||||
return provider.GetOriginalGroups()
|
||||
}
|
||||
|
||||
func (syncer *Syncer) createGroupFromOriginalGroup(originalGroup *OriginalGroup) *Group {
|
||||
group := &Group{
|
||||
Owner: syncer.Organization,
|
||||
Name: originalGroup.Name,
|
||||
CreatedTime: util.GetCurrentTime(),
|
||||
UpdatedTime: util.GetCurrentTime(),
|
||||
DisplayName: originalGroup.DisplayName,
|
||||
Type: originalGroup.Type,
|
||||
Manager: originalGroup.Manager,
|
||||
IsEnabled: true,
|
||||
IsTopGroup: true,
|
||||
}
|
||||
|
||||
if originalGroup.Email != "" {
|
||||
group.ContactEmail = originalGroup.Email
|
||||
}
|
||||
|
||||
return group
|
||||
}
|
||||
|
||||
func (syncer *Syncer) syncGroups() error {
|
||||
fmt.Printf("Running syncGroups()..\n")
|
||||
|
||||
// Get existing groups from Casdoor
|
||||
groups, err := GetGroups(syncer.Organization)
|
||||
if err != nil {
|
||||
line := fmt.Sprintf("[%s] %s\n", util.GetCurrentTime(), err.Error())
|
||||
_, err2 := updateSyncerErrorText(syncer, line)
|
||||
if err2 != nil {
|
||||
panic(err2)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Get groups from the external system
|
||||
oGroups, err := syncer.getOriginalGroups()
|
||||
if err != nil {
|
||||
line := fmt.Sprintf("[%s] %s\n", util.GetCurrentTime(), err.Error())
|
||||
_, err2 := updateSyncerErrorText(syncer, line)
|
||||
if err2 != nil {
|
||||
panic(err2)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Groups: %d, oGroups: %d\n", len(groups), len(oGroups))
|
||||
|
||||
// Create a map of existing groups by name
|
||||
myGroups := map[string]*Group{}
|
||||
for _, group := range groups {
|
||||
myGroups[group.Name] = group
|
||||
}
|
||||
|
||||
// Sync groups from external system to Casdoor
|
||||
newGroups := []*Group{}
|
||||
for _, oGroup := range oGroups {
|
||||
if _, ok := myGroups[oGroup.Name]; !ok {
|
||||
newGroup := syncer.createGroupFromOriginalGroup(oGroup)
|
||||
fmt.Printf("New group: %v\n", newGroup)
|
||||
newGroups = append(newGroups, newGroup)
|
||||
} else {
|
||||
// Group already exists, could update it here if needed
|
||||
existingGroup := myGroups[oGroup.Name]
|
||||
|
||||
// Update group display name and other fields if they've changed
|
||||
if existingGroup.DisplayName != oGroup.DisplayName {
|
||||
existingGroup.DisplayName = oGroup.DisplayName
|
||||
existingGroup.UpdatedTime = util.GetCurrentTime()
|
||||
_, err = UpdateGroup(existingGroup.GetId(), existingGroup)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to update group %s: %v\n", existingGroup.Name, err)
|
||||
} else {
|
||||
fmt.Printf("Updated group: %s\n", existingGroup.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(newGroups) != 0 {
|
||||
_, err = AddGroupsInBatch(newGroups)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (syncer *Syncer) syncGroupsNoError() {
|
||||
err := syncer.syncGroups()
|
||||
if err != nil {
|
||||
fmt.Printf("syncGroupsNoError() error: %s\n", err.Error())
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,17 @@
|
||||
|
||||
package object
|
||||
|
||||
// OriginalGroup represents a group from an external system
|
||||
type OriginalGroup struct {
|
||||
Id string
|
||||
Name string
|
||||
DisplayName string
|
||||
Description string
|
||||
Type string
|
||||
Manager string
|
||||
Email string
|
||||
}
|
||||
|
||||
// SyncerProvider defines the interface that all syncer implementations must satisfy.
|
||||
// Different syncer types (Database, Keycloak, WeCom, Azure AD) implement this interface.
|
||||
type SyncerProvider interface {
|
||||
@@ -23,6 +34,12 @@ type SyncerProvider interface {
|
||||
// GetOriginalUsers retrieves all users from the external system
|
||||
GetOriginalUsers() ([]*OriginalUser, error)
|
||||
|
||||
// GetOriginalGroups retrieves all groups from the external system
|
||||
GetOriginalGroups() ([]*OriginalGroup, error)
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to
|
||||
GetOriginalUserGroups(userId string) ([]string, error)
|
||||
|
||||
// AddUser adds a new user to the external system
|
||||
AddUser(user *OriginalUser) (bool, error)
|
||||
|
||||
|
||||
@@ -29,3 +29,15 @@ func (p *KeycloakSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
|
||||
|
||||
// Note: Keycloak-specific user mapping is handled in syncer_util.go
|
||||
// via getOriginalUsersFromMap which checks syncer.Type == "Keycloak"
|
||||
|
||||
// GetOriginalGroups retrieves all groups from Keycloak (not implemented yet)
|
||||
func (p *KeycloakSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
||||
// TODO: Implement Keycloak group sync
|
||||
return []*OriginalGroup{}, nil
|
||||
}
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to (not implemented yet)
|
||||
func (p *KeycloakSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
||||
// TODO: Implement Keycloak user group membership sync
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
@@ -414,3 +414,15 @@ func (p *LarkSyncerProvider) larkUserToOriginalUser(larkUser *LarkUser) *Origina
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -296,3 +296,15 @@ func (p *OktaSyncerProvider) getOktaOriginalUsers() ([]*OriginalUser, error) {
|
||||
|
||||
return originalUsers, nil
|
||||
}
|
||||
|
||||
// GetOriginalGroups retrieves all groups from Okta (not implemented yet)
|
||||
func (p *OktaSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
||||
// TODO: Implement Okta group sync
|
||||
return []*OriginalGroup{}, nil
|
||||
}
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to (not implemented yet)
|
||||
func (p *OktaSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
||||
// TODO: Implement Okta user group membership sync
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
@@ -335,3 +335,15 @@ func (p *SCIMSyncerProvider) scimUserToOriginalUser(scimUser *SCIMUser) *Origina
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// GetOriginalGroups retrieves all groups from SCIM (not implemented yet)
|
||||
func (p *SCIMSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
||||
// TODO: Implement SCIM group sync
|
||||
return []*OriginalGroup{}, nil
|
||||
}
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to (not implemented yet)
|
||||
func (p *SCIMSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
||||
// TODO: Implement SCIM user group membership sync
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
@@ -303,3 +303,15 @@ func (p *WecomSyncerProvider) wecomUserToOriginalUser(wecomUser *WecomUser) *Ori
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user