forked from casdoor/casdoor
Compare commits
4 Commits
custom
...
copilot/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06dc07602f | ||
|
|
96ffbba309 | ||
|
|
0e27b5fb14 | ||
|
|
4cde0fc602 |
2
go.mod
2
go.mod
@@ -17,6 +17,7 @@ require (
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107
|
||||
github.com/aliyun/aliyun-oss-go-sdk v2.2.2+incompatible
|
||||
github.com/aliyun/credentials-go v1.3.10
|
||||
github.com/aws/aws-sdk-go v1.45.5
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0
|
||||
github.com/beego/beego/v2 v2.3.8
|
||||
github.com/beevik/etree v1.1.0
|
||||
@@ -114,7 +115,6 @@ require (
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
|
||||
github.com/apistd/uni-go-sdk v0.0.2 // indirect
|
||||
github.com/atc0005/go-teams-notify/v2 v2.13.0 // indirect
|
||||
github.com/aws/aws-sdk-go v1.45.5 // indirect
|
||||
github.com/aws/smithy-go v1.24.0 // indirect
|
||||
github.com/baidubce/bce-sdk-go v0.9.156 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
|
||||
327
object/syncer_awsiam.go
Normal file
327
object/syncer_awsiam.go
Normal file
@@ -0,0 +1,327 @@
|
||||
// Copyright 2026 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"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/iam"
|
||||
"github.com/casdoor/casdoor/util"
|
||||
)
|
||||
|
||||
// AwsIamSyncerProvider implements SyncerProvider for AWS IAM API-based syncers
|
||||
type AwsIamSyncerProvider struct {
|
||||
Syncer *Syncer
|
||||
iamClient *iam.IAM
|
||||
}
|
||||
|
||||
// InitAdapter initializes the AWS IAM syncer
|
||||
func (p *AwsIamSyncerProvider) InitAdapter() error {
|
||||
// syncer.Host should be the AWS region (e.g., "us-east-1")
|
||||
// syncer.User should be the AWS Access Key ID
|
||||
// syncer.Password should be the AWS Secret Access Key
|
||||
|
||||
region := p.Syncer.Host
|
||||
if region == "" {
|
||||
return fmt.Errorf("AWS region (host field) is required for AWS IAM syncer")
|
||||
}
|
||||
|
||||
accessKeyId := p.Syncer.User
|
||||
if accessKeyId == "" {
|
||||
return fmt.Errorf("AWS Access Key ID (user field) is required for AWS IAM syncer")
|
||||
}
|
||||
|
||||
secretAccessKey := p.Syncer.Password
|
||||
if secretAccessKey == "" {
|
||||
return fmt.Errorf("AWS Secret Access Key (password field) is required for AWS IAM syncer")
|
||||
}
|
||||
|
||||
// Create AWS session
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(region),
|
||||
Credentials: credentials.NewStaticCredentials(accessKeyId, secretAccessKey, ""),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create AWS session: %w", err)
|
||||
}
|
||||
|
||||
// Create IAM client
|
||||
p.iamClient = iam.New(sess)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOriginalUsers retrieves all users from AWS IAM API
|
||||
func (p *AwsIamSyncerProvider) GetOriginalUsers() ([]*OriginalUser, error) {
|
||||
if p.iamClient == nil {
|
||||
if err := p.InitAdapter(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return p.getAwsIamUsers()
|
||||
}
|
||||
|
||||
// AddUser adds a new user to AWS IAM (not supported for read-only API)
|
||||
func (p *AwsIamSyncerProvider) AddUser(user *OriginalUser) (bool, error) {
|
||||
// AWS IAM syncer is typically read-only
|
||||
return false, fmt.Errorf("adding users to AWS IAM is not supported")
|
||||
}
|
||||
|
||||
// UpdateUser updates an existing user in AWS IAM (not supported for read-only API)
|
||||
func (p *AwsIamSyncerProvider) UpdateUser(user *OriginalUser) (bool, error) {
|
||||
// AWS IAM syncer is typically read-only
|
||||
return false, fmt.Errorf("updating users in AWS IAM is not supported")
|
||||
}
|
||||
|
||||
// TestConnection tests the AWS IAM API connection
|
||||
func (p *AwsIamSyncerProvider) TestConnection() error {
|
||||
if p.iamClient == nil {
|
||||
if err := p.InitAdapter(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Try to list users with a limit of 1 to test the connection
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
input := &iam.ListUsersInput{
|
||||
MaxItems: aws.Int64(1),
|
||||
}
|
||||
|
||||
_, err := p.iamClient.ListUsersWithContext(ctx, input)
|
||||
return err
|
||||
}
|
||||
|
||||
// Close closes any open connections
|
||||
func (p *AwsIamSyncerProvider) Close() error {
|
||||
// AWS IAM client doesn't require explicit cleanup
|
||||
p.iamClient = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAwsIamUsers gets all users from AWS IAM API
|
||||
func (p *AwsIamSyncerProvider) getAwsIamUsers() ([]*OriginalUser, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
allUsers := []*iam.User{}
|
||||
var marker *string
|
||||
|
||||
// Paginate through all users
|
||||
for {
|
||||
input := &iam.ListUsersInput{
|
||||
Marker: marker,
|
||||
}
|
||||
|
||||
result, err := p.iamClient.ListUsersWithContext(ctx, input)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list IAM users: %w", err)
|
||||
}
|
||||
|
||||
allUsers = append(allUsers, result.Users...)
|
||||
|
||||
if result.IsTruncated == nil || !*result.IsTruncated {
|
||||
break
|
||||
}
|
||||
|
||||
marker = result.Marker
|
||||
}
|
||||
|
||||
// Convert AWS IAM users to Casdoor OriginalUser
|
||||
originalUsers := []*OriginalUser{}
|
||||
for _, iamUser := range allUsers {
|
||||
originalUser, err := p.awsIamUserToOriginalUser(iamUser)
|
||||
if err != nil {
|
||||
// Log error but continue processing other users
|
||||
userName := "unknown"
|
||||
if iamUser.UserName != nil {
|
||||
userName = *iamUser.UserName
|
||||
}
|
||||
fmt.Printf("Warning: Failed to convert IAM user %s: %v\n", userName, err)
|
||||
continue
|
||||
}
|
||||
originalUsers = append(originalUsers, originalUser)
|
||||
}
|
||||
|
||||
return originalUsers, nil
|
||||
}
|
||||
|
||||
// awsIamUserToOriginalUser converts AWS IAM user to Casdoor OriginalUser
|
||||
func (p *AwsIamSyncerProvider) awsIamUserToOriginalUser(iamUser *iam.User) (*OriginalUser, error) {
|
||||
if iamUser == nil {
|
||||
return nil, fmt.Errorf("IAM user is nil")
|
||||
}
|
||||
|
||||
user := &OriginalUser{
|
||||
Address: []string{},
|
||||
Properties: map[string]string{},
|
||||
Groups: []string{},
|
||||
}
|
||||
|
||||
// Set ID from UserId (unique identifier)
|
||||
if iamUser.UserId != nil {
|
||||
user.Id = *iamUser.UserId
|
||||
}
|
||||
|
||||
// Set Name from UserName
|
||||
if iamUser.UserName != nil {
|
||||
user.Name = *iamUser.UserName
|
||||
}
|
||||
|
||||
// Set DisplayName (use UserName if not available separately)
|
||||
if iamUser.UserName != nil {
|
||||
user.DisplayName = *iamUser.UserName
|
||||
}
|
||||
|
||||
// Set CreatedTime from CreateDate
|
||||
if iamUser.CreateDate != nil {
|
||||
user.CreatedTime = iamUser.CreateDate.Format(time.RFC3339)
|
||||
} else {
|
||||
user.CreatedTime = util.GetCurrentTime()
|
||||
}
|
||||
|
||||
// Get user tags which might contain additional information
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
tagsInput := &iam.ListUserTagsInput{
|
||||
UserName: iamUser.UserName,
|
||||
}
|
||||
|
||||
tagsResult, err := p.iamClient.ListUserTagsWithContext(ctx, tagsInput)
|
||||
if err == nil && tagsResult != nil {
|
||||
// Process tags to extract additional user information
|
||||
for _, tag := range tagsResult.Tags {
|
||||
if tag.Key != nil && tag.Value != nil {
|
||||
key := *tag.Key
|
||||
value := *tag.Value
|
||||
|
||||
switch key {
|
||||
case "Email", "email":
|
||||
user.Email = value
|
||||
case "Phone", "phone":
|
||||
user.Phone = value
|
||||
case "DisplayName", "displayName":
|
||||
user.DisplayName = value
|
||||
case "FirstName", "firstName":
|
||||
user.FirstName = value
|
||||
case "LastName", "lastName":
|
||||
user.LastName = value
|
||||
case "Title", "title":
|
||||
user.Title = value
|
||||
case "Department", "department":
|
||||
user.Affiliation = value
|
||||
default:
|
||||
// Store other tags in Properties
|
||||
user.Properties[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AWS IAM users are active by default unless specified in tags
|
||||
// Check if there's a "Status" or "Active" tag
|
||||
if status, ok := user.Properties["Status"]; ok {
|
||||
if status == "Inactive" || status == "Disabled" {
|
||||
user.IsForbidden = true
|
||||
}
|
||||
}
|
||||
if active, ok := user.Properties["Active"]; ok {
|
||||
if active == "false" || active == "False" || active == "0" {
|
||||
user.IsForbidden = true
|
||||
}
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetOriginalGroups retrieves all groups from AWS IAM
|
||||
func (p *AwsIamSyncerProvider) GetOriginalGroups() ([]*OriginalGroup, error) {
|
||||
if p.iamClient == nil {
|
||||
if err := p.InitAdapter(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
allGroups := []*iam.Group{}
|
||||
var marker *string
|
||||
|
||||
// Paginate through all groups
|
||||
for {
|
||||
input := &iam.ListGroupsInput{
|
||||
Marker: marker,
|
||||
}
|
||||
|
||||
result, err := p.iamClient.ListGroupsWithContext(ctx, input)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list IAM groups: %w", err)
|
||||
}
|
||||
|
||||
allGroups = append(allGroups, result.Groups...)
|
||||
|
||||
if result.IsTruncated == nil || !*result.IsTruncated {
|
||||
break
|
||||
}
|
||||
|
||||
marker = result.Marker
|
||||
}
|
||||
|
||||
// Convert AWS IAM groups to Casdoor OriginalGroup
|
||||
originalGroups := []*OriginalGroup{}
|
||||
for _, iamGroup := range allGroups {
|
||||
if iamGroup.GroupId != nil && iamGroup.GroupName != nil {
|
||||
group := &OriginalGroup{
|
||||
Id: *iamGroup.GroupId,
|
||||
Name: *iamGroup.GroupName,
|
||||
}
|
||||
|
||||
if iamGroup.GroupName != nil {
|
||||
group.DisplayName = *iamGroup.GroupName
|
||||
}
|
||||
|
||||
originalGroups = append(originalGroups, group)
|
||||
}
|
||||
}
|
||||
|
||||
return originalGroups, nil
|
||||
}
|
||||
|
||||
// GetOriginalUserGroups retrieves the group IDs that a user belongs to
|
||||
func (p *AwsIamSyncerProvider) GetOriginalUserGroups(userId string) ([]string, error) {
|
||||
if p.iamClient == nil {
|
||||
if err := p.InitAdapter(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Note: AWS IAM API requires UserName to query groups, but this interface provides UserId.
|
||||
// This is a known limitation. To properly implement this, we would need to:
|
||||
// 1. Maintain a mapping cache from UserId to UserName, or
|
||||
// 2. Modify the interface to accept both UserId and UserName
|
||||
// For now, returning empty groups to maintain interface compatibility.
|
||||
// TODO: Implement user group synchronization by maintaining a UserId->UserName mapping
|
||||
|
||||
return []string{}, nil
|
||||
}
|
||||
@@ -72,6 +72,8 @@ func GetSyncerProvider(syncer *Syncer) SyncerProvider {
|
||||
return &OktaSyncerProvider{Syncer: syncer}
|
||||
case "SCIM":
|
||||
return &SCIMSyncerProvider{Syncer: syncer}
|
||||
case "AWS IAM":
|
||||
return &AwsIamSyncerProvider{Syncer: syncer}
|
||||
case "Keycloak":
|
||||
return &KeycloakSyncerProvider{
|
||||
DatabaseSyncerProvider: DatabaseSyncerProvider{Syncer: syncer},
|
||||
|
||||
@@ -710,6 +710,79 @@ class SyncerEditPage extends React.Component {
|
||||
"values": [],
|
||||
},
|
||||
];
|
||||
case "AWS IAM":
|
||||
return [
|
||||
{
|
||||
"name": "UserId",
|
||||
"type": "string",
|
||||
"casdoorName": "Id",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "UserName",
|
||||
"type": "string",
|
||||
"casdoorName": "Name",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "UserName",
|
||||
"type": "string",
|
||||
"casdoorName": "DisplayName",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "Tags.Email",
|
||||
"type": "string",
|
||||
"casdoorName": "Email",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "Tags.Phone",
|
||||
"type": "string",
|
||||
"casdoorName": "Phone",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "Tags.FirstName",
|
||||
"type": "string",
|
||||
"casdoorName": "FirstName",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "Tags.LastName",
|
||||
"type": "string",
|
||||
"casdoorName": "LastName",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "Tags.Title",
|
||||
"type": "string",
|
||||
"casdoorName": "Title",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "Tags.Department",
|
||||
"type": "string",
|
||||
"casdoorName": "Affiliation",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
{
|
||||
"name": "CreateDate",
|
||||
"type": "string",
|
||||
"casdoorName": "CreatedTime",
|
||||
"isHashed": true,
|
||||
"values": [],
|
||||
},
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
@@ -766,14 +839,14 @@ class SyncerEditPage extends React.Component {
|
||||
});
|
||||
})}>
|
||||
{
|
||||
["Database", "Keycloak", "WeCom", "Azure AD", "Active Directory", "Google Workspace", "DingTalk", "Lark", "Okta", "SCIM"]
|
||||
["Database", "Keycloak", "WeCom", "Azure AD", "Active Directory", "Google Workspace", "DingTalk", "Lark", "Okta", "SCIM", "AWS IAM"]
|
||||
.map((item, index) => <Option key={index} value={item}>{item}</Option>)
|
||||
}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Active Directory" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" ? null : (
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Active Directory" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" || this.state.syncer.type === "AWS IAM" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("syncer:Database type"), i18next.t("syncer:Database type - Tooltip"))} :
|
||||
@@ -828,7 +901,7 @@ class SyncerEditPage extends React.Component {
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(this.state.syncer.type === "Azure AD" ? i18next.t("provider:Tenant ID") : this.state.syncer.type === "Google Workspace" ? i18next.t("syncer:Admin Email") : this.state.syncer.type === "Active Directory" ? i18next.t("ldap:Server") : this.state.syncer.type === "SCIM" ? i18next.t("syncer:SCIM Server URL") : i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
|
||||
{Setting.getLabel(this.state.syncer.type === "Azure AD" ? i18next.t("provider:Tenant ID") : this.state.syncer.type === "Google Workspace" ? i18next.t("syncer:Admin Email") : this.state.syncer.type === "Active Directory" ? i18next.t("ldap:Server") : this.state.syncer.type === "SCIM" ? i18next.t("syncer:SCIM Server URL") : this.state.syncer.type === "AWS IAM" ? i18next.t("syncer:AWS Region") : i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
|
||||
</Col>
|
||||
<Col span={22} >
|
||||
<Input prefix={<LinkOutlined />} value={this.state.syncer.host} onChange={e => {
|
||||
@@ -839,7 +912,7 @@ class SyncerEditPage extends React.Component {
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" ? null : (
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" || this.state.syncer.type === "AWS IAM" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(this.state.syncer.type === "Active Directory" ? i18next.t("provider:LDAP port") : i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
|
||||
@@ -863,7 +936,8 @@ class SyncerEditPage extends React.Component {
|
||||
this.state.syncer.type === "Azure AD" ? i18next.t("provider:Client ID") :
|
||||
this.state.syncer.type === "Active Directory" ? i18next.t("syncer:Bind DN") :
|
||||
this.state.syncer.type === "SCIM" ? i18next.t("syncer:Username (optional)") :
|
||||
i18next.t("general:User"),
|
||||
this.state.syncer.type === "AWS IAM" ? i18next.t("syncer:AWS Access Key ID") :
|
||||
i18next.t("general:User"),
|
||||
i18next.t("general:User - Tooltip")
|
||||
)} :
|
||||
</Col>
|
||||
@@ -884,7 +958,8 @@ class SyncerEditPage extends React.Component {
|
||||
this.state.syncer.type === "Azure AD" ? i18next.t("provider:Client secret") :
|
||||
this.state.syncer.type === "Google Workspace" ? i18next.t("syncer:Service account key") :
|
||||
this.state.syncer.type === "SCIM" ? i18next.t("syncer:API Token / Password") :
|
||||
i18next.t("general:Password"),
|
||||
this.state.syncer.type === "AWS IAM" ? i18next.t("syncer:AWS Secret Access Key") :
|
||||
i18next.t("general:Password"),
|
||||
i18next.t("general:Password - Tooltip")
|
||||
)} :
|
||||
</Col>
|
||||
@@ -903,7 +978,7 @@ class SyncerEditPage extends React.Component {
|
||||
</Col>
|
||||
</Row>
|
||||
{
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" ? null : (
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" || this.state.syncer.type === "AWS IAM" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(this.state.syncer.type === "Active Directory" ? i18next.t("ldap:Base DN") : i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} :
|
||||
@@ -999,7 +1074,7 @@ class SyncerEditPage extends React.Component {
|
||||
) : null
|
||||
}
|
||||
{
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" ? null : (
|
||||
this.state.syncer.type === "WeCom" || this.state.syncer.type === "Azure AD" || this.state.syncer.type === "Google Workspace" || this.state.syncer.type === "DingTalk" || this.state.syncer.type === "Lark" || this.state.syncer.type === "Okta" || this.state.syncer.type === "SCIM" || this.state.syncer.type === "AWS IAM" ? null : (
|
||||
<Row style={{marginTop: "20px"}} >
|
||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||
{Setting.getLabel(i18next.t("syncer:Table"), i18next.t("syncer:Table - Tooltip"))} :
|
||||
|
||||
Reference in New Issue
Block a user