160 lines
5.5 KiB
Go
160 lines
5.5 KiB
Go
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.basic_profile:readonly、应用发布状态及通讯录可见范围"})
|
|
return
|
|
}
|
|
c.JSON(200, gin.H{"available": true, "name": name, "source": source})
|
|
}
|