forked from baron/baron-sso
chore: snapshot local state before dev merge
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user