forked from baron/baron-sso
동기화 기초구조 마련
This commit is contained in:
@@ -28,6 +28,7 @@ const (
|
||||
|
||||
type WorksmobileDirectoryClient interface {
|
||||
CreateOrgUnit(ctx context.Context, payload WorksmobileOrgUnitPayload) error
|
||||
UpsertOrgUnit(ctx context.Context, payload WorksmobileOrgUnitPayload, matchLocalPart string) error
|
||||
CreateUser(ctx context.Context, payload WorksmobileUserPayload) error
|
||||
UpsertUser(ctx context.Context, payload WorksmobileUserPayload) error
|
||||
DeleteUser(ctx context.Context, userID string) error
|
||||
@@ -36,14 +37,15 @@ type WorksmobileDirectoryClient interface {
|
||||
}
|
||||
|
||||
type WorksmobileHTTPClient struct {
|
||||
BaseURL string
|
||||
DirectoryToken string
|
||||
SCIMToken string
|
||||
HTTPClient *http.Client
|
||||
OAuthConfig WorksmobileOAuthConfig
|
||||
DomainIDs []int64
|
||||
tokenCache worksmobileAccessTokenCache
|
||||
now func() time.Time
|
||||
BaseURL string
|
||||
DirectoryToken string
|
||||
SCIMToken string
|
||||
HTTPClient *http.Client
|
||||
OAuthConfig WorksmobileOAuthConfig
|
||||
DomainIDs []int64
|
||||
OrgUnitWriteDelay time.Duration
|
||||
tokenCache worksmobileAccessTokenCache
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
type WorksmobileOAuthConfig struct {
|
||||
@@ -186,6 +188,103 @@ func (c *WorksmobileHTTPClient) CreateOrgUnit(ctx context.Context, payload Works
|
||||
return c.sendDirectoryJSON(ctx, http.MethodPost, "/v1.0/orgunits", payload)
|
||||
}
|
||||
|
||||
func (c *WorksmobileHTTPClient) UpsertOrgUnit(ctx context.Context, payload WorksmobileOrgUnitPayload, matchLocalPart string) error {
|
||||
err := c.CreateOrgUnit(ctx, payload)
|
||||
if apiErr, ok := err.(WorksmobileHTTPError); ok && apiErr.StatusCode == http.StatusConflict {
|
||||
return c.BackfillOrgUnitExternalKeyByLocalPart(ctx, payload, matchLocalPart)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *WorksmobileHTTPClient) BackfillOrgUnitExternalKeyByLocalPart(ctx context.Context, payload WorksmobileOrgUnitPayload, matchLocalPart string) error {
|
||||
localPart := worksmobileMailLocalPart(matchLocalPart)
|
||||
groups, err := c.ListGroups(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, group := range groups {
|
||||
if payload.DomainID > 0 && group.DomainID > 0 && payload.DomainID != group.DomainID {
|
||||
continue
|
||||
}
|
||||
if group.ExternalID == payload.OrgUnitExternalKey {
|
||||
if strings.TrimSpace(group.ID) == "" {
|
||||
return nil
|
||||
}
|
||||
if delay := c.orgUnitWriteDelay(); delay > 0 {
|
||||
time.Sleep(delay)
|
||||
}
|
||||
return c.PatchOrgUnit(ctx, group.ID, NewWorksmobileOrgUnitPatchPayload(payload))
|
||||
}
|
||||
}
|
||||
if localPart == "" {
|
||||
return fmt.Errorf("worksmobile orgunit local-part match key is required")
|
||||
}
|
||||
matches := make([]WorksmobileRemoteGroup, 0, 1)
|
||||
for _, group := range groups {
|
||||
if payload.DomainID > 0 && group.DomainID > 0 && payload.DomainID != group.DomainID {
|
||||
continue
|
||||
}
|
||||
if group.MailLocalPart == localPart {
|
||||
matches = append(matches, group)
|
||||
}
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return fmt.Errorf("worksmobile orgunit local-part match not found: %s", localPart)
|
||||
}
|
||||
if len(matches) > 1 {
|
||||
return fmt.Errorf("worksmobile orgunit local-part match is ambiguous: %s", localPart)
|
||||
}
|
||||
remote := matches[0]
|
||||
if strings.TrimSpace(remote.ID) == "" {
|
||||
return fmt.Errorf("worksmobile orgunit id is missing for local-part: %s", localPart)
|
||||
}
|
||||
if strings.TrimSpace(remote.ExternalID) != "" {
|
||||
if remote.ExternalID == payload.OrgUnitExternalKey {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("worksmobile orgunit external key already exists for local-part %s: %s", localPart, remote.ExternalID)
|
||||
}
|
||||
if delay := c.orgUnitWriteDelay(); delay > 0 {
|
||||
time.Sleep(delay)
|
||||
}
|
||||
patch := NewWorksmobileOrgUnitPatchPayload(payload)
|
||||
if patch.Email == "" {
|
||||
patch.Email = remote.Email
|
||||
}
|
||||
return c.PatchOrgUnit(ctx, remote.ID, patch)
|
||||
}
|
||||
|
||||
func (c *WorksmobileHTTPClient) PatchOrgUnit(ctx context.Context, orgUnitID string, payload WorksmobileOrgUnitPatchPayload) error {
|
||||
orgUnitID = strings.TrimSpace(orgUnitID)
|
||||
if orgUnitID == "" {
|
||||
return fmt.Errorf("worksmobile orgunit id is required")
|
||||
}
|
||||
return c.sendDirectoryJSON(ctx, http.MethodPatch, "/v1.0/orgunits/"+url.PathEscape(orgUnitID), payload)
|
||||
}
|
||||
|
||||
func (c *WorksmobileHTTPClient) ClearOrgUnitExternalKey(ctx context.Context, orgUnitID string, domainID int64) error {
|
||||
orgUnitID = strings.TrimSpace(orgUnitID)
|
||||
if orgUnitID == "" {
|
||||
return fmt.Errorf("worksmobile orgunit id is required")
|
||||
}
|
||||
payload := map[string]any{
|
||||
"domainId": domainID,
|
||||
"orgUnitExternalKey": "",
|
||||
}
|
||||
return c.sendDirectoryJSON(ctx, http.MethodPatch, "/v1.0/orgunits/"+url.PathEscape(orgUnitID), payload)
|
||||
}
|
||||
|
||||
func (c *WorksmobileHTTPClient) DeleteOrgUnit(ctx context.Context, orgUnitID string) error {
|
||||
orgUnitID = strings.TrimSpace(orgUnitID)
|
||||
if orgUnitID == "" {
|
||||
return fmt.Errorf("worksmobile orgunit id is required")
|
||||
}
|
||||
if delay := c.orgUnitWriteDelay(); delay > 0 {
|
||||
time.Sleep(delay)
|
||||
}
|
||||
return c.sendDirectoryJSON(ctx, http.MethodDelete, "/v1.0/orgunits/"+url.PathEscape(orgUnitID), nil)
|
||||
}
|
||||
|
||||
func (c *WorksmobileHTTPClient) CreateUser(ctx context.Context, payload WorksmobileUserPayload) error {
|
||||
return c.sendDirectoryJSON(ctx, http.MethodPost, "/v1.0/users", payload)
|
||||
}
|
||||
@@ -611,6 +710,15 @@ type WorksmobileUserPatchPayload struct {
|
||||
Organizations []WorksmobileUserOrganization `json:"organizations,omitempty"`
|
||||
}
|
||||
|
||||
type WorksmobileOrgUnitPatchPayload struct {
|
||||
DomainID int64 `json:"domainId"`
|
||||
Email string `json:"email,omitempty"`
|
||||
OrgUnitName string `json:"orgUnitName,omitempty"`
|
||||
OrgUnitExternalKey string `json:"orgUnitExternalKey,omitempty"`
|
||||
ParentOrgUnitID string `json:"parentOrgUnitId,omitempty"`
|
||||
DisplayOrder int `json:"displayOrder,omitempty"`
|
||||
}
|
||||
|
||||
type WorksmobileRemoteUser struct {
|
||||
ID string `json:"id"`
|
||||
ExternalID string `json:"externalId"`
|
||||
@@ -631,13 +739,15 @@ type WorksmobileRemoteUser struct {
|
||||
}
|
||||
|
||||
type WorksmobileRemoteGroup struct {
|
||||
ID string `json:"id"`
|
||||
ExternalID string `json:"externalId"`
|
||||
DisplayName string `json:"displayName"`
|
||||
DomainID int64 `json:"domainId"`
|
||||
DomainName string `json:"domainName"`
|
||||
ParentID string `json:"parentId"`
|
||||
ParentName string `json:"parentName"`
|
||||
ID string `json:"id"`
|
||||
ExternalID string `json:"externalId"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Email string `json:"email,omitempty"`
|
||||
MailLocalPart string `json:"mailLocalPart,omitempty"`
|
||||
DomainID int64 `json:"domainId"`
|
||||
DomainName string `json:"domainName"`
|
||||
ParentID string `json:"parentId"`
|
||||
ParentName string `json:"parentName"`
|
||||
}
|
||||
|
||||
func NewWorksmobileSCIMUserPayload(payload WorksmobileUserPayload) WorksmobileSCIMUserPayload {
|
||||
@@ -681,6 +791,17 @@ func NewWorksmobileUserPatchPayload(payload WorksmobileUserPayload) WorksmobileU
|
||||
}
|
||||
}
|
||||
|
||||
func NewWorksmobileOrgUnitPatchPayload(payload WorksmobileOrgUnitPayload) WorksmobileOrgUnitPatchPayload {
|
||||
return WorksmobileOrgUnitPatchPayload{
|
||||
DomainID: payload.DomainID,
|
||||
Email: strings.TrimSpace(payload.Email),
|
||||
OrgUnitName: strings.TrimSpace(payload.OrgUnitName),
|
||||
OrgUnitExternalKey: strings.TrimSpace(payload.OrgUnitExternalKey),
|
||||
ParentOrgUnitID: strings.TrimSpace(payload.ParentOrgUnitID),
|
||||
DisplayOrder: payload.DisplayOrder,
|
||||
}
|
||||
}
|
||||
|
||||
func worksmobileSCIMPreferredLanguage(locale string) string {
|
||||
locale = strings.TrimSpace(locale)
|
||||
if locale == "" {
|
||||
@@ -716,10 +837,13 @@ func parseWorksmobileRemoteUser(resource map[string]any) WorksmobileRemoteUser {
|
||||
}
|
||||
|
||||
func parseWorksmobileRemoteGroup(resource map[string]any) WorksmobileRemoteGroup {
|
||||
email := firstStringFromMap(resource, "email", "mail", "groupEmail", "mailingList", "orgUnitEmail", "loginId", "userName")
|
||||
group := WorksmobileRemoteGroup{
|
||||
ID: stringFromMap(resource, "id"),
|
||||
ExternalID: stringFromMap(resource, "externalId"),
|
||||
DisplayName: stringFromMap(resource, "displayName"),
|
||||
ID: stringFromMap(resource, "id"),
|
||||
ExternalID: stringFromMap(resource, "externalId"),
|
||||
DisplayName: stringFromMap(resource, "displayName"),
|
||||
Email: email,
|
||||
MailLocalPart: worksmobileMailLocalPart(email),
|
||||
}
|
||||
group.ParentID, group.ParentName = parseWorksmobileParentOrgUnit(resource)
|
||||
return group
|
||||
@@ -751,15 +875,29 @@ func parseWorksmobileDirectoryUser(resource map[string]any) WorksmobileRemoteUse
|
||||
}
|
||||
|
||||
func parseWorksmobileDirectoryGroup(resource map[string]any) WorksmobileRemoteGroup {
|
||||
email := firstStringFromMap(resource, "email", "mail", "groupEmail", "mailingList", "orgUnitEmail", "loginId", "userName")
|
||||
return WorksmobileRemoteGroup{
|
||||
ID: firstStringFromMap(resource, "orgUnitId", "id"),
|
||||
ExternalID: firstStringFromMap(resource, "orgUnitExternalKey", "externalKey", "externalId"),
|
||||
DisplayName: firstStringFromMap(resource, "orgUnitName", "displayName", "name"),
|
||||
ParentID: firstStringFromMap(resource, "parentOrgUnitId", "parentId"),
|
||||
ParentName: firstStringFromMap(resource, "parentOrgUnitName", "parentName"),
|
||||
ID: firstStringFromMap(resource, "orgUnitId", "id"),
|
||||
ExternalID: firstStringFromMap(resource, "orgUnitExternalKey", "externalKey", "externalId"),
|
||||
DisplayName: firstStringFromMap(resource, "orgUnitName", "displayName", "name"),
|
||||
Email: email,
|
||||
MailLocalPart: worksmobileMailLocalPart(email),
|
||||
ParentID: firstStringFromMap(resource, "parentOrgUnitId", "parentId"),
|
||||
ParentName: firstStringFromMap(resource, "parentOrgUnitName", "parentName"),
|
||||
}
|
||||
}
|
||||
|
||||
func worksmobileMailLocalPart(value string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||
if normalized == "" {
|
||||
return ""
|
||||
}
|
||||
if at := strings.Index(normalized, "@"); at >= 0 {
|
||||
normalized = normalized[:at]
|
||||
}
|
||||
return strings.TrimSpace(normalized)
|
||||
}
|
||||
|
||||
func parseWorksmobileDirectoryUserName(resource map[string]any) string {
|
||||
if value := firstStringFromMap(resource, "displayName", "name"); value != "" {
|
||||
return value
|
||||
@@ -969,3 +1107,13 @@ func (c *WorksmobileHTTPClient) currentTime() time.Time {
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func (c *WorksmobileHTTPClient) orgUnitWriteDelay() time.Duration {
|
||||
if c.OrgUnitWriteDelay < 0 {
|
||||
return 0
|
||||
}
|
||||
if c.OrgUnitWriteDelay > 0 {
|
||||
return c.OrgUnitWriteDelay
|
||||
}
|
||||
return 1100 * time.Millisecond
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user