diff --git a/Dockerfile b/Dockerfile index 6ef23975..80448312 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.20.6-alpine3.18 as builder +FROM golang:1.25.0-alpine3.22 as builder WORKDIR $GOPATH/src/github.com/feiyu563/PrometheusAlert @@ -14,7 +14,7 @@ COPY . $GOPATH/src/github.com/feiyu563/PrometheusAlert RUN go build # ----------------------------------------------------------------------------- -FROM alpine:3.18 +FROM alpine:3.22 LABEL maintainer="jikun.zhang" diff --git a/conf/app-example.conf b/conf/app-example.conf index 1592f0d4..fcd31dcb 100644 --- a/conf/app-example.conf +++ b/conf/app-example.conf @@ -62,6 +62,29 @@ maxIdleConns=100 # 热更新配置文件 open-hotreload=0 +#---------------------↓OIDC SSO配置----------------------- +#是否开启OIDC单点登录,0为关闭,1为开启,开启后仍保留本地账号密码作为应急登录方式 +sso_enabled=0 +#OIDC Provider的Issuer地址,例如:https://id.example.com/realms/ops +sso_issuer_url= +#OIDC Client配置,客户端须启用Authorization Code Flow,并注册下方回调地址 +sso_client_id= +sso_client_secret= +sso_redirect_url=https://prometheusalert.example.com/sso/callback +#OIDC授权Claim路径,默认groups,其他Provider可配置为roles、realm_access.roles等实际返回的Claim +sso_authorization_claim=groups +#允许访问的Claim值,多个值使用英文逗号分隔,使用Group Membership Mapper且关闭Full group path时填组名即可 +sso_authorization_values=prometheusalert-admin +#SSO Cookie加密密钥,建议配置32位以上随机字符串 +sso_cookie_secret= +#生产环境HTTPS时设为1,本地HTTP调试时设为0 +sso_cookie_secure=1 +#SSO会话有效期,单位秒,权限变更在下次登录或会话到期后生效 +sso_session_ttl_seconds=28800 +#OIDC Provider的登出地址及回跳地址,Provider支持RP-Initiated Logout时配置 +sso_logout_url= +sso_post_logout_redirect_url=https://prometheusalert.example.com/login + #---------------------↓webhook----------------------- #是否开启钉钉告警通道,可同时开始多个通道0为关闭,1为开启 open-dingding=1 diff --git a/controllers/WebLogin.go b/controllers/WebLogin.go index 7415ffa3..12d62d81 100644 --- a/controllers/WebLogin.go +++ b/controllers/WebLogin.go @@ -18,6 +18,7 @@ func (c *LoginController) Prepare() { title = "PrometheusAlert" } c.Data["AppTitle"] = title + c.Data["SSOEnabled"] = IsSSOEnabled() } func (c *LoginController) Get() { @@ -26,6 +27,11 @@ func (c *LoginController) Get() { c.Ctx.SetCookie("username", "", -1, "/") c.Ctx.SetCookie("logintoken", "", -1, "/") c.Ctx.SetCookie("password", "", -1, "/") // 清除旧版本可能残留的明文密码 cookie + clearSSOCookies(c.Ctx.ResponseWriter) + if logoutRedirect := SSOLogoutRedirect(); logoutRedirect != "" { + c.Redirect(logoutRedirect, 302) + return + } c.Redirect("/", 302) return } @@ -65,18 +71,40 @@ func (c *LoginController) Post() { //return } +func (c *LoginController) Session() { + username := CurrentAccountName(c.Ctx) + if username == "" { + c.Ctx.Output.SetStatus(401) + c.Data["json"] = map[string]interface{}{"authenticated": false} + c.ServeJSON() + return + } + c.Data["json"] = map[string]interface{}{ + "authenticated": true, + "username": username, + } + c.ServeJSON() +} + // 检查cookie是否为登录状态 func CheckAccount(mycookie *context.Context) bool { + return CurrentAccountName(mycookie) != "" +} + +func CurrentAccountName(mycookie *context.Context) string { ckUser, err := mycookie.Request.Cookie("username") if err != nil { - return false + return SSOAccountName(mycookie) } ckToken, err := mycookie.Request.Cookie("logintoken") if err != nil { - return false + return SSOAccountName(mycookie) } cfgUser := beego.AppConfig.String("login_user") cfgPwd := beego.AppConfig.String("login_password") expectedToken := fmt.Sprintf("%x", sha256.Sum256([]byte(cfgUser+cfgPwd+"PrometheusAlert"))) - return ckUser.Value == cfgUser && ckToken.Value == expectedToken + if ckUser.Value == cfgUser && ckToken.Value == expectedToken { + return ckUser.Value + } + return SSOAccountName(mycookie) } diff --git a/controllers/sso.go b/controllers/sso.go new file mode 100644 index 00000000..0c2d7124 --- /dev/null +++ b/controllers/sso.go @@ -0,0 +1,374 @@ +package controllers + +import ( + "context" + "crypto/rand" + "crypto/sha512" + "encoding/base64" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/astaxie/beego" + beecontext "github.com/astaxie/beego/context" + "github.com/coreos/go-oidc/v3/oidc" + "github.com/gorilla/securecookie" + "golang.org/x/oauth2" +) + +const ( + oidcStateCookieName = "pa_oidc_state" + oidcSessionCookieName = "pa_oidc_session" + oidcStateTTL = 10 * time.Minute + defaultOIDCSessionTTL = 8 * time.Hour +) + +type oidcState struct { + State string + Nonce string + ExpiresAt int64 +} + +type oidcSession struct { + Subject string + Username string + AuthorizationValues []string + ExpiresAt int64 +} + +func IsSSOEnabled() bool { + return beego.AppConfig.DefaultString("sso_enabled", "0") == "1" +} + +func ssoCookie(maxAge time.Duration) (*securecookie.SecureCookie, error) { + secret := strings.TrimSpace(beego.AppConfig.String("sso_cookie_secret")) + if len(secret) < 32 { + return nil, fmt.Errorf("sso_cookie_secret must contain at least 32 characters") + } + + keyMaterial := sha512.Sum512([]byte(secret)) + codec := securecookie.New(keyMaterial[:32], keyMaterial[32:]) + codec.MaxAge(int(maxAge.Seconds())) + return codec, nil +} + +func ssoCookieSecure() bool { + return beego.AppConfig.DefaultString("sso_cookie_secure", "1") == "1" +} + +func setSSOCookie(w http.ResponseWriter, name, value string, maxAge time.Duration) { + http.SetCookie(w, &http.Cookie{ + Name: name, + Value: value, + Path: "/", + MaxAge: int(maxAge.Seconds()), + HttpOnly: true, + Secure: ssoCookieSecure(), + SameSite: http.SameSiteLaxMode, + }) +} + +func clearSSOCookies(w http.ResponseWriter) { + for _, name := range []string{oidcStateCookieName, oidcSessionCookieName} { + http.SetCookie(w, &http.Cookie{ + Name: name, + Path: "/", + MaxAge: -1, + HttpOnly: true, + Secure: ssoCookieSecure(), + SameSite: http.SameSiteLaxMode, + }) + } +} + +func randomString() (string, error) { + value := make([]byte, 32) + if _, err := rand.Read(value); err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(value), nil +} + +func oidcConfig(ctxContext context.Context) (*oauth2.Config, *oidc.IDTokenVerifier, error) { + issuerURL := strings.TrimSpace(beego.AppConfig.String("sso_issuer_url")) + clientID := strings.TrimSpace(beego.AppConfig.String("sso_client_id")) + clientSecret := beego.AppConfig.String("sso_client_secret") + redirectURL := strings.TrimSpace(beego.AppConfig.String("sso_redirect_url")) + if issuerURL == "" || clientID == "" || clientSecret == "" || redirectURL == "" { + return nil, nil, fmt.Errorf("SSO is enabled but the issuer URL, client ID, client secret, or redirect URL is missing") + } + + provider, err := oidc.NewProvider(ctxContext, issuerURL) + if err != nil { + return nil, nil, fmt.Errorf("discover OIDC provider: %w", err) + } + + return &oauth2.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + Endpoint: provider.Endpoint(), + RedirectURL: redirectURL, + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + }, provider.Verifier(&oidc.Config{ClientID: clientID}), nil +} + +func ssoSessionTTL() time.Duration { + seconds, err := beego.AppConfig.Int("sso_session_ttl_seconds") + if err != nil || seconds <= 0 { + return defaultOIDCSessionTTL + } + return time.Duration(seconds) * time.Second +} + +func allowedAuthorizationValues() map[string]struct{} { + allowedValues := make(map[string]struct{}) + for _, value := range strings.Split(beego.AppConfig.String("sso_authorization_values"), ",") { + if value = strings.TrimSpace(value); value != "" { + allowedValues[value] = struct{}{} + } + } + return allowedValues +} + +func hasAllowedAuthorizationValue(values []string) bool { + allowed := allowedAuthorizationValues() + if len(allowed) == 0 { + return false + } + for _, value := range values { + if _, ok := allowed[value]; ok { + return true + } + } + return false +} + +func authorizationValuesFromClaims(claims map[string]interface{}) ([]string, error) { + claimPath := strings.TrimSpace(beego.AppConfig.DefaultString("sso_authorization_claim", "groups")) + var claimValue interface{} = claims + for _, part := range strings.Split(claimPath, ".") { + claimMap, ok := claimValue.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("OIDC authorization claim %q is not an object path", claimPath) + } + claimValue, ok = claimMap[part] + if !ok { + return nil, fmt.Errorf("OIDC authorization claim %q is missing", claimPath) + } + } + + switch values := claimValue.(type) { + case []interface{}: + result := make([]string, 0, len(values)) + for _, value := range values { + valueName, ok := value.(string) + if !ok { + return nil, fmt.Errorf("OIDC authorization claim %q contains a non-string value", claimPath) + } + result = append(result, valueName) + } + return result, nil + case []string: + return values, nil + case string: + return strings.Split(values, ","), nil + default: + return nil, fmt.Errorf("OIDC authorization claim %q must be a string or string array", claimPath) + } +} + +func (c *LoginController) SSOLogin() { + if !IsSSOEnabled() { + c.Abort("404") + return + } + + state, err := randomString() + if err != nil { + http.Error(c.Ctx.ResponseWriter, "generate SSO state: "+err.Error(), http.StatusInternalServerError) + return + } + nonce, err := randomString() + if err != nil { + http.Error(c.Ctx.ResponseWriter, "generate SSO nonce: "+err.Error(), http.StatusInternalServerError) + return + } + + codec, err := ssoCookie(oidcStateTTL) + if err != nil { + http.Error(c.Ctx.ResponseWriter, err.Error(), http.StatusInternalServerError) + return + } + encodedState, err := codec.Encode(oidcStateCookieName, oidcState{ + State: state, + Nonce: nonce, + ExpiresAt: time.Now().Add(oidcStateTTL).Unix(), + }) + if err != nil { + http.Error(c.Ctx.ResponseWriter, "encode SSO state: "+err.Error(), http.StatusInternalServerError) + return + } + + oauthConfig, _, err := oidcConfig(c.Ctx.Request.Context()) + if err != nil { + http.Error(c.Ctx.ResponseWriter, err.Error(), http.StatusServiceUnavailable) + return + } + setSSOCookie(c.Ctx.ResponseWriter, oidcStateCookieName, encodedState, oidcStateTTL) + c.Redirect(oauthConfig.AuthCodeURL(state, oidc.Nonce(nonce)), http.StatusFound) +} + +func (c *LoginController) SSOCallback() { + if !IsSSOEnabled() { + c.Abort("404") + return + } + if providerError := c.GetString("error"); providerError != "" { + http.Error(c.Ctx.ResponseWriter, "OIDC login failed: "+providerError, http.StatusUnauthorized) + return + } + + stateCookie, err := c.Ctx.Request.Cookie(oidcStateCookieName) + if err != nil { + http.Error(c.Ctx.ResponseWriter, "missing SSO state", http.StatusBadRequest) + return + } + codec, err := ssoCookie(oidcStateTTL) + if err != nil { + http.Error(c.Ctx.ResponseWriter, err.Error(), http.StatusInternalServerError) + return + } + var savedState oidcState + if err := codec.Decode(oidcStateCookieName, stateCookie.Value, &savedState); err != nil || + savedState.ExpiresAt < time.Now().Unix() || + savedState.State != c.GetString("state") { + http.Error(c.Ctx.ResponseWriter, "invalid SSO state", http.StatusBadRequest) + return + } + + oauthConfig, verifier, err := oidcConfig(c.Ctx.Request.Context()) + if err != nil { + http.Error(c.Ctx.ResponseWriter, err.Error(), http.StatusServiceUnavailable) + return + } + token, err := oauthConfig.Exchange(c.Ctx.Request.Context(), c.GetString("code")) + if err != nil { + http.Error(c.Ctx.ResponseWriter, "exchange authorization code: "+err.Error(), http.StatusUnauthorized) + return + } + rawIDToken, ok := token.Extra("id_token").(string) + if !ok { + http.Error(c.Ctx.ResponseWriter, "OIDC provider response did not contain an ID token", http.StatusUnauthorized) + return + } + idToken, err := verifier.Verify(c.Ctx.Request.Context(), rawIDToken) + if err != nil || idToken.Nonce != savedState.Nonce { + http.Error(c.Ctx.ResponseWriter, "invalid ID token", http.StatusUnauthorized) + return + } + + var claims map[string]interface{} + if err := idToken.Claims(&claims); err != nil { + http.Error(c.Ctx.ResponseWriter, "decode ID token claims: "+err.Error(), http.StatusUnauthorized) + return + } + authorizationValues, err := authorizationValuesFromClaims(claims) + if err != nil { + http.Error(c.Ctx.ResponseWriter, err.Error(), http.StatusForbidden) + return + } + if !hasAllowedAuthorizationValue(authorizationValues) { + http.Error(c.Ctx.ResponseWriter, "your OIDC account is not authorized for PrometheusAlert", http.StatusForbidden) + return + } + + subject, _ := claims["sub"].(string) + if subject == "" { + http.Error(c.Ctx.ResponseWriter, "OIDC ID token did not contain a subject", http.StatusUnauthorized) + return + } + username, _ := claims["preferred_username"].(string) + if username == "" { + username, _ = claims["email"].(string) + } + if username == "" { + username = subject + } + sessionTTL := ssoSessionTTL() + sessionCodec, err := ssoCookie(sessionTTL) + if err != nil { + http.Error(c.Ctx.ResponseWriter, err.Error(), http.StatusInternalServerError) + return + } + encodedSession, err := sessionCodec.Encode(oidcSessionCookieName, oidcSession{ + Subject: subject, + Username: username, + AuthorizationValues: authorizationValues, + ExpiresAt: time.Now().Add(sessionTTL).Unix(), + }) + if err != nil { + http.Error(c.Ctx.ResponseWriter, "encode SSO session: "+err.Error(), http.StatusInternalServerError) + return + } + + clearSSOCookies(c.Ctx.ResponseWriter) + setSSOCookie(c.Ctx.ResponseWriter, oidcSessionCookieName, encodedSession, sessionTTL) + c.Redirect("/", http.StatusFound) +} + +func CheckSSOAccount(ctx *beecontext.Context) bool { + _, ok := currentSSOSession(ctx) + return ok +} + +func SSOAccountName(ctx *beecontext.Context) string { + session, ok := currentSSOSession(ctx) + if !ok { + return "" + } + return session.Username +} + +func currentSSOSession(ctx *beecontext.Context) (*oidcSession, bool) { + if !IsSSOEnabled() { + return nil, false + } + cookie, err := ctx.Request.Cookie(oidcSessionCookieName) + if err != nil { + return nil, false + } + codec, err := ssoCookie(ssoSessionTTL()) + if err != nil { + return nil, false + } + var session oidcSession + if err := codec.Decode(oidcSessionCookieName, cookie.Value, &session); err != nil { + return nil, false + } + if session.Subject == "" || session.ExpiresAt < time.Now().Unix() || !hasAllowedAuthorizationValue(session.AuthorizationValues) { + return nil, false + } + return &session, true +} + +func SSOLogoutRedirect() string { + logoutURL := strings.TrimSpace(beego.AppConfig.String("sso_logout_url")) + if logoutURL == "" { + return "" + } + parsedURL, err := url.Parse(logoutURL) + if err != nil { + return "" + } + query := parsedURL.Query() + if redirectURL := strings.TrimSpace(beego.AppConfig.String("sso_post_logout_redirect_url")); redirectURL != "" { + query.Set("post_logout_redirect_uri", redirectURL) + } + if clientID := strings.TrimSpace(beego.AppConfig.String("sso_client_id")); clientID != "" { + query.Set("client_id", clientID) + } + parsedURL.RawQuery = query.Encode() + return parsedURL.String() +} diff --git a/go.mod b/go.mod index 145e1799..bf28f2fc 100644 --- a/go.mod +++ b/go.mod @@ -1,23 +1,24 @@ module PrometheusAlert -go 1.20 +go 1.25.0 require ( github.com/IBM/sarama v1.43.2 github.com/aliyun/alibaba-cloud-sdk-go v1.62.771 github.com/astaxie/beego v1.12.1 github.com/baidubce/bce-sdk-go v0.9.183 + github.com/coreos/go-oidc/v3 v3.20.0 github.com/elastic/go-elasticsearch/v7 v7.17.10 github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df github.com/go-sql-driver/mysql v1.8.1 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 + github.com/gorilla/securecookie v1.1.2 github.com/lib/pq v1.10.9 github.com/mattn/go-sqlite3 v1.14.32 github.com/prometheus/client_golang v1.19.1 github.com/robfig/cron/v3 v3.0.1 - github.com/smartystreets/goconvey v1.8.1 - github.com/stretchr/testify v1.9.0 github.com/ysicing/workwxbot v1.1.0 + golang.org/x/oauth2 v0.36.0 ) require ( @@ -28,8 +29,8 @@ require ( github.com/eapache/go-resiliency v1.6.0 // indirect github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect github.com/eapache/queue v1.1.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect @@ -40,19 +41,16 @@ require ( github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/jtolds/gls v4.20.0+incompatible // indirect github.com/klauspost/compress v1.17.8 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 // indirect - github.com/smarty/assertions v1.15.0 // indirect golang.org/x/crypto v0.22.0 // indirect golang.org/x/net v0.24.0 // indirect golang.org/x/sync v0.16.0 // indirect @@ -63,5 +61,4 @@ require ( gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 896ca7e2..a0aa16c3 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwt github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= +github.com/coreos/go-oidc/v3 v3.20.0 h1:EtE0WIBHk03N+DqGkY4+UONzzZHk7amKt6IyNd7OsZE= +github.com/coreos/go-oidc/v3 v3.20.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4= github.com/couchbase/go-couchbase v0.0.0-20181122212707-3e9b6e1258bb/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U= github.com/couchbase/gomemcached v0.0.0-20181122193126-5125a94a666c/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c= github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs= @@ -44,9 +46,12 @@ github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3C github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df h1:Bao6dhmbTA1KFVxmJ6nBoMuOJit2yjEgLJpIMYpop0E= github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= @@ -62,10 +67,13 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= -github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -92,12 +100,11 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -137,15 +144,12 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqn github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 h1:X+yvsM2yrEktyI+b2qND5gpH8YhURn0k8OCaeRnkINo= github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg= github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw= github.com/siddontang/ledisdb v0.0.0-20181029004158-becf5f38d373/go.mod h1:mF1DpOSOUiJRMR+FDqaqu3EBqrybQtrDDszLUZ6oxPg= github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA= -github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= -github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= -github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= -github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -197,6 +201,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= @@ -242,6 +248,7 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= diff --git a/main.go b/main.go index 66eb78fb..2e9e68b7 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "PrometheusAlert/models" _ "PrometheusAlert/routers" + "io" "os" "path" @@ -23,23 +24,53 @@ func IsExist(path string) bool { return err == nil || os.IsExist(err) } +func sqliteDBPath() string { + return beego.AppConfig.DefaultString("db_path", "./db/PrometheusAlertDB.db") +} + +func ensureSQLiteDB(dbName string) error { + if IsExist(dbName) { + return nil + } + + if err := os.MkdirAll(path.Dir(dbName), os.ModePerm); err != nil { + return err + } + + const seedDB = "./db/PrometheusAlertDB.db" + if dbName != seedDB && IsExist(seedDB) { + source, err := os.Open(seedDB) + if err != nil { + return err + } + defer source.Close() + + target, err := os.Create(dbName) + if err != nil { + return err + } + defer target.Close() + + _, err = io.Copy(target, source) + return err + } + + file, err := os.Create(dbName) + if err != nil { + return err + } + return file.Close() +} + func init() { dbDriver := beego.AppConfig.String("db_driver") switch dbDriver { case "sqlite3": // 检查数据库文件 - DbName := "./db/PrometheusAlertDB.db" - if !IsExist(DbName) { - err := os.MkdirAll(path.Dir(DbName), os.ModePerm) - if err != nil { - logs.Error(err) - return - } - _, err = os.Create(DbName) - if err != nil { - logs.Error(err) - return - } + DbName := sqliteDBPath() + if err := ensureSQLiteDB(DbName); err != nil { + logs.Error(err) + return } // 注册驱动(“sqlite3” 属于默认注册,此处代码可省略) err := orm.RegisterDriver("db_driver", orm.DRSqlite) @@ -83,18 +114,10 @@ func init() { orm.SetMaxOpenConns("default", 10) default: // 检查数据库文件 - Db_name := "./db/PrometheusAlertDB.db" - if !IsExist(Db_name) { - err := os.MkdirAll(path.Dir(Db_name), os.ModePerm) - if err != nil { - logs.Error(err) - return - } - _, err = os.Create(Db_name) - if err != nil { - logs.Error(err) - return - } + Db_name := sqliteDBPath() + if err := ensureSQLiteDB(Db_name); err != nil { + logs.Error(err) + return } // 注册驱动(“sqlite3” 属于默认注册,此处代码可省略) err := orm.RegisterDriver("db_driver", orm.DRSqlite) diff --git a/routers/router.go b/routers/router.go index 0534005c..e0d52061 100644 --- a/routers/router.go +++ b/routers/router.go @@ -10,6 +10,9 @@ func init() { //page beego.Router("/", &controllers.MainController{}) beego.Router("/login", &controllers.LoginController{}) + beego.Router("/sso/login", &controllers.LoginController{}, "get:SSOLogin") + beego.Router("/sso/callback", &controllers.LoginController{}, "get:SSOCallback") + beego.Router("/api/session", &controllers.LoginController{}, "get:Session") beego.Router("/template", &controllers.MainController{}, "get:Template") beego.Router("/template/add", &controllers.MainController{}, "get:TemplateAdd") diff --git a/views/login.html b/views/login.html index 0f74deb1..993a224c 100644 --- a/views/login.html +++ b/views/login.html @@ -11,6 +11,27 @@ +