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}) }