mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-09 10:30:27 +01:00
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix(authn): include base path in SSO callback and error-redirect URLs The SAML ACS URL and the OIDC/Google redirect URLs were built from the site URL host plus a hardcoded path (e.g. /api/v1/complete/saml), dropping the base path. When SigNoz is served under a sub-path (global.external_url with a path, e.g. https://example.com/signoz), the API is served at <prefix>/api/v1/complete/<provider>, so the identity provider was told to call back to a path without the prefix and hit a 404. Thread global.Config into the SAML/OIDC/Google callback providers and the session handler, and prepend global.Config.ExternalPath() to the callback paths and the SSO error redirect to /login. Root deployments are unchanged since ExternalPath() returns "" without a configured sub-path. * fix(authn): run callbackauthn suite with base path * refactor(tests): self-contained base-path fixture for callbackauthn Move the base-path setup out of the shared create_signoz factory and into a package-scoped signoz fixture in the callbackauthn suite's own conftest (same pattern as rootuser/conftest.py). When --base-path is set the fixture appends SIGNOZ_GLOBAL_EXTERNAL__URL and the url-config prefix locally; without it it behaves exactly like the global fixture. The shared factory and docker config are left untouched. * test(authn): add base-path SSO integration suite Adds a dedicated `basepath` integration suite that serves SigNoz under a hardcoded /signoz prefix (SIGNOZ_GLOBAL_EXTERNAL__URL) and exercises the SAML and OIDC happy-path logins end-to-end. Every SigNoz API call is issued under the prefix and the IdP callback (ACS / redirect URI) is registered with the prefix, so the flow only passes when the backend builds prefixed callback URLs. The shared TestContainerUrlConfig and create_signoz factory are left untouched. The suite's conftest shadows the same-named auth fixtures (create_user_admin, get_token, get_session_context, apply_license) with base-path-aware variants and reuses the Keycloak/browser fixtures, which are not under the base path. Google SSO is not covered: it requires the real accounts.google.com issuer and a real Google login, so it cannot run against the local Keycloak IdP; it shares the identical path.Join(ExternalPath, redirectPath) callback logic that SAML and OIDC validate. * revert: drop in-place base-path wiring from integration harness Removes the --base-path flag, TestContainerUrlConfig.base_path, the idp.py and 02_saml.py .get() changes, and the callbackauthn base-path conftest fixture. Base-path SSO is now covered by the dedicated `basepath` suite, so the shared harness (TestContainerUrlConfig, create_signoz, callbackauthn) is back to its original root-only form. * refactor(test): remove apply_license fixture * refactor(test): extract base-path-aware auth factories Extract the session-context / token / token-pair / admin-registration logic in fixtures/auth.py into reusable factory functions that take an optional base_path (token_getter, session_context_getter, tokens_getter, register_admin), with the fixtures delegating to them. Default base_path="" is byte-identical for existing callers. The basepath suite's conftest now reuses these factories with the /signoz prefix as thin one-line fixture overrides instead of duplicating the request logic. * refactor(test): give base-path admin registration a distinct cache key register_admin takes an optional cache_key (default "create_user_admin"); the basepath suite passes a distinct key so that under --reuse the admin marker cached against the signoz-base-path container is not restored for (or from) other suites' default signoz instance.
174 lines
4.7 KiB
Go
174 lines
4.7 KiB
Go
package implsession
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"time"
|
|
|
|
"github.com/SigNoz/signoz/pkg/errors"
|
|
"github.com/SigNoz/signoz/pkg/global"
|
|
"github.com/SigNoz/signoz/pkg/http/binding"
|
|
"github.com/SigNoz/signoz/pkg/http/render"
|
|
"github.com/SigNoz/signoz/pkg/modules/session"
|
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
|
"github.com/SigNoz/signoz/pkg/valuer"
|
|
)
|
|
|
|
type handler struct {
|
|
module session.Module
|
|
globalConfig global.Config
|
|
}
|
|
|
|
func NewHandler(module session.Module, globalConfig global.Config) session.Handler {
|
|
return &handler{module: module, globalConfig: globalConfig}
|
|
}
|
|
|
|
func (handler *handler) GetSessionContext(rw http.ResponseWriter, req *http.Request) {
|
|
ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
email, err := valuer.NewEmail(req.URL.Query().Get("email"))
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
siteURL, err := url.Parse(req.URL.Query().Get("ref"))
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
sessionContext, err := handler.module.GetSessionContext(ctx, email, siteURL)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
render.Success(rw, http.StatusOK, sessionContext)
|
|
}
|
|
|
|
func (handler *handler) CreateSessionByEmailPassword(rw http.ResponseWriter, req *http.Request) {
|
|
ctx, cancel := context.WithTimeout(req.Context(), 15*time.Second)
|
|
defer cancel()
|
|
|
|
body := new(authtypes.PostableEmailPasswordSession)
|
|
if err := binding.JSON.BindBody(req.Body, body); err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
token, err := handler.module.CreatePasswordAuthNSession(ctx, authtypes.AuthNProviderEmailPassword, body.Email, body.Password, body.OrgID)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
render.Success(rw, http.StatusOK, authtypes.NewGettableTokenFromToken(token, handler.module.GetRotationInterval(ctx)))
|
|
}
|
|
|
|
func (handler *handler) CreateSessionByGoogleCallback(rw http.ResponseWriter, req *http.Request) {
|
|
ctx, cancel := context.WithTimeout(req.Context(), 15*time.Second)
|
|
defer cancel()
|
|
|
|
values := req.URL.Query()
|
|
|
|
redirectURL, err := handler.module.CreateCallbackAuthNSession(ctx, authtypes.AuthNProviderGoogleAuth, values)
|
|
if err != nil {
|
|
http.Redirect(rw, req, handler.getRedirectURLFromErr(err), http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
http.Redirect(rw, req, redirectURL, http.StatusSeeOther)
|
|
}
|
|
|
|
func (handler *handler) CreateSessionBySAMLCallback(rw http.ResponseWriter, req *http.Request) {
|
|
ctx, cancel := context.WithTimeout(req.Context(), 15*time.Second)
|
|
defer cancel()
|
|
|
|
err := req.ParseForm()
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
redirectURL, err := handler.module.CreateCallbackAuthNSession(ctx, authtypes.AuthNProviderSAML, req.Form)
|
|
if err != nil {
|
|
http.Redirect(rw, req, handler.getRedirectURLFromErr(err), http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
http.Redirect(rw, req, redirectURL, http.StatusSeeOther)
|
|
}
|
|
|
|
func (handler *handler) CreateSessionByOIDCCallback(rw http.ResponseWriter, req *http.Request) {
|
|
ctx, cancel := context.WithTimeout(req.Context(), 15*time.Second)
|
|
defer cancel()
|
|
|
|
values := req.URL.Query()
|
|
redirectURL, err := handler.module.CreateCallbackAuthNSession(ctx, authtypes.AuthNProviderOIDC, values)
|
|
if err != nil {
|
|
http.Redirect(rw, req, handler.getRedirectURLFromErr(err), http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
http.Redirect(rw, req, redirectURL, http.StatusSeeOther)
|
|
}
|
|
|
|
func (handler *handler) RotateSession(rw http.ResponseWriter, req *http.Request) {
|
|
ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
body := new(authtypes.PostableRotateToken)
|
|
if err := binding.JSON.BindBody(req.Body, body); err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
accessToken, err := authtypes.AccessTokenFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
token, err := handler.module.RotateSession(ctx, accessToken, body.RefreshToken)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
render.Success(rw, http.StatusOK, authtypes.NewGettableTokenFromToken(token, handler.module.GetRotationInterval(ctx)))
|
|
}
|
|
|
|
func (handler *handler) DeleteSession(rw http.ResponseWriter, req *http.Request) {
|
|
ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
accessToken, err := authtypes.AccessTokenFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
err = handler.module.DeleteSession(ctx, accessToken)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
render.Success(rw, http.StatusNoContent, nil)
|
|
}
|
|
|
|
func (handler *handler) getRedirectURLFromErr(err error) string {
|
|
values := errors.AsURLValues(err)
|
|
values.Add("callbackauthnerr", "true")
|
|
|
|
return (&url.URL{
|
|
// When UI is being served on a prefix, we need to redirect to the login page on the prefix.
|
|
Path: path.Join(handler.globalConfig.ExternalPath(), "/login"),
|
|
RawQuery: values.Encode(),
|
|
}).String()
|
|
}
|