117 lines
4.0 KiB
Go
117 lines
4.0 KiB
Go
package identityprovider
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
const feishuContactUser = "https://open.feishu.cn/open-apis/contact/v3/users/"
|
|
const feishuBasicBatch = "https://open.feishu.cn/open-apis/contact/v3/users/basic_batch?user_id_type=open_id"
|
|
|
|
func fetchDirectoryName(ctx context.Context, appID, appSecret, openID string) (string, error) {
|
|
name, _, err := fetchDirectoryNameWithSource(ctx, appID, appSecret, openID)
|
|
return name, err
|
|
}
|
|
|
|
func fetchDirectoryNameWithSource(ctx context.Context, appID, appSecret, openID string) (string, string, error) {
|
|
if !strings.HasPrefix(openID, "ou_") || len(openID) > 128 {
|
|
return "", "", errors.New("invalid open_id")
|
|
}
|
|
payload, _ := json.Marshal(map[string]string{"app_id": appID, "app_secret": appSecret})
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, feishuTenantToken, bytes.NewReader(payload))
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := upstreamClient.Do(req)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
var token struct {
|
|
Code *int `json:"code"`
|
|
TenantAccessToken string `json:"tenant_access_token"`
|
|
}
|
|
err = json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&token)
|
|
resp.Body.Close()
|
|
if err != nil || resp.StatusCode != http.StatusOK || token.Code == nil || *token.Code != 0 || token.TenantAccessToken == "" {
|
|
return "", "", errors.New("tenant token unavailable")
|
|
}
|
|
if name, err := fetchBasicName(ctx, token.TenantAccessToken, openID); err == nil {
|
|
return name, "basic_batch", nil
|
|
}
|
|
name, err := fetchContactName(ctx, token.TenantAccessToken, openID)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return name, "contact_user", nil
|
|
}
|
|
|
|
// basic_batch returns only core identity fields and does not require the user
|
|
// to be in the application's directory visibility range. Never accept an
|
|
// uncorrelated name from a batch response.
|
|
func fetchBasicName(ctx context.Context, token, openID string) (string, error) {
|
|
payload, _ := json.Marshal(map[string][]string{"user_ids": {openID}})
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, feishuBasicBatch, bytes.NewReader(payload))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := upstreamClient.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
var result struct {
|
|
Code *int `json:"code"`
|
|
Data struct {
|
|
Users []struct {
|
|
UserID string `json:"user_id"`
|
|
Name string `json:"name"`
|
|
} `json:"users"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&result); err != nil || resp.StatusCode != http.StatusOK || result.Code == nil || *result.Code != 0 {
|
|
return "", errors.New("basic identity unavailable")
|
|
}
|
|
for _, user := range result.Data.Users {
|
|
if user.UserID == openID && strings.TrimSpace(user.Name) != "" && len(user.Name) <= 256 {
|
|
return strings.TrimSpace(user.Name), nil
|
|
}
|
|
}
|
|
return "", errors.New("basic identity mismatch")
|
|
}
|
|
|
|
func fetchContactName(ctx context.Context, token, openID string) (string, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, feishuContactUser+url.PathEscape(openID)+"?user_id_type=open_id", nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err := upstreamClient.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
var result struct {
|
|
Code *int `json:"code"`
|
|
Data struct {
|
|
User struct {
|
|
OpenID string `json:"open_id"`
|
|
Name string `json:"name"`
|
|
} `json:"user"`
|
|
} `json:"data"`
|
|
}
|
|
err = json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&result)
|
|
if err != nil || resp.StatusCode != http.StatusOK || result.Code == nil || *result.Code != 0 || result.Data.User.OpenID != openID || strings.TrimSpace(result.Data.User.Name) == "" || len(result.Data.User.Name) > 256 {
|
|
return "", errors.New("directory identity unavailable")
|
|
}
|
|
return strings.TrimSpace(result.Data.User.Name), nil
|
|
}
|