1
0
forked from baron/baron-sso

chore: snapshot local state before dev merge

This commit is contained in:
2026-06-17 21:25:42 +09:00
parent b2808759d2
commit 49560e8a8c
107 changed files with 8958 additions and 939 deletions

View File

@@ -53,6 +53,8 @@ type WorksmobileHTTPClient struct {
DomainIDs []int64
OrgUnitWriteDelay time.Duration
tokenCache worksmobileAccessTokenCache
levelCache map[int64][]WorksmobileUserLevel
levelCacheMu sync.Mutex
now func() time.Time
}
@@ -326,8 +328,21 @@ func (c *WorksmobileHTTPClient) DeleteOrgUnit(ctx context.Context, orgUnitID str
}
func (c *WorksmobileHTTPClient) CreateUser(ctx context.Context, payload WorksmobileUserPayload) error {
var err error
payload = normalizeWorksmobileUserCreatePayload(payload)
return c.sendDirectoryJSON(ctx, http.MethodPost, "/v1.0/users", payload)
levelDomainID := worksmobilePayloadLevelDomainID(payload)
levelID, err := c.resolveWorksmobilePayloadLevelIDForDomain(ctx, payload, levelDomainID)
if err != nil {
return err
}
payload.LevelID = ""
if err := c.sendDirectoryJSON(ctx, http.MethodPost, "/v1.0/users", payload); err != nil {
return err
}
if levelID != "" {
return c.PatchUserOrganizationLevelByName(ctx, payload.Email, levelDomainID, levelID)
}
return nil
}
func (c *WorksmobileHTTPClient) UpsertUser(ctx context.Context, payload WorksmobileUserPayload) error {
@@ -346,6 +361,12 @@ func (c *WorksmobileHTTPClient) UpdateUserOnly(ctx context.Context, payload Work
}
func (c *WorksmobileHTTPClient) updateUserByPatchOnly(ctx context.Context, payload WorksmobileUserPayload) error {
levelDomainID := worksmobilePayloadLevelDomainID(payload)
levelID, err := c.resolveWorksmobilePayloadLevelIDForDomain(ctx, payload, levelDomainID)
if err != nil {
return err
}
payload.LevelID = ""
identifier := strings.TrimSpace(payload.Email)
if identifier == "" {
identifier = strings.TrimSpace(payload.UserExternalKey)
@@ -369,6 +390,9 @@ func (c *WorksmobileHTTPClient) updateUserByPatchOnly(ctx context.Context, paylo
}
return patchErr
}
if levelID != "" {
return c.PatchUserOrganizationLevelByName(ctx, identifier, levelDomainID, levelID)
}
return nil
}
@@ -585,6 +609,221 @@ func (c *WorksmobileHTTPClient) PatchUser(ctx context.Context, identifier string
return c.sendDirectoryJSON(ctx, http.MethodPatch, "/v1.0/users/"+url.PathEscape(identifier), payload)
}
func (c *WorksmobileHTTPClient) PatchUserLevel(ctx context.Context, identifier string, domainID int64, levelID string) error {
identifier = strings.TrimSpace(identifier)
levelID = strings.TrimSpace(levelID)
if identifier == "" {
return fmt.Errorf("worksmobile user identifier is required")
}
if domainID <= 0 {
return fmt.Errorf("worksmobile domain id is required")
}
if levelID == "" {
return nil
}
payload := map[string]any{
"domainId": domainID,
"level": WorksmobileUserLevelRef{
LevelID: levelID,
},
}
return c.sendDirectoryJSON(ctx, http.MethodPatch, "/v1.0/users/"+url.PathEscape(identifier), payload)
}
func (c *WorksmobileHTTPClient) PatchUserLevelByName(ctx context.Context, identifier string, domainID int64, levelName string) error {
payload, err := c.resolveWorksmobilePayloadLevelID(ctx, WorksmobileUserPayload{
DomainID: domainID,
LevelID: levelName,
})
if err != nil {
return err
}
return c.PatchUserLevel(ctx, identifier, domainID, payload.LevelID)
}
func (c *WorksmobileHTTPClient) PatchUserOrganizationLevelByName(ctx context.Context, identifier string, domainID int64, levelName string) error {
identifier = strings.TrimSpace(identifier)
if identifier == "" {
return fmt.Errorf("worksmobile user identifier is required")
}
payload, err := c.resolveWorksmobilePayloadLevelID(ctx, WorksmobileUserPayload{
DomainID: domainID,
LevelID: levelName,
})
if err != nil {
return err
}
var raw map[string]any
if err := c.getDirectoryJSON(ctx, "/v1.0/users/"+url.PathEscape(identifier), &raw); err != nil {
return err
}
rawOrganizations, ok := raw["organizations"].([]any)
if !ok || len(rawOrganizations) == 0 {
return fmt.Errorf("worksmobile user organizations are missing: %s", identifier)
}
organizations := make([]any, 0, len(rawOrganizations))
updated := false
for _, rawOrganization := range rawOrganizations {
organization, ok := rawOrganization.(map[string]any)
if !ok {
continue
}
next := make(map[string]any, len(organization)+1)
for key, value := range organization {
next[key] = value
}
if !updated && worksmobileRawDomainID(next["domainId"]) == domainID {
next["levelId"] = payload.LevelID
updated = true
}
organizations = append(organizations, next)
}
if !updated {
return fmt.Errorf("worksmobile user organization not found for domain_id=%d: %s", domainID, identifier)
}
request := map[string]any{
"domainId": domainID,
"email": firstStringFromMap(raw, "email", "loginId", "userName"),
"userName": raw["userName"],
"organizations": organizations,
}
if value := firstStringFromMap(raw, "userExternalKey", "externalKey", "externalId"); value != "" {
request["userExternalKey"] = value
}
return c.sendDirectoryJSON(ctx, http.MethodPatch, "/v1.0/users/"+url.PathEscape(identifier), request)
}
func worksmobileRawDomainID(raw any) int64 {
switch value := raw.(type) {
case int64:
return value
case int:
return int64(value)
case float64:
return int64(value)
case json.Number:
parsed, _ := value.Int64()
return parsed
case string:
parsed, _ := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
return parsed
default:
return 0
}
}
func (c *WorksmobileHTTPClient) ListUserLevels(ctx context.Context, domainID int64) ([]WorksmobileUserLevel, error) {
if domainID <= 0 {
return nil, fmt.Errorf("worksmobile domain id is required")
}
c.levelCacheMu.Lock()
if c.levelCache != nil {
if cached, ok := c.levelCache[domainID]; ok {
c.levelCacheMu.Unlock()
return cached, nil
}
}
c.levelCacheMu.Unlock()
var response struct {
Levels []WorksmobileUserLevel `json:"levels"`
}
if err := c.getDirectoryJSON(ctx, "/v1.0/users/levels?domainId="+strconv.FormatInt(domainID, 10), &response); err != nil {
return nil, err
}
c.levelCacheMu.Lock()
if c.levelCache == nil {
c.levelCache = map[int64][]WorksmobileUserLevel{}
}
c.levelCache[domainID] = response.Levels
c.levelCacheMu.Unlock()
return response.Levels, nil
}
func (c *WorksmobileHTTPClient) resolveWorksmobilePayloadLevelID(ctx context.Context, payload WorksmobileUserPayload) (WorksmobileUserPayload, error) {
level := strings.TrimSpace(payload.LevelID)
if level == "" {
return payload, nil
}
if isLikelyWorksmobileUUID(level) {
payload.LevelID = level
return payload, nil
}
if isWorksmobileExternalKeyLevelID(level) {
payload.LevelID = level
return payload, nil
}
levels, err := c.ListUserLevels(ctx, payload.DomainID)
if err != nil {
return WorksmobileUserPayload{}, err
}
for _, candidate := range levels {
if strings.TrimSpace(candidate.LevelID) == level || strings.TrimSpace(candidate.LevelName) == level {
payload.LevelID = strings.TrimSpace(candidate.LevelID)
return payload, nil
}
}
return WorksmobileUserPayload{}, fmt.Errorf("worksmobile level not found: domain_id=%d level=%s", payload.DomainID, level)
}
func (c *WorksmobileHTTPClient) resolveWorksmobilePayloadLevelIDForDomain(ctx context.Context, payload WorksmobileUserPayload, domainID int64) (string, error) {
level := strings.TrimSpace(payload.LevelID)
if level == "" {
return "", nil
}
levelPayload := payload
levelPayload.DomainID = domainID
resolved, err := c.resolveWorksmobilePayloadLevelID(ctx, levelPayload)
if err != nil {
return "", err
}
return strings.TrimSpace(resolved.LevelID), nil
}
func worksmobilePayloadLevelDomainID(payload WorksmobileUserPayload) int64 {
if payload.LevelDomainID > 0 {
return payload.LevelDomainID
}
if domainID := worksmobilePayloadPrimaryOrganizationDomainID(payload); domainID > 0 {
return domainID
}
return payload.DomainID
}
func worksmobilePayloadPrimaryOrganizationDomainID(payload WorksmobileUserPayload) int64 {
for _, organization := range payload.Organizations {
if organization.Primary && organization.DomainID > 0 {
return organization.DomainID
}
}
for _, organization := range payload.Organizations {
if organization.DomainID > 0 {
return organization.DomainID
}
}
return 0
}
func isLikelyWorksmobileUUID(value string) bool {
value = strings.TrimSpace(value)
if len(value) != 36 {
return false
}
for i, ch := range value {
if i == 8 || i == 13 || i == 18 || i == 23 {
if ch != '-' {
return false
}
continue
}
if (ch < '0' || ch > '9') && (ch < 'a' || ch > 'f') && (ch < 'A' || ch > 'F') {
return false
}
}
return true
}
func (c *WorksmobileHTTPClient) DeleteUser(ctx context.Context, userID string) error {
userID = strings.TrimSpace(userID)
if userID == "" {
@@ -1074,6 +1313,15 @@ type WorksmobileUserPatchPayload struct {
Organizations []WorksmobileUserOrganization `json:"organizations,omitempty"`
}
type WorksmobileUserLevelRef struct {
LevelID string `json:"levelId"`
}
type WorksmobileUserLevel struct {
LevelID string `json:"levelId"`
LevelName string `json:"levelName"`
}
type WorksmobileOrgUnitPatchPayload struct {
DomainID int64 `json:"domainId"`
Email string `json:"email,omitempty"`
@@ -1268,6 +1516,7 @@ func parseWorksmobileDirectoryUser(resource map[string]any) WorksmobileRemoteUse
"employeeId",
"employeeID",
),
DomainID: worksmobileRawDomainID(resource["domainId"]),
LevelID: parseWorksmobileUserLevelID(resource),
LevelName: parseWorksmobileUserLevelName(resource),
Task: firstStringFromMap(resource, "task", "job", "jobDescription"),
@@ -1396,6 +1645,9 @@ func parseWorksmobileUserLevelID(resource map[string]any) string {
if level, ok := resource["level"].(map[string]any); ok {
return firstStringFromMap(level, "levelId", "id", "value")
}
if value := parseWorksmobileOrganizationLevel(resource, "levelId", "id", "value"); value != "" {
return value
}
return ""
}
@@ -1406,9 +1658,42 @@ func parseWorksmobileUserLevelName(resource map[string]any) string {
if level, ok := resource["level"].(map[string]any); ok {
return firstStringFromMap(level, "levelName", "displayName", "name")
}
if value := parseWorksmobileOrganizationLevel(resource, "levelName", "displayName", "name"); value != "" {
return value
}
return ""
}
func parseWorksmobileOrganizationLevel(resource map[string]any, keys ...string) string {
rawOrganizations, ok := resource["organizations"].([]any)
if !ok {
return ""
}
fallback := ""
for _, raw := range rawOrganizations {
organization, ok := raw.(map[string]any)
if !ok {
continue
}
value := firstStringFromMap(organization, keys...)
if value == "" {
if level, ok := organization["level"].(map[string]any); ok {
value = firstStringFromMap(level, keys...)
}
}
if value == "" {
continue
}
if boolFromMap(organization, "primary") {
return value
}
if fallback == "" {
fallback = value
}
}
return fallback
}
type worksmobileOrgUnitDetail struct {
ID string
Name string
@@ -1481,11 +1766,23 @@ func parseWorksmobileUserOrganizationList(raw any) []WorksmobileUserOrganization
if len(orgUnits) == 0 {
continue
}
levelID := firstStringFromMap(organization, "levelId")
levelName := firstStringFromMap(organization, "levelName")
if level, ok := organization["level"].(map[string]any); ok {
if levelID == "" {
levelID = firstStringFromMap(level, "levelId", "id", "value")
}
if levelName == "" {
levelName = firstStringFromMap(level, "levelName", "displayName", "name")
}
}
organizations = append(organizations, WorksmobileUserOrganization{
DomainID: int64FromMap(organization, "domainId"),
Email: firstStringFromMap(organization, "email"),
Primary: boolFromMap(organization, "primary"),
OrgUnits: orgUnits,
DomainID: int64FromMap(organization, "domainId"),
Email: firstStringFromMap(organization, "email"),
Primary: boolFromMap(organization, "primary"),
LevelID: levelID,
LevelName: levelName,
OrgUnits: orgUnits,
})
}
return organizations