Improve Feishu permission checks and OpenVPN auth result page
This commit is contained in:
1 parent
0231c376c6
commit
373a0904be
9 files changed
+104
-40
No files matched your search
@@ -162,6 +162,7 @@ func (p *Provider) Status(c *gin.Context) {
|
||||
"tenant_key": cfg.TenantKey, "client_id": cfg.ClientID, "client_secret_configured": cfg.ClientSecret != "",
|
||||
"issuer": base, "feishu_callback": callback, "vpn_callback": vpnCallback,
|
||||
"tls_ready": tlsError == nil, "gateway_ready": issuerRoute,
|
||||
"user_scope_verified": cfg.UserScopeVerifiedAt != nil,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -218,7 +219,7 @@ func (p *Provider) Configure(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if cfg.AppID != req.AppID && req.Enabled && !autoTenant {
|
||||
if (cfg.AppID != req.AppID || req.AppSecret != "") && req.Enabled && !autoTenant {
|
||||
creds, err := resolveTenantCredentials(tenantCredentials{req.AppID, req.AppSecret}, cfg.AppID, cfg.AppSecret)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
@@ -245,6 +246,9 @@ func (p *Provider) Configure(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if cfg.AppID != req.AppID || req.AppSecret != "" || cfg.TenantKey != req.TenantKey {
|
||||
cfg.UserScopeVerifiedAt = nil
|
||||
}
|
||||
if req.Enabled && cfg.AppSecret == "" {
|
||||
c.JSON(400, gin.H{"error": "飞书 App Secret 未配置"})
|
||||
return
|
||||
|
||||
@@ -222,6 +222,9 @@ func (p *Provider) FeishuCallback(c *gin.Context) {
|
||||
oidcError(c, 503)
|
||||
return
|
||||
}
|
||||
// A successful user-info lookup confirms this application's browser scope.
|
||||
// Do not store user identifiers; reset this evidence when credentials change.
|
||||
_ = store.DB.Model(&model.IdentityProvider{}).Where("id = ? AND app_id = ? AND tenant_key = ?", cfg.ID, cfg.AppID, cfg.TenantKey).Update("user_scope_verified_at", time.Now()).Error
|
||||
u, _ := url.Parse(state.Redirect)
|
||||
q := u.Query()
|
||||
q.Set("code", code)
|
||||
|
||||
@@ -6,14 +6,15 @@ import "time"
|
||||
// IdentityProvider is the opt-in, single-tenant Feishu OIDC issuer configuration.
|
||||
// Credentials are encrypted with the platform key and never serialized in API responses.
|
||||
type IdentityProvider struct {
|
||||
ID uint `gorm:"primaryKey" json:"-"`
|
||||
Enabled bool `json:"enabled"`
|
||||
AppID string `gorm:"size:128" json:"app_id"`
|
||||
AppSecret string `gorm:"type:text" json:"-"`
|
||||
TenantKey string `gorm:"size:128" json:"tenant_key"`
|
||||
ClientID string `gorm:"size:128" json:"client_id"`
|
||||
ClientSecret string `gorm:"type:text" json:"-"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint `gorm:"primaryKey" json:"-"`
|
||||
Enabled bool `json:"enabled"`
|
||||
AppID string `gorm:"size:128" json:"app_id"`
|
||||
AppSecret string `gorm:"type:text" json:"-"`
|
||||
TenantKey string `gorm:"size:128" json:"tenant_key"`
|
||||
ClientID string `gorm:"size:128" json:"client_id"`
|
||||
ClientSecret string `gorm:"type:text" json:"-"`
|
||||
UserScopeVerifiedAt *time.Time `json:"-"` // Set only after a successful Feishu user-info callback.
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// IdentityGrant stores only a hash of each browser state, authorization code,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package openvpn
|
||||
|
||||
// This page is intentionally outcome-neutral: the auth component can render its
|
||||
// end template for different outcomes. Only the VPN client knows connection state.
|
||||
const authResultPage = `<!doctype html>
|
||||
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="light"><title>返回 VPN 客户端 · OPSX VPN</title>
|
||||
<style>
|
||||
*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;background:radial-gradient(circle at 50% 0%,#e8f2ff 0%,#f5f8fc 48%,#f3f6fa 100%);color:#1c334e;font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
||||
.card{width:min(100%,440px);padding:44px 36px 38px;background:#fff;border:1px solid #e3eaf3;border-radius:22px;box-shadow:0 22px 65px rgba(29,68,113,.09);text-align:center}
|
||||
.brand{color:#3975bb;letter-spacing:.16em;font-size:12px;font-weight:800}.mark{display:grid;place-items:center;width:72px;height:72px;margin:28px auto 22px;background:#ebf4ff;border-radius:22px;color:#3479c8}
|
||||
h1{font-size:25px;line-height:1.3;margin:0 0 12px}p{margin:0;color:#62758a}.tip{margin-top:30px;padding:14px;background:#f4f8fd;border-radius:12px;font-size:14px;color:#526a84}
|
||||
@media(max-width:480px){.card{padding:36px 24px}}
|
||||
</style></head><body><main class="card"><div class="brand">OPSX VPN</div>
|
||||
<div class="mark" aria-hidden="true"><svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19 3 33 9v9c0 9-5.8 14.6-14 17C10.8 32.6 5 27 5 18V9L19 3Z" stroke="currentColor" stroke-width="2.5"/><path d="m12.5 19 4.5 4.5 8.5-9" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
|
||||
<h1>请返回 VPN 客户端</h1><p>浏览器认证步骤已结束。连接状态请以客户端显示为准。</p>
|
||||
<div class="tip">此页面可以关闭</div></main></body></html>`
|
||||
@@ -62,10 +62,11 @@ func yamlString(s string) string {
|
||||
|
||||
func oidcConfig(t *model.Tunnel, cfg map[string]interface{}) string {
|
||||
dir := service.InstanceDir(t)
|
||||
return fmt.Sprintf("http:\n baseurl: %s\n listen: %s\n secret: %s\noauth2:\n issuer: %s\n client:\n id: %s\n secret: %s\nopenvpn:\n addr: %s\n password: %s\n auth-token-user: true\n",
|
||||
return fmt.Sprintf("http:\n baseurl: %s\n listen: %s\n secret: %s\n template: %s\noauth2:\n issuer: %s\n client:\n id: %s\n secret: %s\nopenvpn:\n addr: %s\n password: %s\n auth-token-user: true\n",
|
||||
yamlString(service.Str(cfg, "oidc_base_url")),
|
||||
yamlString(fmt.Sprintf("127.0.0.1:%d", int(service.Num(cfg, "oidc_http_port", 19090)))),
|
||||
yamlString(service.Str(cfg, "oidc_http_secret")),
|
||||
yamlString(filepath.Join(dir, "auth-result.html")),
|
||||
yamlString(service.Str(cfg, "oidc_issuer")),
|
||||
yamlString(service.Str(cfg, "oidc_client_id")),
|
||||
yamlString(service.Str(cfg, "oidc_client_secret")),
|
||||
@@ -140,6 +141,13 @@ func prepareOIDC(t *model.Tunnel, cfg map[string]interface{}) error {
|
||||
if err := os.Chmod(passwordPath, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
pagePath, err := service.WriteConfig(t, "auth-result.html", authResultPage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(pagePath, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
configPath, err := service.WriteConfig(t, "openvpn-auth-oauth2.yaml", oidcConfig(t, cfg))
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
Reference in new issue
Block a user