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,159 @@
|
||||
package identityprovider
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"vpn-platform/backend/internal/model"
|
||||
"vpn-platform/backend/internal/service"
|
||||
"vpn-platform/backend/internal/store"
|
||||
)
|
||||
|
||||
func managedTunnel(c *gin.Context) (model.Tunnel, model.IdentityProvider, string, bool) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(400, gin.H{"error": "无效专线"})
|
||||
return model.Tunnel{}, model.IdentityProvider{}, "", false
|
||||
}
|
||||
var tunnel model.Tunnel
|
||||
if store.DB.First(&tunnel, id).Error != nil {
|
||||
c.JSON(404, gin.H{"error": "专线不存在"})
|
||||
return tunnel, model.IdentityProvider{}, "", false
|
||||
}
|
||||
cfg, err := store.TunnelConfig(&tunnel)
|
||||
if err != nil || tunnel.Protocol != "openvpn" || service.Str(cfg, "auth_mode") != "oidc" || service.Str(cfg, "oidc_provider") != "builtin" {
|
||||
c.JSON(400, gin.H{"error": "仅支持内置飞书 OpenVPN 专线"})
|
||||
return tunnel, model.IdentityProvider{}, "", false
|
||||
}
|
||||
idp, base, err := enabled()
|
||||
if err != nil || service.Str(cfg, "oidc_issuer") != base || service.Str(cfg, "oidc_client_id") != idp.ClientID {
|
||||
c.JSON(409, gin.H{"error": "专线与当前飞书身份服务不匹配"})
|
||||
return tunnel, idp, base, false
|
||||
}
|
||||
return tunnel, idp, base, true
|
||||
}
|
||||
|
||||
func (p *Provider) Profiles(c *gin.Context) {
|
||||
noStore(c)
|
||||
idp, base, err := enabled()
|
||||
if err != nil {
|
||||
c.JSON(409, gin.H{"error": "飞书身份服务未启用"})
|
||||
return
|
||||
}
|
||||
var profiles []model.IdentityProfile
|
||||
if err := store.DB.Where("issuer = ? AND app_id = ?", base, idp.AppID).Order("updated_at desc").Limit(500).Find(&profiles).Error; err != nil {
|
||||
c.JSON(500, gin.H{"error": "查询用户失败"})
|
||||
return
|
||||
}
|
||||
items := make([]gin.H, 0, len(profiles))
|
||||
for _, profile := range profiles {
|
||||
items = append(items, gin.H{"subject": profile.Subject, "name": profile.Name, "updated_at": profile.UpdatedAt})
|
||||
}
|
||||
c.JSON(200, gin.H{"profiles": items})
|
||||
}
|
||||
|
||||
func (p *Provider) Grants(c *gin.Context) {
|
||||
noStore(c)
|
||||
tunnel, idp, base, ok := managedTunnel(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var grants []model.FeishuTunnelGrant
|
||||
if err := store.DB.Where("tunnel_id = ? AND issuer = ? AND app_id = ?", tunnel.ID, base, idp.AppID).Order("created_at desc").Find(&grants).Error; err != nil {
|
||||
c.JSON(500, gin.H{"error": "查询授权失败"})
|
||||
return
|
||||
}
|
||||
if grants == nil {
|
||||
grants = []model.FeishuTunnelGrant{}
|
||||
}
|
||||
c.JSON(200, gin.H{"grants": grants})
|
||||
}
|
||||
|
||||
func (p *Provider) AddGrant(c *gin.Context) {
|
||||
noStore(c)
|
||||
tunnel, idp, base, ok := managedTunnel(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var input struct {
|
||||
Subject string `json:"subject"`
|
||||
}
|
||||
if c.ShouldBindJSON(&input) != nil || len(input.Subject) > 256 || !strings.HasPrefix(input.Subject, idp.TenantKey+":ou_") {
|
||||
c.JSON(400, gin.H{"error": "请选择已验证的飞书成员"})
|
||||
return
|
||||
}
|
||||
var profile model.IdentityProfile
|
||||
if store.DB.Where("issuer = ? AND app_id = ? AND subject = ?", base, idp.AppID, input.Subject).First(&profile).Error != nil {
|
||||
c.JSON(400, gin.H{"error": "用户尚未完成飞书验证"})
|
||||
return
|
||||
}
|
||||
grant := model.FeishuTunnelGrant{TunnelID: tunnel.ID, Issuer: base, AppID: idp.AppID, Subject: input.Subject}
|
||||
if err := store.DB.Where(&model.FeishuTunnelGrant{TunnelID: tunnel.ID, Issuer: base, AppID: idp.AppID, Subject: input.Subject}).FirstOrCreate(&grant).Error; err != nil {
|
||||
c.JSON(500, gin.H{"error": "授权失败"})
|
||||
return
|
||||
}
|
||||
store.Audit(c.GetString("username"), "feishu_grant_added", tunnel.Name, input.Subject)
|
||||
c.JSON(http.StatusOK, gin.H{"grant": grant})
|
||||
}
|
||||
|
||||
func (p *Provider) DeleteGrant(c *gin.Context) {
|
||||
noStore(c)
|
||||
tunnel, idp, base, ok := managedTunnel(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
grantID, err := strconv.ParseUint(c.Param("grant_id"), 10, 32)
|
||||
if err != nil || grantID == 0 {
|
||||
c.JSON(400, gin.H{"error": "无效授权"})
|
||||
return
|
||||
}
|
||||
var grant model.FeishuTunnelGrant
|
||||
if store.DB.Where("id = ? AND tunnel_id = ? AND issuer = ? AND app_id = ?", grantID, tunnel.ID, base, idp.AppID).First(&grant).Error != nil {
|
||||
c.JSON(404, gin.H{"error": "授权不存在"})
|
||||
return
|
||||
}
|
||||
if err := store.DB.Delete(&grant).Error; err != nil {
|
||||
c.JSON(500, gin.H{"error": "撤销失败"})
|
||||
return
|
||||
}
|
||||
store.Audit(c.GetString("username"), "feishu_grant_removed", tunnel.Name, grant.Subject)
|
||||
c.Status(204)
|
||||
}
|
||||
|
||||
func (p *Provider) DirectoryCheck(c *gin.Context) {
|
||||
noStore(c)
|
||||
if !secureAdmin(c) {
|
||||
c.JSON(403, gin.H{"error": "请通过可信 HTTPS 管理入口检查权限"})
|
||||
return
|
||||
}
|
||||
idp, base, err := enabled()
|
||||
if err != nil {
|
||||
c.JSON(409, gin.H{"error": "飞书身份服务未启用"})
|
||||
return
|
||||
}
|
||||
var input struct {
|
||||
Subject string `json:"subject"`
|
||||
}
|
||||
if c.ShouldBindJSON(&input) != nil || len(input.Subject) > 256 || !strings.HasPrefix(input.Subject, idp.TenantKey+":ou_") {
|
||||
c.JSON(400, gin.H{"error": "请选择已验证的飞书成员"})
|
||||
return
|
||||
}
|
||||
var profile model.IdentityProfile
|
||||
if store.DB.Where("issuer = ? AND app_id = ? AND subject = ?", base, idp.AppID, input.Subject).First(&profile).Error != nil {
|
||||
c.JSON(404, gin.H{"error": "用户尚未完成飞书验证"})
|
||||
return
|
||||
}
|
||||
secret, err := store.DecryptSecret(idp.AppSecret)
|
||||
if err != nil {
|
||||
c.JSON(503, gin.H{"error": "读取飞书配置失败"})
|
||||
return
|
||||
}
|
||||
name, source, err := fetchDirectoryNameWithSource(c.Request.Context(), idp.AppID, secret, strings.TrimPrefix(input.Subject, idp.TenantKey+":"))
|
||||
if err != nil {
|
||||
c.JSON(200, gin.H{"available": false, "hint": "请在飞书开放平台检查 contact:user.base:readonly 权限、应用发布状态和通讯录可见范围"})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"available": true, "name": name, "source": source})
|
||||
}
|
||||
Reference in new issue
Block a user