Resolve Feishu names by verified open ID and scope VPN member access
This commit is contained in:
1 parent
578a58a129
commit
d4eddf7365
19 files changed
+837
-35
No files matched your search
@@ -0,0 +1,116 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user