1
0
forked from baron/baron-sso

동기화 기초구조 마련

This commit is contained in:
2026-05-12 12:25:31 +09:00
parent 3063450ee0
commit 5e649c279f
33 changed files with 3364 additions and 408 deletions

View File

@@ -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
}