Files

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
}