1
0
forked from baron/baron-sso

merge: integrate origin dev into dev

Includes Worksmobile SSOT sync comparison updates, UUID import conflict resolution, and Playwright route mock stabilization.
This commit is contained in:
2026-06-01 17:48:39 +09:00
91 changed files with 2173 additions and 1268 deletions

View File

@@ -17,15 +17,14 @@ import (
)
type KratosIdentity struct {
ID string `json:"id"`
SchemaID string `json:"schema_id,omitempty"`
Traits map[string]interface{} `json:"traits"`
State string `json:"state,omitempty"`
MetadataAdmin interface{} `json:"metadata_admin,omitempty"`
MetadataPublic interface{} `json:"metadata_public,omitempty"`
ExternalID string `json:"external_id,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
ID string `json:"id"`
SchemaID string `json:"schema_id,omitempty"`
Traits map[string]any `json:"traits"`
State string `json:"state,omitempty"`
MetadataAdmin any `json:"metadata_admin,omitempty"`
MetadataPublic any `json:"metadata_public,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type KratosSessionDevice struct {
@@ -36,9 +35,9 @@ type KratosSessionDevice struct {
type KratosSession struct {
ID string `json:"id"`
Active bool `json:"active"`
AuthenticatedAt time.Time `json:"authenticated_at,omitempty"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
IssuedAt time.Time `json:"issued_at,omitempty"`
AuthenticatedAt time.Time `json:"authenticated_at"`
ExpiresAt time.Time `json:"expires_at"`
IssuedAt time.Time `json:"issued_at"`
Identity *KratosIdentity `json:"identity,omitempty"`
Devices []KratosSessionDevice `json:"devices,omitempty"`
}
@@ -47,7 +46,7 @@ type KratosAdminService interface {
ListIdentities(ctx context.Context) ([]KratosIdentity, error)
FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error)
GetIdentity(ctx context.Context, identityID string) (*KratosIdentity, error)
UpdateIdentity(ctx context.Context, identityID string, traits map[string]interface{}, state string) (*KratosIdentity, error)
UpdateIdentity(ctx context.Context, identityID string, traits map[string]any, state string) (*KratosIdentity, error)
UpdateIdentityPassword(ctx context.Context, identityID, newPassword string) error
DeleteIdentity(ctx context.Context, identityID string) error
CreateUser(ctx context.Context, user *domain.BrokerUser, password string) (string, error)
@@ -99,13 +98,66 @@ func (s *kratosAdminService) FindIdentityIDByIdentifier(ctx context.Context, ide
}
endpoint := strings.TrimRight(s.AdminURL, "/") + "/admin/identities"
// 1. Try credentials_identifier (Email/LoginID/Phone)
id, err := s.searchIdentities(ctx, endpoint, "credentials_identifier", identifier)
if err == nil && id != "" {
// VERIFY: Kratos sometimes ignores unknown query params and returns the first identity.
if s.verifyIdentityMatch(ctx, id, identifier) {
return id, nil
}
}
identity, err := s.GetIdentity(ctx, identifier)
if err == nil && identity != nil {
return identity.ID, nil
}
return "", nil
}
func (s *kratosAdminService) verifyIdentityMatch(ctx context.Context, id, identifier string) bool {
identity, err := s.GetIdentity(ctx, id)
if err != nil || identity == nil {
return false
}
// Exact ID match
if strings.EqualFold(identity.ID, identifier) {
return true
}
// Check traits (Email, CustomLoginIDs)
if email, ok := identity.Traits["email"].(string); ok && strings.EqualFold(email, identifier) {
return true
}
if phone, ok := identity.Traits["phone_number"].(string); ok && strings.EqualFold(phone, identifier) {
return true
}
if lids, ok := identity.Traits["custom_login_ids"].([]any); ok {
for _, lid := range lids {
if s, ok := lid.(string); ok && strings.EqualFold(s, identifier) {
return true
}
}
} else if lids, ok := identity.Traits["custom_login_ids"].([]string); ok {
for _, lid := range lids {
if strings.EqualFold(lid, identifier) {
return true
}
}
}
return false
}
func (s *kratosAdminService) searchIdentities(ctx context.Context, endpoint, key, value string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return "", err
}
query := req.URL.Query()
query.Set("credentials_identifier", identifier)
query.Set(key, value)
req.URL.RawQuery = query.Encode()
resp, err := s.httpClient().Do(req)
@@ -119,7 +171,7 @@ func (s *kratosAdminService) FindIdentityIDByIdentifier(ctx context.Context, ide
}
if resp.StatusCode >= 300 {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
return "", fmt.Errorf("kratos admin search failed status=%d body=%s", resp.StatusCode, string(body))
return "", fmt.Errorf("kratos admin search by %s failed status=%d body=%s", key, resp.StatusCode, string(body))
}
var identities []struct {
@@ -162,8 +214,8 @@ func (s *kratosAdminService) GetIdentity(ctx context.Context, identityID string)
return &identity, nil
}
func (s *kratosAdminService) UpdateIdentity(ctx context.Context, identityID string, traits map[string]interface{}, state string) (*KratosIdentity, error) {
payload := map[string]interface{}{
func (s *kratosAdminService) UpdateIdentity(ctx context.Context, identityID string, traits map[string]any, state string) (*KratosIdentity, error) {
payload := map[string]any{
"schema_id": "default",
"traits": traits,
}
@@ -211,12 +263,12 @@ func (s *kratosAdminService) UpdateIdentityPassword(ctx context.Context, identit
return err
}
payload := map[string]interface{}{
payload := map[string]any{
"schema_id": identity.SchemaID,
"traits": identity.Traits,
"state": identity.State,
"credentials": map[string]interface{}{
"password": map[string]interface{}{
"credentials": map[string]any{
"password": map[string]any{
"config": map[string]string{
"hashed_password": hashedPassword,
},
@@ -235,10 +287,6 @@ func (s *kratosAdminService) UpdateIdentityPassword(ctx context.Context, identit
if identity.MetadataPublic != nil {
payload["metadata_public"] = identity.MetadataPublic
}
if identity.ExternalID != "" {
payload["external_id"] = identity.ExternalID
}
body, _ := json.Marshal(payload)
endpoint := fmt.Sprintf("%s/admin/identities/%s", strings.TrimRight(s.AdminURL, "/"), identityID)
req, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint, bytes.NewReader(body))
@@ -263,8 +311,11 @@ func (s *kratosAdminService) CreateUser(ctx context.Context, user *domain.Broker
if user == nil {
return "", fmt.Errorf("kratos admin: user payload is nil")
}
if strings.TrimSpace(user.ID) != "" {
return "", fmt.Errorf("kratos admin: requested identity id import is disabled; use backup/restore")
}
traits := map[string]interface{}{
traits := map[string]any{
"email": user.Email,
"name": user.Name,
}
@@ -278,11 +329,11 @@ func (s *kratosAdminService) CreateUser(ctx context.Context, user *domain.Broker
traits[k] = v
}
payload := map[string]interface{}{
payload := map[string]any{
"schema_id": "default",
"traits": traits,
"credentials": map[string]interface{}{
"password": map[string]interface{}{
"credentials": map[string]any{
"password": map[string]any{
"config": map[string]string{
"password": password,
},
@@ -290,10 +341,6 @@ func (s *kratosAdminService) CreateUser(ctx context.Context, user *domain.Broker
},
"state": "active",
}
if requestedID := strings.TrimSpace(user.ID); requestedID != "" {
payload["id"] = requestedID
}
body, _ := json.Marshal(payload)
endpoint := strings.TrimRight(s.AdminURL, "/") + "/admin/identities"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
@@ -319,10 +366,6 @@ func (s *kratosAdminService) CreateUser(ctx context.Context, user *domain.Broker
if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
return "", err
}
if requestedID := strings.TrimSpace(user.ID); requestedID != "" && created.ID != requestedID {
return "", fmt.Errorf("kratos admin: requested identity id was not preserved requested=%s actual=%s", requestedID, created.ID)
}
return created.ID, nil
}