forked from baron/baron-sso
237 lines
6.3 KiB
Go
237 lines
6.3 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type KratosIdentity struct {
|
|
ID string `json:"id"`
|
|
Traits map[string]interface{} `json:"traits"`
|
|
State string `json:"state,omitempty"`
|
|
CreatedAt time.Time `json:"created_at,omitempty"`
|
|
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
|
}
|
|
|
|
type KratosAdminService struct {
|
|
AdminURL string
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
func NewKratosAdminService() *KratosAdminService {
|
|
return &KratosAdminService{
|
|
AdminURL: getenvKratos("KRATOS_ADMIN_URL", "http://kratos:4434"),
|
|
}
|
|
}
|
|
|
|
func (s *KratosAdminService) ListIdentities(ctx context.Context) ([]KratosIdentity, error) {
|
|
endpoint := strings.TrimRight(s.AdminURL, "/") + "/admin/identities"
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := s.httpClient().Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 300 {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
|
return nil, fmt.Errorf("kratos admin list identities failed status=%d body=%s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var identities []KratosIdentity
|
|
if err := json.NewDecoder(resp.Body).Decode(&identities); err != nil {
|
|
return nil, err
|
|
}
|
|
return identities, nil
|
|
}
|
|
|
|
func (s *KratosAdminService) FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error) {
|
|
identifier = strings.TrimSpace(identifier)
|
|
if identifier == "" {
|
|
return "", nil
|
|
}
|
|
|
|
endpoint := strings.TrimRight(s.AdminURL, "/") + "/admin/identities"
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
query := req.URL.Query()
|
|
query.Set("credentials_identifier", identifier)
|
|
req.URL.RawQuery = query.Encode()
|
|
|
|
resp, err := s.httpClient().Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return "", nil
|
|
}
|
|
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))
|
|
}
|
|
|
|
var identities []struct {
|
|
ID string `json:"id"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&identities); err != nil {
|
|
return "", err
|
|
}
|
|
if len(identities) == 0 {
|
|
return "", nil
|
|
}
|
|
return identities[0].ID, nil
|
|
}
|
|
|
|
func (s *KratosAdminService) GetIdentity(ctx context.Context, identityID string) (*KratosIdentity, error) {
|
|
endpoint := fmt.Sprintf("%s/admin/identities/%s", strings.TrimRight(s.AdminURL, "/"), identityID)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := s.httpClient().Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return nil, nil
|
|
}
|
|
if resp.StatusCode >= 300 {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
|
return nil, fmt.Errorf("kratos admin get identity failed status=%d body=%s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var identity KratosIdentity
|
|
if err := json.NewDecoder(resp.Body).Decode(&identity); err != nil {
|
|
return nil, err
|
|
}
|
|
return &identity, nil
|
|
}
|
|
|
|
func (s *KratosAdminService) UpdateIdentity(ctx context.Context, identityID string, traits map[string]interface{}, state string) (*KratosIdentity, error) {
|
|
payload := map[string]interface{}{
|
|
"schema_id": "default",
|
|
"traits": traits,
|
|
}
|
|
if strings.TrimSpace(state) != "" {
|
|
payload["state"] = strings.TrimSpace(state)
|
|
}
|
|
|
|
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))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := s.httpClient().Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 300 {
|
|
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
|
return nil, fmt.Errorf("kratos admin update identity failed status=%d body=%s", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
var updated KratosIdentity
|
|
if err := json.NewDecoder(resp.Body).Decode(&updated); err != nil {
|
|
return nil, err
|
|
}
|
|
return &updated, nil
|
|
}
|
|
|
|
func (s *KratosAdminService) UpdateIdentityPassword(ctx context.Context, identityID, newPassword string) error {
|
|
payload := map[string]interface{}{
|
|
"credentials": map[string]interface{}{
|
|
"password": map[string]interface{}{
|
|
"config": map[string]string{
|
|
"password": newPassword,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
endpoint := fmt.Sprintf("%s/admin/identities/%s", strings.TrimRight(s.AdminURL, "/"), identityID)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := s.httpClient().Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode >= 300 {
|
|
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
|
return fmt.Errorf("kratos admin update password failed status=%d body=%s", resp.StatusCode, string(respBody))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *KratosAdminService) DeleteIdentity(ctx context.Context, identityID string) error {
|
|
endpoint := fmt.Sprintf("%s/admin/identities/%s", strings.TrimRight(s.AdminURL, "/"), identityID)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := s.httpClient().Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 300 {
|
|
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
|
return fmt.Errorf("kratos admin delete identity failed status=%d body=%s", resp.StatusCode, string(respBody))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *KratosAdminService) httpClient() *http.Client {
|
|
if s.HTTPClient != nil {
|
|
return s.HTTPClient
|
|
}
|
|
return &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
Transport: &http.Transport{
|
|
DialContext: (&net.Dialer{
|
|
Timeout: 5 * time.Second,
|
|
KeepAlive: 30 * time.Second,
|
|
}).DialContext,
|
|
TLSHandshakeTimeout: 5 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
func getenvKratos(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|