mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-06-04 11:25:52 +08:00
chore(auth): refactor the auth modules and handler in preparation for multi tenant login (#7778)
* chore: update auth * chore: password changes * chore: make changes in oss code * chore: login * chore: get to a running state * fix: migration inital commit * fix: signoz cloud intgtn tests * fix: minor fixes * chore: sso code fixed with org domain * fix: tests * fix: ee auth api's * fix: changes in name * fix: return user in login api * fix: address comments * fix: validate password * fix: handle get domain by email properly * fix: move authomain to usermodule * fix: use displayname instead of hname * fix: rename back endpoints * fix: update telemetry * fix: correct errors * fix: test and fix the invite endpoints * fix: delete all things related to user in store * fix: address issues * fix: ee delete invite * fix: rename func * fix: update user and update role * fix: update role * fix: login and invite changes * fix: return org name in users response * fix: update user role * fix: nil check * fix: getinvite and update role * fix: sso * fix: getinvite use sso ctx * fix: use correct sourceurl * fix: getsourceurl from req payload * fix: update created_at * fix: fix reset password * fix: sso signup and token password change * fix: don't delete last admin * fix: reset password and migration * fix: migration * fix: reset password for sso users * fix: clean up invite * fix: migration * fix: update claims and store code * fix: use correct error * fix: proper nil checks * fix: make migration multitenant * fix: address comments * fix: minor fixes * fix: test * fix: rename reset password --------- Co-authored-by: Vikrant Gupta <vikrant@signoz.io>
This commit is contained in:
parent
16938c6cc0
commit
0a2b7ca1d8
@ -61,14 +61,14 @@ func (p *Pat) Wrap(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
role, err := authtypes.NewRole(user.Role)
|
||||
role, err := types.NewRole(user.Role)
|
||||
if err != nil {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
jwt := authtypes.Claims{
|
||||
UserID: user.ID,
|
||||
UserID: user.ID.String(),
|
||||
Role: role,
|
||||
Email: user.Email,
|
||||
OrgID: user.OrgID,
|
||||
|
201
ee/modules/user/impluser/handler.go
Normal file
201
ee/modules/user/impluser/handler.go
Normal file
@ -0,0 +1,201 @@
|
||||
package impluser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// EnterpriseHandler embeds the base handler implementation
|
||||
type Handler struct {
|
||||
user.Handler // Embed the base handler interface
|
||||
module user.Module
|
||||
}
|
||||
|
||||
func NewHandler(module user.Module) user.Handler {
|
||||
baseHandler := impluser.NewHandler(module)
|
||||
return &Handler{
|
||||
Handler: baseHandler,
|
||||
module: module,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var req types.PostableLoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// the EE handler wrapper passes the feature flag value in context
|
||||
ssoAvailable, ok := ctx.Value(types.SSOAvailable).(bool)
|
||||
if !ok {
|
||||
render.Error(w, errors.New(errors.TypeInternal, errors.CodeInternal, "failed to retrieve SSO availability"))
|
||||
return
|
||||
}
|
||||
|
||||
if ssoAvailable {
|
||||
_, err := h.module.CanUsePassword(ctx, req.Email)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
user, err := h.module.GetAuthenticatedUser(ctx, req.OrgID, req.Email, req.Password, req.RefreshToken)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
jwt, err := h.module.GetJWTForUser(ctx, user)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
gettableLoginResponse := &types.GettableLoginResponse{
|
||||
GettableUserJwt: jwt,
|
||||
UserID: user.ID.String(),
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, gettableLoginResponse)
|
||||
}
|
||||
|
||||
// Override only the methods you need with enterprise-specific implementations
|
||||
func (h *Handler) LoginPrecheck(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// assume user is valid unless proven otherwise and assign default values for rest of the fields
|
||||
|
||||
email := r.URL.Query().Get("email")
|
||||
sourceUrl := r.URL.Query().Get("ref")
|
||||
orgID := r.URL.Query().Get("orgID")
|
||||
|
||||
resp, err := h.module.LoginPrecheck(ctx, orgID, email, sourceUrl)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, resp)
|
||||
|
||||
}
|
||||
|
||||
func (h *Handler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req := new(types.PostableAcceptInvite)
|
||||
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
|
||||
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode user"))
|
||||
return
|
||||
}
|
||||
|
||||
// get invite object
|
||||
invite, err := h.module.GetInviteByToken(ctx, req.InviteToken)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgDomain, err := h.module.GetAuthDomainByEmail(ctx, invite.Email)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
precheckResp := &types.GettableLoginPrecheck{
|
||||
SSO: false,
|
||||
IsUser: false,
|
||||
}
|
||||
|
||||
if invite.Name == "" && req.DisplayName != "" {
|
||||
invite.Name = req.DisplayName
|
||||
}
|
||||
|
||||
user, err := types.NewUser(invite.Name, invite.Email, invite.Role, invite.OrgID)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if orgDomain != nil && orgDomain.SsoEnabled {
|
||||
// sso is enabled, create user and respond precheck data
|
||||
err = h.module.CreateUser(ctx, user)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// check if sso is enforced for the org
|
||||
precheckResp, err = h.module.LoginPrecheck(ctx, invite.OrgID, user.Email, req.SourceURL)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
password, err := types.NewFactorPassword(req.Password)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err = h.module.CreateUserWithPassword(ctx, user, password)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
precheckResp.IsUser = true
|
||||
}
|
||||
|
||||
// delete the invite
|
||||
if err := h.module.DeleteInvite(ctx, invite.OrgID, invite.ID); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, precheckResp)
|
||||
}
|
||||
|
||||
func (h *Handler) GetInvite(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
token := mux.Vars(r)["token"]
|
||||
sourceUrl := r.URL.Query().Get("ref")
|
||||
invite, err := h.module.GetInviteByToken(ctx, token)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// precheck the user
|
||||
precheckResp, err := h.module.LoginPrecheck(ctx, invite.OrgID, invite.Email, sourceUrl)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
gettableInvite := &types.GettableEEInvite{
|
||||
GettableInvite: *invite,
|
||||
PreCheck: precheckResp,
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, gettableInvite)
|
||||
return
|
||||
}
|
229
ee/modules/user/impluser/module.go
Normal file
229
ee/modules/user/impluser/module.go
Normal file
@ -0,0 +1,229 @@
|
||||
package impluser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
baseimpl "github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// EnterpriseModule embeds the base module implementation
|
||||
type Module struct {
|
||||
*baseimpl.Module // Embed the base module implementation
|
||||
store types.UserStore
|
||||
}
|
||||
|
||||
func NewModule(store types.UserStore) user.Module {
|
||||
baseModule := baseimpl.NewModule(store).(*baseimpl.Module)
|
||||
return &Module{
|
||||
Module: baseModule,
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Module) createUserForSAMLRequest(ctx context.Context, email string) (*types.User, error) {
|
||||
// get auth domain from email domain
|
||||
_, err := m.GetAuthDomainByEmail(ctx, email)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get name from email
|
||||
parts := strings.Split(email, "@")
|
||||
if len(parts) < 2 {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid email format")
|
||||
}
|
||||
name := parts[0]
|
||||
|
||||
defaultOrgID, err := m.store.GetDefaultOrgID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := types.NewUser(name, email, types.RoleViewer.String(), defaultOrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = m.CreateUser(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (m *Module) PrepareSsoRedirect(ctx context.Context, redirectUri, email string, jwt *authtypes.JWT) (string, error) {
|
||||
users, err := m.GetUsersByEmail(ctx, email)
|
||||
if err != nil {
|
||||
zap.L().Error("failed to get user with email received from auth provider", zap.String("error", err.Error()))
|
||||
return "", err
|
||||
}
|
||||
user := &types.User{}
|
||||
|
||||
if len(users) == 0 {
|
||||
newUser, err := m.createUserForSAMLRequest(ctx, email)
|
||||
user = newUser
|
||||
if err != nil {
|
||||
zap.L().Error("failed to create user with email received from auth provider", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
user = &users[0].User
|
||||
}
|
||||
|
||||
tokenStore, err := m.GetJWTForUser(ctx, user)
|
||||
if err != nil {
|
||||
zap.L().Error("failed to generate token for SSO login user", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s?jwt=%s&usr=%s&refreshjwt=%s",
|
||||
redirectUri,
|
||||
tokenStore.AccessJwt,
|
||||
user.ID,
|
||||
tokenStore.RefreshJwt), nil
|
||||
}
|
||||
|
||||
func (m *Module) CanUsePassword(ctx context.Context, email string) (bool, error) {
|
||||
domain, err := m.GetAuthDomainByEmail(ctx, email)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if domain != nil && domain.SsoEnabled {
|
||||
// sso is enabled, check if the user has admin role
|
||||
users, err := m.GetUsersByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
return false, errors.New(errors.TypeNotFound, errors.CodeNotFound, "user not found")
|
||||
}
|
||||
|
||||
if users[0].Role != types.RoleAdmin.String() {
|
||||
return false, errors.New(errors.TypeForbidden, errors.CodeForbidden, "auth method not supported")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (m *Module) LoginPrecheck(ctx context.Context, orgID, email, sourceUrl string) (*types.GettableLoginPrecheck, error) {
|
||||
resp := &types.GettableLoginPrecheck{IsUser: true, CanSelfRegister: false}
|
||||
|
||||
// check if email is a valid user
|
||||
users, err := m.GetUsersByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
resp.IsUser = false
|
||||
}
|
||||
|
||||
// give them an option to select an org
|
||||
if orgID == "" && len(users) > 1 {
|
||||
resp.SelectOrg = true
|
||||
resp.Orgs = make([]string, len(users))
|
||||
for i, user := range users {
|
||||
resp.Orgs[i] = user.OrgID
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// select the user with the corresponding orgID
|
||||
if len(users) > 1 {
|
||||
found := false
|
||||
for _, tuser := range users {
|
||||
if tuser.OrgID == orgID {
|
||||
// user = tuser
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
resp.IsUser = false
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
// the EE handler wrapper passes the feature flag value in context
|
||||
ssoAvailable, ok := ctx.Value(types.SSOAvailable).(bool)
|
||||
if !ok {
|
||||
zap.L().Error("failed to retrieve ssoAvailable from context")
|
||||
return nil, errors.New(errors.TypeInternal, errors.CodeInternal, "failed to retrieve SSO availability")
|
||||
}
|
||||
|
||||
if ssoAvailable {
|
||||
|
||||
// TODO(Nitya): in multitenancy this should use orgId as well.
|
||||
orgDomain, err := m.GetAuthDomainByEmail(ctx, email)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if orgDomain != nil && orgDomain.SsoEnabled {
|
||||
// this is to allow self registration
|
||||
resp.IsUser = true
|
||||
|
||||
// saml is enabled for this domain, lets prepare sso url
|
||||
if sourceUrl == "" {
|
||||
sourceUrl = constants.GetDefaultSiteURL()
|
||||
}
|
||||
|
||||
// parse source url that generated the login request
|
||||
var err error
|
||||
escapedUrl, _ := url.QueryUnescape(sourceUrl)
|
||||
siteUrl, err := url.Parse(escapedUrl)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse referer")
|
||||
}
|
||||
|
||||
// build Idp URL that will authenticat the user
|
||||
// the front-end will redirect user to this url
|
||||
resp.SSOUrl, err = orgDomain.BuildSsoUrl(siteUrl)
|
||||
if err != nil {
|
||||
zap.L().Error("failed to prepare saml request for domain", zap.String("domain", orgDomain.Name), zap.Error(err))
|
||||
return nil, errors.New(errors.TypeInternal, errors.CodeInternal, "failed to prepare saml request for domain")
|
||||
}
|
||||
|
||||
// set SSO to true, as the url is generated correctly
|
||||
resp.SSO = true
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *Module) GetAuthDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, error) {
|
||||
|
||||
if email == "" {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "email is required")
|
||||
}
|
||||
|
||||
components := strings.Split(email, "@")
|
||||
if len(components) < 2 {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid email format")
|
||||
}
|
||||
|
||||
domain, err := m.store.GetDomainByName(ctx, components[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gettableDomain := &types.GettableOrgDomain{StorableOrgDomain: *domain}
|
||||
if err := gettableDomain.LoadConfig(domain.Data); err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to load domain config")
|
||||
}
|
||||
return gettableDomain, nil
|
||||
}
|
37
ee/modules/user/impluser/store.go
Normal file
37
ee/modules/user/impluser/store.go
Normal file
@ -0,0 +1,37 @@
|
||||
package impluser
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
baseimpl "github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
)
|
||||
|
||||
type store struct {
|
||||
*baseimpl.Store
|
||||
sqlstore sqlstore.SQLStore
|
||||
}
|
||||
|
||||
func NewStore(sqlstore sqlstore.SQLStore) types.UserStore {
|
||||
baseStore := baseimpl.NewStore(sqlstore).(*baseimpl.Store)
|
||||
return &store{
|
||||
Store: baseStore,
|
||||
sqlstore: sqlstore,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *store) GetDomainByName(ctx context.Context, name string) (*types.StorableOrgDomain, error) {
|
||||
domain := new(types.StorableOrgDomain)
|
||||
err := s.sqlstore.BunDB().NewSelect().
|
||||
Model(domain).
|
||||
Where("name = ?", name).
|
||||
Limit(1).
|
||||
Scan(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeNotFound, errors.CodeNotFound, "failed to get domain from name")
|
||||
}
|
||||
return domain, nil
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"time"
|
||||
@ -9,10 +10,13 @@ import (
|
||||
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
|
||||
"github.com/SigNoz/signoz/ee/query-service/interfaces"
|
||||
"github.com/SigNoz/signoz/ee/query-service/license"
|
||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
||||
"github.com/SigNoz/signoz/ee/query-service/usage"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/apis/fields"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||
quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
|
||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
@ -23,9 +27,11 @@ import (
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
rules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
"github.com/gorilla/mux"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type APIHandlerOptions struct {
|
||||
@ -120,43 +126,24 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
|
||||
// routes available only in ee version
|
||||
|
||||
router.HandleFunc("/api/v1/featureFlags",
|
||||
am.OpenAccess(ah.getFeatureFlags)).
|
||||
Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/loginPrecheck", am.OpenAccess(ah.loginPrecheck)).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/loginPrecheck",
|
||||
am.OpenAccess(ah.precheckLogin)).
|
||||
Methods(http.MethodGet)
|
||||
// invite
|
||||
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(ah.getInvite)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/invite/accept", am.OpenAccess(ah.acceptInvite)).Methods(http.MethodPost)
|
||||
|
||||
// paid plans specific routes
|
||||
router.HandleFunc("/api/v1/complete/saml",
|
||||
am.OpenAccess(ah.receiveSAML)).
|
||||
Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/complete/saml", am.OpenAccess(ah.receiveSAML)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/complete/google", am.OpenAccess(ah.receiveGoogleAuth)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/orgs/{orgId}/domains", am.AdminAccess(ah.listDomainsByOrg)).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/complete/google",
|
||||
am.OpenAccess(ah.receiveGoogleAuth)).
|
||||
Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/orgs/{orgId}/domains",
|
||||
am.AdminAccess(ah.listDomainsByOrg)).
|
||||
Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/domains",
|
||||
am.AdminAccess(ah.postDomain)).
|
||||
Methods(http.MethodPost)
|
||||
|
||||
router.HandleFunc("/api/v1/domains/{id}",
|
||||
am.AdminAccess(ah.putDomain)).
|
||||
Methods(http.MethodPut)
|
||||
|
||||
router.HandleFunc("/api/v1/domains/{id}",
|
||||
am.AdminAccess(ah.deleteDomain)).
|
||||
Methods(http.MethodDelete)
|
||||
router.HandleFunc("/api/v1/domains", am.AdminAccess(ah.postDomain)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/domains/{id}", am.AdminAccess(ah.putDomain)).Methods(http.MethodPut)
|
||||
router.HandleFunc("/api/v1/domains/{id}", am.AdminAccess(ah.deleteDomain)).Methods(http.MethodDelete)
|
||||
|
||||
// base overrides
|
||||
router.HandleFunc("/api/v1/version", am.OpenAccess(ah.getVersion)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(ah.getInvite)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/register", am.OpenAccess(ah.registerUser)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/login", am.OpenAccess(ah.loginUser)).Methods(http.MethodPost)
|
||||
|
||||
// PAT APIs
|
||||
@ -188,6 +175,54 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
|
||||
}
|
||||
|
||||
// TODO(nitya): remove this once we know how to get the FF's
|
||||
func (ah *APIHandler) updateRequestContext(w http.ResponseWriter, r *http.Request) (*http.Request, error) {
|
||||
ssoAvailable := true
|
||||
err := ah.FF().CheckFeature(model.SSO)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case basemodel.ErrFeatureUnavailable:
|
||||
// do nothing, just skip sso
|
||||
ssoAvailable = false
|
||||
default:
|
||||
zap.L().Error("feature check failed", zap.String("featureKey", model.SSO), zap.Error(err))
|
||||
return r, errors.New(errors.TypeInternal, errors.CodeInternal, "error checking SSO feature")
|
||||
}
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), types.SSOAvailable, ssoAvailable)
|
||||
return r.WithContext(ctx), nil
|
||||
}
|
||||
|
||||
func (ah *APIHandler) loginPrecheck(w http.ResponseWriter, r *http.Request) {
|
||||
r, err := ah.updateRequestContext(w, r)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
ah.Signoz.Handlers.User.LoginPrecheck(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
func (ah *APIHandler) acceptInvite(w http.ResponseWriter, r *http.Request) {
|
||||
r, err := ah.updateRequestContext(w, r)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
ah.Signoz.Handlers.User.AcceptInvite(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getInvite(w http.ResponseWriter, r *http.Request) {
|
||||
r, err := ah.updateRequestContext(w, r)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
ah.Signoz.Handlers.User.GetInvite(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
func (ah *APIHandler) RegisterCloudIntegrationsRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
|
||||
ah.APIHandler.RegisterCloudIntegrationsRoutes(router, am)
|
||||
|
@ -9,13 +9,11 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
||||
baseauth "github.com/SigNoz/signoz/pkg/query-service/auth"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
)
|
||||
|
||||
func parseRequest(r *http.Request, req interface{}) error {
|
||||
@ -31,161 +29,13 @@ func parseRequest(r *http.Request, req interface{}) error {
|
||||
|
||||
// loginUser overrides base handler and considers SSO case.
|
||||
func (ah *APIHandler) loginUser(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
req := basemodel.LoginRequest{}
|
||||
err := parseRequest(r, &req)
|
||||
r, err := ah.updateRequestContext(w, r)
|
||||
if err != nil {
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if req.Email != "" && ah.CheckFeature(model.SSO) {
|
||||
var apierr basemodel.BaseApiError
|
||||
_, apierr = ah.AppDao().CanUsePassword(ctx, req.Email)
|
||||
if apierr != nil && !apierr.IsNil() {
|
||||
RespondError(w, apierr, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// if all looks good, call auth
|
||||
resp, err := baseauth.Login(ctx, &req, ah.opts.JWT)
|
||||
if ah.HandleError(w, err, http.StatusUnauthorized) {
|
||||
return
|
||||
}
|
||||
|
||||
ah.WriteJSON(w, r, resp)
|
||||
}
|
||||
|
||||
// registerUser registers a user and responds with a precheck
|
||||
// so the front-end can decide the login method
|
||||
func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if !ah.CheckFeature(model.SSO) {
|
||||
ah.APIHandler.Register(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
var req *baseauth.RegisterRequest
|
||||
|
||||
defer r.Body.Close()
|
||||
requestBody, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
zap.L().Error("received no input in api", zap.Error(err))
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(requestBody, &req)
|
||||
|
||||
if err != nil {
|
||||
zap.L().Error("received invalid user registration request", zap.Error(err))
|
||||
RespondError(w, model.BadRequest(fmt.Errorf("failed to register user")), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// get invite object
|
||||
invite, err := baseauth.ValidateInvite(ctx, req)
|
||||
if err != nil {
|
||||
zap.L().Error("failed to validate invite token", zap.Error(err))
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if invite == nil {
|
||||
zap.L().Error("failed to validate invite token: it is either empty or invalid", zap.Error(err))
|
||||
RespondError(w, model.BadRequest(basemodel.ErrSignupFailed{}), nil)
|
||||
return
|
||||
}
|
||||
|
||||
// get auth domain from email domain
|
||||
domain, apierr := ah.AppDao().GetDomainByEmail(ctx, invite.Email)
|
||||
if apierr != nil {
|
||||
zap.L().Error("failed to get domain from email", zap.Error(apierr))
|
||||
RespondError(w, model.InternalError(basemodel.ErrSignupFailed{}), nil)
|
||||
}
|
||||
|
||||
precheckResp := &basemodel.PrecheckResponse{
|
||||
SSO: false,
|
||||
IsUser: false,
|
||||
}
|
||||
|
||||
if domain != nil && domain.SsoEnabled {
|
||||
// sso is enabled, create user and respond precheck data
|
||||
user, apierr := baseauth.RegisterInvitedUser(ctx, req, true)
|
||||
if apierr != nil {
|
||||
RespondError(w, apierr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var precheckError basemodel.BaseApiError
|
||||
|
||||
precheckResp, precheckError = ah.AppDao().PrecheckLogin(ctx, user.Email, req.SourceUrl)
|
||||
if precheckError != nil {
|
||||
RespondError(w, precheckError, precheckResp)
|
||||
}
|
||||
|
||||
} else {
|
||||
// no-sso, validate password
|
||||
if err := baseauth.ValidatePassword(req.Password); err != nil {
|
||||
RespondError(w, model.InternalError(fmt.Errorf("password is not in a valid format")), nil)
|
||||
return
|
||||
}
|
||||
|
||||
_, registerError := baseauth.Register(ctx, req, ah.Signoz.Alertmanager, ah.Signoz.Modules.Organization, ah.QuickFilterModule)
|
||||
if !registerError.IsNil() {
|
||||
RespondError(w, apierr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
precheckResp.IsUser = true
|
||||
}
|
||||
|
||||
ah.Respond(w, precheckResp)
|
||||
}
|
||||
|
||||
// getInvite returns the invite object details for the given invite token. We do not need to
|
||||
// protect this API because invite token itself is meant to be private.
|
||||
func (ah *APIHandler) getInvite(w http.ResponseWriter, r *http.Request) {
|
||||
token := mux.Vars(r)["token"]
|
||||
sourceUrl := r.URL.Query().Get("ref")
|
||||
|
||||
inviteObject, err := baseauth.GetInvite(r.Context(), token, ah.Signoz.Modules.Organization)
|
||||
if err != nil {
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
resp := model.GettableInvitation{
|
||||
InvitationResponseObject: inviteObject,
|
||||
}
|
||||
|
||||
precheck, apierr := ah.AppDao().PrecheckLogin(r.Context(), inviteObject.Email, sourceUrl)
|
||||
resp.Precheck = precheck
|
||||
|
||||
if apierr != nil {
|
||||
RespondError(w, apierr, resp)
|
||||
}
|
||||
|
||||
ah.WriteJSON(w, r, resp)
|
||||
}
|
||||
|
||||
// PrecheckLogin enables browser login page to display appropriate
|
||||
// login methods
|
||||
func (ah *APIHandler) precheckLogin(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background()
|
||||
|
||||
email := r.URL.Query().Get("email")
|
||||
sourceUrl := r.URL.Query().Get("ref")
|
||||
|
||||
resp, apierr := ah.AppDao().PrecheckLogin(ctx, email, sourceUrl)
|
||||
if apierr != nil {
|
||||
RespondError(w, apierr, resp)
|
||||
}
|
||||
|
||||
ah.Respond(w, resp)
|
||||
ah.Signoz.Handlers.User.Login(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
func handleSsoError(w http.ResponseWriter, r *http.Request, redirectURL string) {
|
||||
@ -252,7 +102,7 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, identity.Email, ah.opts.JWT)
|
||||
nextPage, err := ah.Signoz.Modules.User.PrepareSsoRedirect(ctx, redirectUri, identity.Email, ah.opts.JWT)
|
||||
if err != nil {
|
||||
zap.L().Error("[receiveGoogleAuth] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
|
||||
handleSsoError(w, r, redirectUri)
|
||||
@ -330,7 +180,7 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, email, ah.opts.JWT)
|
||||
nextPage, err := ah.Signoz.Modules.User.PrepareSsoRedirect(ctx, redirectUri, email, ah.opts.JWT)
|
||||
if err != nil {
|
||||
zap.L().Error("[receiveSAML] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
|
||||
handleSsoError(w, r, redirectUri)
|
||||
|
@ -12,8 +12,8 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||
eeTypes "github.com/SigNoz/signoz/ee/types"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/auth"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
@ -123,7 +123,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
|
||||
))
|
||||
}
|
||||
for _, p := range allPats {
|
||||
if p.UserID == integrationUser.ID && p.Name == integrationPATName {
|
||||
if p.UserID == integrationUser.ID.String() && p.Name == integrationPATName {
|
||||
return p.Token, nil
|
||||
}
|
||||
}
|
||||
@ -135,8 +135,8 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
|
||||
|
||||
newPAT := eeTypes.NewGettablePAT(
|
||||
integrationPATName,
|
||||
authtypes.RoleViewer.String(),
|
||||
integrationUser.ID,
|
||||
types.RoleViewer.String(),
|
||||
integrationUser.ID.String(),
|
||||
0,
|
||||
)
|
||||
integrationPAT, err := ah.AppDao().CreatePAT(ctx, orgId, newPAT)
|
||||
@ -154,10 +154,9 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
||||
cloudIntegrationUser := fmt.Sprintf("%s-integration", cloudProvider)
|
||||
email := fmt.Sprintf("%s@signoz.io", cloudIntegrationUser)
|
||||
|
||||
// TODO(nitya): there should be orgId here
|
||||
integrationUserResult, apiErr := ah.AppDao().GetUserByEmail(ctx, email)
|
||||
if apiErr != nil {
|
||||
return nil, basemodel.WrapApiError(apiErr, "couldn't look for integration user")
|
||||
integrationUserResult, err := ah.Signoz.Modules.User.GetUserByEmailInOrg(ctx, orgId, email)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil, basemodel.NotFoundError(fmt.Errorf("couldn't look for integration user: %w", err))
|
||||
}
|
||||
|
||||
if integrationUserResult != nil {
|
||||
@ -169,29 +168,18 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
||||
zap.String("cloudProvider", cloudProvider),
|
||||
)
|
||||
|
||||
newUser := &types.User{
|
||||
ID: uuid.New().String(),
|
||||
Name: cloudIntegrationUser,
|
||||
Email: email,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
OrgID: orgId,
|
||||
}
|
||||
|
||||
newUser.Role = authtypes.RoleViewer.String()
|
||||
|
||||
passwordHash, err := auth.PasswordHash(uuid.NewString())
|
||||
newUser, err := types.NewUser(cloudIntegrationUser, email, types.RoleViewer.String(), orgId)
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf(
|
||||
"couldn't hash random password for cloud integration user: %w", err,
|
||||
"couldn't create cloud integration user: %w", err,
|
||||
))
|
||||
}
|
||||
newUser.Password = passwordHash
|
||||
|
||||
integrationUser, apiErr := ah.AppDao().CreateUser(ctx, newUser, false)
|
||||
if apiErr != nil {
|
||||
return nil, basemodel.WrapApiError(apiErr, "couldn't create cloud integration user")
|
||||
password, err := types.NewFactorPassword(uuid.NewString())
|
||||
|
||||
integrationUser, err := ah.Signoz.Modules.User.CreateUserWithPassword(ctx, newUser, password)
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
|
||||
}
|
||||
|
||||
return integrationUser, nil
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
||||
"github.com/SigNoz/signoz/ee/types"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
@ -56,7 +56,7 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func validatePATRequest(req eeTypes.GettablePAT) error {
|
||||
_, err := authtypes.NewRole(req.Role)
|
||||
_, err := types.NewRole(req.Role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -93,16 +93,16 @@ func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
//get the pat
|
||||
existingPAT, paterr := ah.AppDao().GetPATByID(r.Context(), claims.OrgID, id)
|
||||
if paterr != nil {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, paterr.Error()))
|
||||
existingPAT, err := ah.AppDao().GetPATByID(r.Context(), claims.OrgID, id)
|
||||
if err != nil {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// get the user
|
||||
createdByUser, usererr := ah.AppDao().GetUser(r.Context(), existingPAT.UserID)
|
||||
if usererr != nil {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, usererr.Error()))
|
||||
createdByUser, err := ah.Signoz.Modules.User.GetUserByID(r.Context(), claims.OrgID, existingPAT.UserID)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -119,7 +119,6 @@ func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
req.UpdatedByUserID = claims.UserID
|
||||
req.UpdatedAt = time.Now()
|
||||
zap.L().Info("Got Update PAT request", zap.Any("pat", req))
|
||||
var apierr basemodel.BaseApiError
|
||||
if apierr = ah.AppDao().UpdatePAT(r.Context(), claims.OrgID, req, id); apierr != nil {
|
||||
RespondError(w, apierr, nil)
|
||||
@ -167,9 +166,9 @@ func (ah *APIHandler) revokePAT(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// get the user
|
||||
createdByUser, usererr := ah.AppDao().GetUser(r.Context(), existingPAT.UserID)
|
||||
if usererr != nil {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, usererr.Error()))
|
||||
createdByUser, err := ah.Signoz.Modules.User.GetUserByID(r.Context(), claims.OrgID, existingPAT.UserID)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -33,3 +33,7 @@ func NewDataConnector(
|
||||
ClickHouseReader: chReader,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ClickhouseReader) GetSQLStore() sqlstore.SQLStore {
|
||||
return r.appdb
|
||||
}
|
||||
|
@ -195,6 +195,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
}
|
||||
|
||||
telemetry.GetInstance().SetReader(reader)
|
||||
telemetry.GetInstance().SetSqlStore(serverOptions.SigNoz.SQLStore)
|
||||
telemetry.GetInstance().SetSaasOperator(constants.SaasSegmentKey)
|
||||
|
||||
fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval)
|
||||
|
@ -4,11 +4,11 @@ import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/types"
|
||||
eeTypes "github.com/SigNoz/signoz/ee/types"
|
||||
basedao "github.com/SigNoz/signoz/pkg/query-service/dao"
|
||||
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/google/uuid"
|
||||
"github.com/uptrace/bun"
|
||||
@ -23,8 +23,6 @@ type ModelDao interface {
|
||||
DB() *bun.DB
|
||||
|
||||
// auth methods
|
||||
CanUsePassword(ctx context.Context, email string) (bool, basemodel.BaseApiError)
|
||||
PrepareSsoRedirect(ctx context.Context, redirectUri, email string, jwt *authtypes.JWT) (redirectURL string, apierr basemodel.BaseApiError)
|
||||
GetDomainFromSsoResponse(ctx context.Context, relayState *url.URL) (*types.GettableOrgDomain, error)
|
||||
|
||||
// org domain (auth domains) CRUD ops
|
||||
@ -35,10 +33,10 @@ type ModelDao interface {
|
||||
DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.BaseApiError
|
||||
GetDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, basemodel.BaseApiError)
|
||||
|
||||
CreatePAT(ctx context.Context, orgID string, p types.GettablePAT) (types.GettablePAT, basemodel.BaseApiError)
|
||||
UpdatePAT(ctx context.Context, orgID string, p types.GettablePAT, id valuer.UUID) basemodel.BaseApiError
|
||||
GetPAT(ctx context.Context, pat string) (*types.GettablePAT, basemodel.BaseApiError)
|
||||
GetPATByID(ctx context.Context, orgID string, id valuer.UUID) (*types.GettablePAT, basemodel.BaseApiError)
|
||||
ListPATs(ctx context.Context, orgID string) ([]types.GettablePAT, basemodel.BaseApiError)
|
||||
CreatePAT(ctx context.Context, orgID string, p eeTypes.GettablePAT) (eeTypes.GettablePAT, basemodel.BaseApiError)
|
||||
UpdatePAT(ctx context.Context, orgID string, p eeTypes.GettablePAT, id valuer.UUID) basemodel.BaseApiError
|
||||
GetPAT(ctx context.Context, pat string) (*eeTypes.GettablePAT, basemodel.BaseApiError)
|
||||
GetPATByID(ctx context.Context, orgID string, id valuer.UUID) (*eeTypes.GettablePAT, basemodel.BaseApiError)
|
||||
ListPATs(ctx context.Context, orgID string) ([]eeTypes.GettablePAT, basemodel.BaseApiError)
|
||||
RevokePAT(ctx context.Context, orgID string, id valuer.UUID, userID string) basemodel.BaseApiError
|
||||
}
|
||||
|
@ -1,191 +0,0 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
||||
baseauth "github.com/SigNoz/signoz/pkg/query-service/auth"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (*types.User, basemodel.BaseApiError) {
|
||||
// get auth domain from email domain
|
||||
domain, apierr := m.GetDomainByEmail(ctx, email)
|
||||
if apierr != nil {
|
||||
zap.L().Error("failed to get domain from email", zap.Error(apierr))
|
||||
return nil, model.InternalErrorStr("failed to get domain from email")
|
||||
}
|
||||
if domain == nil {
|
||||
zap.L().Error("email domain does not match any authenticated domain", zap.String("email", email))
|
||||
return nil, model.InternalErrorStr("email domain does not match any authenticated domain")
|
||||
}
|
||||
|
||||
hash, err := baseauth.PasswordHash(utils.GeneratePassowrd())
|
||||
if err != nil {
|
||||
zap.L().Error("failed to generate password hash when registering a user via SSO redirect", zap.Error(err))
|
||||
return nil, model.InternalErrorStr("failed to generate password hash")
|
||||
}
|
||||
|
||||
user := &types.User{
|
||||
ID: uuid.New().String(),
|
||||
Name: "",
|
||||
Email: email,
|
||||
Password: hash,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
ProfilePictureURL: "", // Currently unused
|
||||
Role: authtypes.RoleViewer.String(),
|
||||
OrgID: domain.OrgID,
|
||||
}
|
||||
|
||||
user, apiErr := m.CreateUser(ctx, user, false)
|
||||
if apiErr != nil {
|
||||
zap.L().Error("CreateUser failed", zap.Error(apiErr))
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
return user, nil
|
||||
|
||||
}
|
||||
|
||||
// PrepareSsoRedirect prepares redirect page link after SSO response
|
||||
// is successfully parsed (i.e. valid email is available)
|
||||
func (m *modelDao) PrepareSsoRedirect(ctx context.Context, redirectUri, email string, jwt *authtypes.JWT) (redirectURL string, apierr basemodel.BaseApiError) {
|
||||
|
||||
userPayload, apierr := m.GetUserByEmail(ctx, email)
|
||||
if !apierr.IsNil() {
|
||||
zap.L().Error("failed to get user with email received from auth provider", zap.String("error", apierr.Error()))
|
||||
return "", model.BadRequestStr("invalid user email received from the auth provider")
|
||||
}
|
||||
|
||||
user := &types.User{}
|
||||
|
||||
if userPayload == nil {
|
||||
newUser, apiErr := m.createUserForSAMLRequest(ctx, email)
|
||||
user = newUser
|
||||
if apiErr != nil {
|
||||
zap.L().Error("failed to create user with email received from auth provider", zap.Error(apiErr))
|
||||
return "", apiErr
|
||||
}
|
||||
} else {
|
||||
user = &userPayload.User
|
||||
}
|
||||
|
||||
tokenStore, err := baseauth.GenerateJWTForUser(user, jwt)
|
||||
if err != nil {
|
||||
zap.L().Error("failed to generate token for SSO login user", zap.Error(err))
|
||||
return "", model.InternalErrorStr("failed to generate token for the user")
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s?jwt=%s&usr=%s&refreshjwt=%s",
|
||||
redirectUri,
|
||||
tokenStore.AccessJwt,
|
||||
user.ID,
|
||||
tokenStore.RefreshJwt), nil
|
||||
}
|
||||
|
||||
func (m *modelDao) CanUsePassword(ctx context.Context, email string) (bool, basemodel.BaseApiError) {
|
||||
domain, apierr := m.GetDomainByEmail(ctx, email)
|
||||
if apierr != nil {
|
||||
return false, apierr
|
||||
}
|
||||
|
||||
if domain != nil && domain.SsoEnabled {
|
||||
// sso is enabled, check if the user has admin role
|
||||
userPayload, baseapierr := m.GetUserByEmail(ctx, email)
|
||||
|
||||
if baseapierr != nil || userPayload == nil {
|
||||
return false, baseapierr
|
||||
}
|
||||
|
||||
if userPayload.Role != authtypes.RoleAdmin.String() {
|
||||
return false, model.BadRequest(fmt.Errorf("auth method not supported"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// PrecheckLogin is called when the login or signup page is loaded
|
||||
// to check sso login is to be prompted
|
||||
func (m *modelDao) PrecheckLogin(ctx context.Context, email, sourceUrl string) (*basemodel.PrecheckResponse, basemodel.BaseApiError) {
|
||||
|
||||
// assume user is valid unless proven otherwise
|
||||
resp := &basemodel.PrecheckResponse{IsUser: true, CanSelfRegister: false}
|
||||
|
||||
// check if email is a valid user
|
||||
userPayload, baseApiErr := m.GetUserByEmail(ctx, email)
|
||||
if baseApiErr != nil {
|
||||
return resp, baseApiErr
|
||||
}
|
||||
|
||||
if userPayload == nil {
|
||||
resp.IsUser = false
|
||||
}
|
||||
|
||||
ssoAvailable := true
|
||||
err := m.checkFeature(model.SSO)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case basemodel.ErrFeatureUnavailable:
|
||||
// do nothing, just skip sso
|
||||
ssoAvailable = false
|
||||
default:
|
||||
zap.L().Error("feature check failed", zap.String("featureKey", model.SSO), zap.Error(err))
|
||||
return resp, model.BadRequestStr(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if ssoAvailable {
|
||||
|
||||
resp.IsUser = true
|
||||
|
||||
// find domain from email
|
||||
orgDomain, apierr := m.GetDomainByEmail(ctx, email)
|
||||
if apierr != nil {
|
||||
zap.L().Error("failed to get org domain from email", zap.String("email", email), zap.Error(apierr.ToError()))
|
||||
return resp, apierr
|
||||
}
|
||||
|
||||
if orgDomain != nil && orgDomain.SsoEnabled {
|
||||
// saml is enabled for this domain, lets prepare sso url
|
||||
|
||||
if sourceUrl == "" {
|
||||
sourceUrl = constants.GetDefaultSiteURL()
|
||||
}
|
||||
|
||||
// parse source url that generated the login request
|
||||
var err error
|
||||
escapedUrl, _ := url.QueryUnescape(sourceUrl)
|
||||
siteUrl, err := url.Parse(escapedUrl)
|
||||
if err != nil {
|
||||
zap.L().Error("failed to parse referer", zap.Error(err))
|
||||
return resp, model.InternalError(fmt.Errorf("failed to generate login request"))
|
||||
}
|
||||
|
||||
// build Idp URL that will authenticat the user
|
||||
// the front-end will redirect user to this url
|
||||
resp.SsoUrl, err = orgDomain.BuildSsoUrl(siteUrl)
|
||||
|
||||
if err != nil {
|
||||
zap.L().Error("failed to prepare saml request for domain", zap.String("domain", orgDomain.Name), zap.Error(err))
|
||||
return resp, model.InternalError(err)
|
||||
}
|
||||
|
||||
// set SSO to true, as the url is generated correctly
|
||||
resp.SSO = true
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
@ -10,8 +10,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
||||
"github.com/SigNoz/signoz/ee/types"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
ossTypes "github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
@ -44,7 +44,7 @@ func (m *modelDao) GetDomainFromSsoResponse(ctx context.Context, relayState *url
|
||||
}
|
||||
|
||||
domain, err = m.GetDomain(ctx, domainId)
|
||||
if (err != nil) || domain == nil {
|
||||
if err != nil {
|
||||
zap.L().Error("failed to find domain from domainId received in IdP response", zap.Error(err))
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
}
|
||||
@ -54,7 +54,7 @@ func (m *modelDao) GetDomainFromSsoResponse(ctx context.Context, relayState *url
|
||||
|
||||
domainFromDB, err := m.GetDomainByName(ctx, domainNameStr)
|
||||
domain = domainFromDB
|
||||
if (err != nil) || domain == nil {
|
||||
if err != nil {
|
||||
zap.L().Error("failed to find domain from domainName received in IdP response", zap.Error(err))
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package sqlite
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
basedao "github.com/SigNoz/signoz/pkg/query-service/dao"
|
||||
basedsql "github.com/SigNoz/signoz/pkg/query-service/dao/sqlite"
|
||||
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
@ -12,7 +14,8 @@ import (
|
||||
|
||||
type modelDao struct {
|
||||
*basedsql.ModelDaoSqlite
|
||||
flags baseint.FeatureLookup
|
||||
flags baseint.FeatureLookup
|
||||
userModule user.Module
|
||||
}
|
||||
|
||||
// SetFlagProvider sets the feature lookup provider
|
||||
@ -37,7 +40,8 @@ func InitDB(sqlStore sqlstore.SQLStore) (*modelDao, error) {
|
||||
}
|
||||
// set package variable so dependent base methods (e.g. AuthCache) will work
|
||||
basedao.SetDB(dao)
|
||||
m := &modelDao{ModelDaoSqlite: dao}
|
||||
userModule := impluser.NewModule(impluser.NewStore(sqlStore))
|
||||
m := &modelDao{ModelDaoSqlite: dao, userModule: userModule}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ func (m *modelDao) CreatePAT(ctx context.Context, orgID string, p types.Gettable
|
||||
return types.GettablePAT{}, model.InternalError(fmt.Errorf("PAT insertion failed"))
|
||||
}
|
||||
|
||||
createdByUser, _ := m.GetUser(ctx, p.UserID)
|
||||
createdByUser, _ := m.userModule.GetUserByID(ctx, orgID, p.UserID)
|
||||
if createdByUser == nil {
|
||||
p.CreatedByUser = types.PatUser{
|
||||
NotFound: true,
|
||||
@ -33,14 +33,15 @@ func (m *modelDao) CreatePAT(ctx context.Context, orgID string, p types.Gettable
|
||||
} else {
|
||||
p.CreatedByUser = types.PatUser{
|
||||
User: ossTypes.User{
|
||||
ID: createdByUser.ID,
|
||||
Name: createdByUser.Name,
|
||||
Email: createdByUser.Email,
|
||||
Identifiable: ossTypes.Identifiable{
|
||||
ID: createdByUser.ID,
|
||||
},
|
||||
DisplayName: createdByUser.DisplayName,
|
||||
Email: createdByUser.Email,
|
||||
TimeAuditable: ossTypes.TimeAuditable{
|
||||
CreatedAt: createdByUser.CreatedAt,
|
||||
UpdatedAt: createdByUser.UpdatedAt,
|
||||
},
|
||||
ProfilePictureURL: createdByUser.ProfilePictureURL,
|
||||
},
|
||||
NotFound: false,
|
||||
}
|
||||
@ -82,7 +83,7 @@ func (m *modelDao) ListPATs(ctx context.Context, orgID string) ([]types.Gettable
|
||||
StorablePersonalAccessToken: pats[i],
|
||||
}
|
||||
|
||||
createdByUser, _ := m.GetUser(ctx, pats[i].UserID)
|
||||
createdByUser, _ := m.userModule.GetUserByID(ctx, orgID, pats[i].UserID)
|
||||
if createdByUser == nil {
|
||||
patWithUser.CreatedByUser = types.PatUser{
|
||||
NotFound: true,
|
||||
@ -90,20 +91,21 @@ func (m *modelDao) ListPATs(ctx context.Context, orgID string) ([]types.Gettable
|
||||
} else {
|
||||
patWithUser.CreatedByUser = types.PatUser{
|
||||
User: ossTypes.User{
|
||||
ID: createdByUser.ID,
|
||||
Name: createdByUser.Name,
|
||||
Email: createdByUser.Email,
|
||||
Identifiable: ossTypes.Identifiable{
|
||||
ID: createdByUser.ID,
|
||||
},
|
||||
DisplayName: createdByUser.DisplayName,
|
||||
Email: createdByUser.Email,
|
||||
TimeAuditable: ossTypes.TimeAuditable{
|
||||
CreatedAt: createdByUser.CreatedAt,
|
||||
UpdatedAt: createdByUser.UpdatedAt,
|
||||
},
|
||||
ProfilePictureURL: createdByUser.ProfilePictureURL,
|
||||
},
|
||||
NotFound: false,
|
||||
}
|
||||
}
|
||||
|
||||
updatedByUser, _ := m.GetUser(ctx, pats[i].UpdatedByUserID)
|
||||
updatedByUser, _ := m.userModule.GetUserByID(ctx, orgID, pats[i].UpdatedByUserID)
|
||||
if updatedByUser == nil {
|
||||
patWithUser.UpdatedByUser = types.PatUser{
|
||||
NotFound: true,
|
||||
@ -111,14 +113,15 @@ func (m *modelDao) ListPATs(ctx context.Context, orgID string) ([]types.Gettable
|
||||
} else {
|
||||
patWithUser.UpdatedByUser = types.PatUser{
|
||||
User: ossTypes.User{
|
||||
ID: updatedByUser.ID,
|
||||
Name: updatedByUser.Name,
|
||||
Email: updatedByUser.Email,
|
||||
Identifiable: ossTypes.Identifiable{
|
||||
ID: updatedByUser.ID,
|
||||
},
|
||||
DisplayName: updatedByUser.DisplayName,
|
||||
Email: updatedByUser.Email,
|
||||
TimeAuditable: ossTypes.TimeAuditable{
|
||||
CreatedAt: updatedByUser.CreatedAt,
|
||||
UpdatedAt: updatedByUser.UpdatedAt,
|
||||
},
|
||||
ProfilePictureURL: updatedByUser.ProfilePictureURL,
|
||||
},
|
||||
NotFound: false,
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
eeuserimpl "github.com/SigNoz/signoz/ee/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/ee/query-service/app"
|
||||
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
|
||||
"github.com/SigNoz/signoz/ee/zeus"
|
||||
@ -13,8 +14,10 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/config"
|
||||
"github.com/SigNoz/signoz/pkg/config/envprovider"
|
||||
"github.com/SigNoz/signoz/pkg/config/fileprovider"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
@ -118,6 +121,12 @@ func main() {
|
||||
signoz.NewWebProviderFactories(),
|
||||
sqlStoreFactories,
|
||||
signoz.NewTelemetryStoreProviderFactories(),
|
||||
func(sqlstore sqlstore.SQLStore) user.Module {
|
||||
return eeuserimpl.NewModule(eeuserimpl.NewStore(sqlstore))
|
||||
},
|
||||
func(userModule user.Module) user.Handler {
|
||||
return eeuserimpl.NewHandler(userModule)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
zap.L().Fatal("Failed to create signoz", zap.Error(err))
|
||||
|
@ -1,12 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
)
|
||||
|
||||
// GettableInvitation overrides base object and adds precheck into
|
||||
// response
|
||||
type GettableInvitation struct {
|
||||
*basemodel.InvitationResponseObject
|
||||
Precheck *basemodel.PrecheckResponse `json:"precheck"`
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package sso
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// SSOIdentity contains details of user received from SSO provider
|
||||
type SSOIdentity struct {
|
||||
UserID string
|
||||
Username string
|
||||
PreferredUsername string
|
||||
Email string
|
||||
EmailVerified bool
|
||||
ConnectorData []byte
|
||||
}
|
||||
|
||||
// OAuthCallbackProvider is an interface implemented by connectors which use an OAuth
|
||||
// style redirect flow to determine user information.
|
||||
type OAuthCallbackProvider interface {
|
||||
// The initial URL user would be redirect to.
|
||||
// OAuth2 implementations support various scopes but we only need profile and user as
|
||||
// the roles are still being managed in SigNoz.
|
||||
BuildAuthURL(state string) (string, error)
|
||||
|
||||
// Handle the callback to the server (after login at oauth provider site)
|
||||
// and return a email identity.
|
||||
// At the moment we dont support auto signup flow (based on domain), so
|
||||
// the full identity (including name, group etc) is not required outside of the
|
||||
// connector
|
||||
HandleCallback(r *http.Request) (identity *SSOIdentity, err error)
|
||||
}
|
@ -19,12 +19,14 @@ var (
|
||||
var (
|
||||
Org = "org"
|
||||
User = "user"
|
||||
FactorPassword = "factor_password"
|
||||
CloudIntegration = "cloud_integration"
|
||||
)
|
||||
|
||||
var (
|
||||
OrgReference = `("org_id") REFERENCES "organizations" ("id")`
|
||||
UserReference = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE`
|
||||
FactorPasswordReference = `("password_id") REFERENCES "factor_password" ("id")`
|
||||
CloudIntegrationReference = `("cloud_integration_id") REFERENCES "cloud_integration" ("id") ON DELETE CASCADE`
|
||||
)
|
||||
|
||||
@ -264,6 +266,8 @@ func (dialect *dialect) RenameTableAndModifyModel(ctx context.Context, bun bun.I
|
||||
fkReferences = append(fkReferences, OrgReference)
|
||||
} else if reference == User && !slices.Contains(fkReferences, UserReference) {
|
||||
fkReferences = append(fkReferences, UserReference)
|
||||
} else if reference == FactorPassword && !slices.Contains(fkReferences, FactorPasswordReference) {
|
||||
fkReferences = append(fkReferences, FactorPasswordReference)
|
||||
} else if reference == CloudIntegration && !slices.Contains(fkReferences, CloudIntegrationReference) {
|
||||
fkReferences = append(fkReferences, CloudIntegrationReference)
|
||||
}
|
||||
|
472
pkg/modules/user/impluser/handler.go
Normal file
472
pkg/modules/user/impluser/handler.go
Normal file
@ -0,0 +1,472 @@
|
||||
package impluser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
module user.Module
|
||||
}
|
||||
|
||||
func NewHandler(module user.Module) user.Handler {
|
||||
return &handler{module: module}
|
||||
}
|
||||
|
||||
func (h *handler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req := new(types.PostableAcceptInvite)
|
||||
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
|
||||
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode user"))
|
||||
return
|
||||
}
|
||||
|
||||
// SSO users might not have a password
|
||||
if err := req.Validate(); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
invite, err := h.module.GetInviteByToken(ctx, req.InviteToken)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if invite.Name == "" && req.DisplayName != "" {
|
||||
invite.Name = req.DisplayName
|
||||
}
|
||||
|
||||
user, err := types.NewUser(invite.Name, invite.Email, invite.Role, invite.OrgID)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
password, err := types.NewFactorPassword(req.Password)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err = h.module.CreateUserWithPassword(ctx, user, password)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// delete the invite
|
||||
if err := h.module.DeleteInvite(ctx, invite.OrgID, invite.ID); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusCreated, user)
|
||||
}
|
||||
|
||||
func (h *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
var req types.PostableInvite
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = h.module.CreateBulkInvite(ctx, claims.OrgID, claims.UserID, &types.PostableBulkInviteRequest{
|
||||
Invites: []types.PostableInvite{req},
|
||||
})
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusCreated, nil)
|
||||
return
|
||||
}
|
||||
|
||||
func (h *handler) CreateBulkInvite(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
var req types.PostableBulkInviteRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that the request contains users
|
||||
if len(req.Invites) == 0 {
|
||||
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "no invites provided for invitation"))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = h.module.CreateBulkInvite(ctx, claims.OrgID, claims.UserID, &req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusCreated, nil)
|
||||
return
|
||||
}
|
||||
|
||||
func (h *handler) GetInvite(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
token := mux.Vars(r)["token"]
|
||||
invite, err := h.module.GetInviteByToken(ctx, token)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, invite)
|
||||
return
|
||||
}
|
||||
|
||||
func (h *handler) ListInvite(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
invites, err := h.module.ListInvite(ctx, claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
render.Success(w, http.StatusOK, invites)
|
||||
}
|
||||
|
||||
func (h *handler) DeleteInvite(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
uuid, err := valuer.NewUUID(id)
|
||||
if err != nil {
|
||||
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.module.DeleteInvite(ctx, claims.OrgID, uuid); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
render.Success(w, http.StatusNoContent, nil)
|
||||
}
|
||||
func (h *handler) GetUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.module.GetUserByID(ctx, claims.OrgID, id)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, user)
|
||||
}
|
||||
|
||||
func (h *handler) ListUsers(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
users, err := h.module.ListUsers(ctx, claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, users)
|
||||
}
|
||||
|
||||
func (h *handler) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
var user types.User
|
||||
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
existingUser, err := h.module.GetUserByID(ctx, claims.OrgID, id)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// only displayName, role can be updated
|
||||
if user.DisplayName == "" {
|
||||
user.DisplayName = existingUser.DisplayName
|
||||
}
|
||||
|
||||
if user.Role == "" {
|
||||
user.Role = existingUser.Role
|
||||
}
|
||||
|
||||
if user.Role != existingUser.Role && claims.Role != types.RoleAdmin {
|
||||
render.Error(w, errors.New(errors.TypeForbidden, errors.CodeForbidden, "only admins can change roles"))
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure that the request is not demoting the last admin user.
|
||||
// also an admin user can only change role of their own or other user
|
||||
if user.Role != existingUser.Role && existingUser.Role == types.RoleAdmin.String() {
|
||||
adminUsers, err := h.module.GetUsersByRoleInOrg(ctx, claims.OrgID, types.RoleAdmin)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(adminUsers) == 1 {
|
||||
render.Error(w, errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot demote the last admin"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
updatedUser, err := h.module.UpdateUser(ctx, claims.OrgID, id, &user)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, updatedUser)
|
||||
}
|
||||
|
||||
func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.module.DeleteUser(ctx, claims.OrgID, id); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (h *handler) LoginPrecheck(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
email := r.URL.Query().Get("email")
|
||||
sourceUrl := r.URL.Query().Get("ref")
|
||||
orgID := r.URL.Query().Get("orgID")
|
||||
|
||||
resp, err := h.module.LoginPrecheck(ctx, orgID, email, sourceUrl)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (h *handler) GetResetPasswordToken(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// check if the id lies in the same org as the claims
|
||||
_, err = h.module.GetUserByID(ctx, claims.OrgID, id)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.module.CreateResetPasswordToken(ctx, id)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, token)
|
||||
}
|
||||
|
||||
func (h *handler) ResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req := new(types.PostableResetPassword)
|
||||
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
entry, err := h.module.GetResetPassword(ctx, req.Token)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.module.UpdatePasswordAndDeleteResetPasswordEntry(ctx, entry.PasswordID, req.Password)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, nil)
|
||||
}
|
||||
|
||||
func (h *handler) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var req types.ChangePasswordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// get the current password
|
||||
password, err := h.module.GetPasswordByUserID(ctx, req.UserId)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !types.ComparePassword(password.Password, req.OldPassword) {
|
||||
render.Error(w, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "old password is incorrect"))
|
||||
return
|
||||
}
|
||||
|
||||
err = h.module.UpdatePassword(ctx, req.UserId, req.NewPassword)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, nil)
|
||||
}
|
||||
|
||||
func (h *handler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var req types.PostableLoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.module.GetAuthenticatedUser(ctx, req.OrgID, req.Email, req.Password, req.RefreshToken)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
render.Error(w, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid email or password"))
|
||||
return
|
||||
}
|
||||
|
||||
jwt, err := h.module.GetJWTForUser(ctx, user)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
gettableLoginResponse := &types.GettableLoginResponse{
|
||||
GettableUserJwt: jwt,
|
||||
UserID: user.ID.String(),
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, gettableLoginResponse)
|
||||
}
|
||||
|
||||
func (h *handler) GetCurrentUserFromJWT(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.module.GetUserByID(ctx, claims.OrgID, claims.UserID)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, user)
|
||||
|
||||
}
|
394
pkg/modules/user/impluser/module.go
Normal file
394
pkg/modules/user/impluser/module.go
Normal file
@ -0,0 +1,394 @@
|
||||
package impluser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
|
||||
smtpservice "github.com/SigNoz/signoz/pkg/query-service/utils/smtpService"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
store types.UserStore
|
||||
JWT *authtypes.JWT
|
||||
}
|
||||
|
||||
// This module is a WIP, don't take inspiration from this.
|
||||
func NewModule(store types.UserStore) user.Module {
|
||||
jwtSecret := os.Getenv("SIGNOZ_JWT_SECRET")
|
||||
jwt := authtypes.NewJWT(jwtSecret, 30*time.Minute, 30*24*time.Hour)
|
||||
return &Module{store: store, JWT: jwt}
|
||||
}
|
||||
|
||||
// CreateBulk implements invite.Module.
|
||||
func (m *Module) CreateBulkInvite(ctx context.Context, orgID, userID string, bulkInvites *types.PostableBulkInviteRequest) ([]*types.Invite, error) {
|
||||
creator, err := m.GetUserByID(ctx, orgID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
invites := make([]*types.Invite, 0, len(bulkInvites.Invites))
|
||||
|
||||
for _, invite := range bulkInvites.Invites {
|
||||
// check if user exists
|
||||
existingUser, err := m.GetUserByEmailInOrg(ctx, orgID, invite.Email)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
if existingUser != nil {
|
||||
return nil, errors.New(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "User already exists with the same email")
|
||||
}
|
||||
|
||||
// Check if an invite already exists
|
||||
existingInvite, err := m.GetInviteByEmailInOrg(ctx, orgID, invite.Email)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
if existingInvite != nil {
|
||||
return nil, errors.New(errors.TypeAlreadyExists, errors.CodeAlreadyExists, "An invite already exists for this email")
|
||||
}
|
||||
|
||||
role, err := types.NewRole(invite.Role.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newInvite, err := types.NewInvite(orgID, role.String(), invite.Name, invite.Email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newInvite.InviteLink = fmt.Sprintf("%s/signup?token=%s", invite.FrontendBaseUrl, newInvite.Token)
|
||||
invites = append(invites, newInvite)
|
||||
}
|
||||
|
||||
err = m.store.CreateBulkInvite(ctx, invites)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// send telemetry event
|
||||
for i := 0; i < len(invites); i++ {
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_USER_INVITATION_SENT, map[string]interface{}{
|
||||
"invited user email": invites[i].Email,
|
||||
}, creator.Email, true, false)
|
||||
|
||||
// send email if SMTP is enabled
|
||||
if os.Getenv("SMTP_ENABLED") == "true" && bulkInvites.Invites[i].FrontendBaseUrl != "" {
|
||||
m.inviteEmail(&bulkInvites.Invites[i], creator.Email, creator.DisplayName, invites[i].Token)
|
||||
}
|
||||
}
|
||||
|
||||
return invites, nil
|
||||
}
|
||||
|
||||
func (m *Module) inviteEmail(req *types.PostableInvite, creatorEmail, creatorName, token string) {
|
||||
smtp := smtpservice.GetInstance()
|
||||
data := types.InviteEmailData{
|
||||
CustomerName: req.Name,
|
||||
InviterName: creatorName,
|
||||
InviterEmail: creatorEmail,
|
||||
Link: fmt.Sprintf("%s/signup?token=%s", req.FrontendBaseUrl, token),
|
||||
}
|
||||
|
||||
tmpl, err := template.ParseFiles(constants.InviteEmailTemplate)
|
||||
if err != nil {
|
||||
zap.L().Error("failed to send email", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
var body bytes.Buffer
|
||||
if err := tmpl.Execute(&body, data); err != nil {
|
||||
zap.L().Error("failed to send email", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
err = smtp.SendEmail(
|
||||
req.Email,
|
||||
creatorName+" has invited you to their team in SigNoz",
|
||||
body.String(),
|
||||
)
|
||||
if err != nil {
|
||||
zap.L().Error("failed to send email", zap.Error(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Module) ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error) {
|
||||
return m.store.ListInvite(ctx, orgID)
|
||||
}
|
||||
|
||||
func (m *Module) DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error {
|
||||
return m.store.DeleteInvite(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (m *Module) GetInviteByToken(ctx context.Context, token string) (*types.GettableInvite, error) {
|
||||
return m.store.GetInviteByToken(ctx, token)
|
||||
}
|
||||
|
||||
func (m *Module) GetInviteByEmailInOrg(ctx context.Context, orgID string, email string) (*types.Invite, error) {
|
||||
return m.store.GetInviteByEmailInOrg(ctx, orgID, email)
|
||||
}
|
||||
|
||||
func (m *Module) CreateUserWithPassword(ctx context.Context, user *types.User, password *types.FactorPassword) (*types.User, error) {
|
||||
|
||||
user, err := m.store.CreateUserWithPassword(ctx, user, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (m *Module) CreateUser(ctx context.Context, user *types.User) error {
|
||||
return m.store.CreateUser(ctx, user)
|
||||
}
|
||||
|
||||
func (m *Module) GetUserByID(ctx context.Context, orgID string, id string) (*types.GettableUser, error) {
|
||||
return m.store.GetUserByID(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (m *Module) GetUserByEmailInOrg(ctx context.Context, orgID string, email string) (*types.GettableUser, error) {
|
||||
return m.store.GetUserByEmailInOrg(ctx, orgID, email)
|
||||
}
|
||||
|
||||
func (m *Module) GetUsersByEmail(ctx context.Context, email string) ([]*types.GettableUser, error) {
|
||||
return m.store.GetUsersByEmail(ctx, email)
|
||||
}
|
||||
|
||||
func (m *Module) GetUsersByRoleInOrg(ctx context.Context, orgID string, role types.Role) ([]*types.GettableUser, error) {
|
||||
return m.store.GetUsersByRoleInOrg(ctx, orgID, role)
|
||||
}
|
||||
|
||||
func (m *Module) ListUsers(ctx context.Context, orgID string) ([]*types.GettableUser, error) {
|
||||
return m.store.ListUsers(ctx, orgID)
|
||||
}
|
||||
|
||||
func (m *Module) UpdateUser(ctx context.Context, orgID string, id string, user *types.User) (*types.User, error) {
|
||||
return m.store.UpdateUser(ctx, orgID, id, user)
|
||||
}
|
||||
|
||||
func (m *Module) DeleteUser(ctx context.Context, orgID string, id string) error {
|
||||
user, err := m.store.GetUserByID(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(user.Email)) {
|
||||
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "integration user cannot be deleted")
|
||||
}
|
||||
|
||||
// don't allow to delete the last admin user
|
||||
adminUsers, err := m.GetUsersByRoleInOrg(ctx, orgID, types.RoleAdmin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(adminUsers) == 1 && user.Role == types.RoleAdmin.String() {
|
||||
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot delete the last admin")
|
||||
}
|
||||
|
||||
return m.store.DeleteUser(ctx, orgID, user.ID.StringValue())
|
||||
}
|
||||
|
||||
func (m *Module) CreateResetPasswordToken(ctx context.Context, userID string) (*types.ResetPasswordRequest, error) {
|
||||
password, err := m.store.GetPasswordByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
// if the user does not have a password, we need to create a new one
|
||||
// this will happen for SSO users
|
||||
if errors.Ast(err, errors.TypeNotFound) {
|
||||
password, err = m.store.CreatePassword(ctx, &types.FactorPassword{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
Password: valuer.GenerateUUID().String(),
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
resetPasswordRequest, err := types.NewResetPasswordRequest(password.ID.StringValue())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check if a reset password token already exists for this user
|
||||
existingRequest, err := m.store.GetResetPasswordByPasswordID(ctx, resetPasswordRequest.PasswordID)
|
||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existingRequest != nil {
|
||||
return existingRequest, nil
|
||||
}
|
||||
|
||||
err = m.store.CreateResetPasswordToken(ctx, resetPasswordRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resetPasswordRequest, nil
|
||||
}
|
||||
|
||||
func (m *Module) GetPasswordByUserID(ctx context.Context, id string) (*types.FactorPassword, error) {
|
||||
return m.store.GetPasswordByUserID(ctx, id)
|
||||
}
|
||||
|
||||
func (m *Module) GetResetPassword(ctx context.Context, token string) (*types.ResetPasswordRequest, error) {
|
||||
return m.store.GetResetPassword(ctx, token)
|
||||
}
|
||||
|
||||
func (m *Module) UpdatePasswordAndDeleteResetPasswordEntry(ctx context.Context, passwordID string, password string) error {
|
||||
hashedPassword, err := types.HashPassword(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existingPassword, err := m.store.GetPasswordByID(ctx, passwordID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.store.UpdatePasswordAndDeleteResetPasswordEntry(ctx, existingPassword.UserID, hashedPassword)
|
||||
}
|
||||
|
||||
func (m *Module) UpdatePassword(ctx context.Context, userID string, password string) error {
|
||||
hashedPassword, err := types.HashPassword(password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.store.UpdatePassword(ctx, userID, hashedPassword)
|
||||
}
|
||||
|
||||
func (m *Module) GetAuthenticatedUser(ctx context.Context, orgID, email, password, refreshToken string) (*types.User, error) {
|
||||
if refreshToken != "" {
|
||||
// parse the refresh token
|
||||
claims, err := m.JWT.Claims(refreshToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := m.store.GetUserByID(ctx, claims.OrgID, claims.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.User, nil
|
||||
}
|
||||
|
||||
var dbUser *types.User
|
||||
|
||||
// when the orgID is provided
|
||||
if orgID != "" {
|
||||
user, err := m.store.GetUserByEmailInOrg(ctx, orgID, email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbUser = &user.User
|
||||
}
|
||||
|
||||
// when the orgID is not provided we login if the user exists in just one org
|
||||
user, err := m.store.GetUsersByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(user) == 1 {
|
||||
dbUser = &user[0].User
|
||||
} else {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "please provide an orgID")
|
||||
}
|
||||
|
||||
existingPassword, err := m.store.GetPasswordByUserID(ctx, dbUser.ID.StringValue())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !types.ComparePassword(existingPassword.Password, password) {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid password")
|
||||
}
|
||||
|
||||
return dbUser, nil
|
||||
}
|
||||
|
||||
func (m *Module) LoginPrecheck(ctx context.Context, orgID, email, sourceUrl string) (*types.GettableLoginPrecheck, error) {
|
||||
// assume user is valid unless proven otherwise and assign default values for rest of the fields
|
||||
resp := &types.GettableLoginPrecheck{IsUser: true, CanSelfRegister: false, SSO: false, SSOUrl: "", SSOError: ""}
|
||||
|
||||
// check if email is a valid user
|
||||
users, err := m.GetUsersByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
resp.IsUser = false
|
||||
}
|
||||
|
||||
if len(users) > 1 {
|
||||
resp.SelectOrg = true
|
||||
resp.Orgs = make([]string, len(users))
|
||||
for i, user := range users {
|
||||
resp.Orgs[i] = user.OrgID
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (m *Module) GetJWTForUser(ctx context.Context, user *types.User) (types.GettableUserJwt, error) {
|
||||
role, err := types.NewRole(user.Role)
|
||||
if err != nil {
|
||||
return types.GettableUserJwt{}, err
|
||||
}
|
||||
|
||||
accessJwt, accessClaims, err := m.JWT.AccessToken(user.OrgID, user.ID.String(), user.Email, role)
|
||||
if err != nil {
|
||||
return types.GettableUserJwt{}, err
|
||||
}
|
||||
|
||||
refreshJwt, refreshClaims, err := m.JWT.RefreshToken(user.OrgID, user.ID.String(), user.Email, role)
|
||||
if err != nil {
|
||||
return types.GettableUserJwt{}, err
|
||||
}
|
||||
|
||||
return types.GettableUserJwt{
|
||||
AccessJwt: accessJwt,
|
||||
RefreshJwt: refreshJwt,
|
||||
AccessJwtExpiry: accessClaims.ExpiresAt.Unix(),
|
||||
RefreshJwtExpiry: refreshClaims.ExpiresAt.Unix(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *Module) CreateUserForSAMLRequest(ctx context.Context, email string) (*types.User, error) {
|
||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "SAML login is not supported")
|
||||
}
|
||||
|
||||
func (m *Module) PrepareSsoRedirect(ctx context.Context, redirectUri, email string, jwt *authtypes.JWT) (string, error) {
|
||||
return "", errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "SSO is not supported")
|
||||
}
|
||||
|
||||
func (m *Module) CanUsePassword(ctx context.Context, email string) (bool, error) {
|
||||
return false, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "SSO is not supported")
|
||||
}
|
||||
|
||||
func (m *Module) GetAuthDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, error) {
|
||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "SSO is not supported")
|
||||
}
|
476
pkg/modules/user/impluser/store.go
Normal file
476
pkg/modules/user/impluser/store.go
Normal file
@ -0,0 +1,476 @@
|
||||
package impluser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
}
|
||||
|
||||
func NewStore(sqlstore sqlstore.SQLStore) types.UserStore {
|
||||
return &Store{sqlstore: sqlstore}
|
||||
}
|
||||
|
||||
// CreateBulkInvite implements types.InviteStore.
|
||||
func (s *Store) CreateBulkInvite(ctx context.Context, invites []*types.Invite) error {
|
||||
_, err := s.sqlstore.BunDB().NewInsert().
|
||||
Model(&invites).
|
||||
Exec(ctx)
|
||||
|
||||
if err != nil {
|
||||
return s.sqlstore.WrapAlreadyExistsErrf(err, types.ErrInviteAlreadyExists, "invite with email: %s already exists in org: %s", invites[0].Email, invites[0].OrgID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete implements types.InviteStore.
|
||||
func (s *Store) DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error {
|
||||
_, err := s.sqlstore.BunDB().NewDelete().
|
||||
Model(&types.Invite{}).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("id = ?", id).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return s.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite with id: %s does not exist in org: %s", id.StringValue(), orgID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInviteByEmailInOrg implements types.InviteStore.
|
||||
func (s *Store) GetInviteByEmailInOrg(ctx context.Context, orgID string, email string) (*types.Invite, error) {
|
||||
invite := new(types.Invite)
|
||||
err := s.sqlstore.BunDB().NewSelect().
|
||||
Model(invite).
|
||||
Where("email = ?", email).
|
||||
Where("org_id = ?", orgID).
|
||||
Scan(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, s.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite with email: %s does not exist in org: %s", email, orgID)
|
||||
}
|
||||
|
||||
return invite, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetInviteByToken(ctx context.Context, token string) (*types.GettableInvite, error) {
|
||||
invite := new(types.Invite)
|
||||
err := s.sqlstore.BunDB().NewSelect().
|
||||
Model(invite).
|
||||
Where("token = ?", token).
|
||||
Scan(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, s.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite with token: %s does not exist", token)
|
||||
}
|
||||
|
||||
orgName, err := s.getOrgNameByID(ctx, invite.OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gettableInvite := &types.GettableInvite{
|
||||
Invite: *invite,
|
||||
Organization: orgName,
|
||||
}
|
||||
|
||||
return gettableInvite, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error) {
|
||||
invites := new([]*types.Invite)
|
||||
err := s.sqlstore.BunDB().NewSelect().
|
||||
Model(invites).
|
||||
Where("org_id = ?", orgID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, s.sqlstore.WrapNotFoundErrf(err, types.ErrInviteNotFound, "invite with org id: %s does not exist", orgID)
|
||||
}
|
||||
return *invites, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreatePassword(ctx context.Context, password *types.FactorPassword) (*types.FactorPassword, error) {
|
||||
_, err := s.sqlstore.BunDB().NewInsert().
|
||||
Model(password).
|
||||
Exec(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, s.sqlstore.WrapAlreadyExistsErrf(err, types.ErrPasswordAlreadyExists, "password with user id: %s already exists", password.UserID)
|
||||
}
|
||||
|
||||
return password, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateUserWithPassword(ctx context.Context, user *types.User, password *types.FactorPassword) (*types.User, error) {
|
||||
tx, err := s.sqlstore.BunDB().BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to start transaction")
|
||||
}
|
||||
|
||||
defer tx.Rollback()
|
||||
|
||||
if _, err := tx.NewInsert().
|
||||
Model(user).
|
||||
Exec(ctx); err != nil {
|
||||
return nil, s.sqlstore.WrapAlreadyExistsErrf(err, types.ErrUserAlreadyExists, "user with email: %s already exists in org: %s", user.Email, user.OrgID)
|
||||
}
|
||||
|
||||
password.UserID = user.ID.StringValue()
|
||||
if _, err := tx.NewInsert().
|
||||
Model(password).
|
||||
Exec(ctx); err != nil {
|
||||
return nil, s.sqlstore.WrapAlreadyExistsErrf(err, types.ErrPasswordAlreadyExists, "password with email: %s already exists in org: %s", user.Email, user.OrgID)
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to commit transaction")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateUser(ctx context.Context, user *types.User) error {
|
||||
_, err := s.sqlstore.BunDB().NewInsert().
|
||||
Model(user).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return s.sqlstore.WrapAlreadyExistsErrf(err, types.ErrUserAlreadyExists, "user with email: %s already exists in org: %s", user.Email, user.OrgID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) GetDefaultOrgID(ctx context.Context) (string, error) {
|
||||
org := new(types.Organization)
|
||||
err := s.sqlstore.BunDB().NewSelect().
|
||||
Model(org).
|
||||
Limit(1).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return "", s.sqlstore.WrapNotFoundErrf(err, types.ErrOrganizationNotFound, "default org does not exist")
|
||||
}
|
||||
return org.ID.String(), nil
|
||||
}
|
||||
|
||||
// this is temporary function, we plan to remove this in the next PR.
|
||||
func (s *Store) getOrgNameByID(ctx context.Context, orgID string) (string, error) {
|
||||
org := new(types.Organization)
|
||||
err := s.sqlstore.BunDB().NewSelect().
|
||||
Model(org).
|
||||
Where("id = ?", orgID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return "", s.sqlstore.WrapNotFoundErrf(err, types.ErrOrganizationNotFound, "org with id: %s does not exist", orgID)
|
||||
}
|
||||
return org.DisplayName, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetUserByID(ctx context.Context, orgID string, id string) (*types.GettableUser, error) {
|
||||
user := new(types.User)
|
||||
err := s.sqlstore.BunDB().NewSelect().
|
||||
Model(user).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("id = ?", id).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, s.sqlstore.WrapNotFoundErrf(err, types.ErrUserNotFound, "user with id: %s does not exist in org: %s", id, orgID)
|
||||
}
|
||||
|
||||
// remove this in next PR
|
||||
orgName, err := s.getOrgNameByID(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.GettableUser{User: *user, Organization: orgName}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetUserByEmailInOrg(ctx context.Context, orgID string, email string) (*types.GettableUser, error) {
|
||||
user := new(types.User)
|
||||
err := s.sqlstore.BunDB().NewSelect().
|
||||
Model(user).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("email = ?", email).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, s.sqlstore.WrapNotFoundErrf(err, types.ErrUserNotFound, "user with email: %s does not exist in org: %s", email, orgID)
|
||||
}
|
||||
|
||||
// remove this in next PR
|
||||
orgName, err := s.getOrgNameByID(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.GettableUser{User: *user, Organization: orgName}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetUsersByEmail(ctx context.Context, email string) ([]*types.GettableUser, error) {
|
||||
users := new([]*types.User)
|
||||
err := s.sqlstore.BunDB().NewSelect().
|
||||
Model(users).
|
||||
Where("email = ?", email).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, s.sqlstore.WrapNotFoundErrf(err, types.ErrUserNotFound, "user with email: %s does not exist", email)
|
||||
}
|
||||
|
||||
// remove this in next PR
|
||||
usersWithOrg := []*types.GettableUser{}
|
||||
for _, user := range *users {
|
||||
orgName, err := s.getOrgNameByID(ctx, user.OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usersWithOrg = append(usersWithOrg, &types.GettableUser{User: *user, Organization: orgName})
|
||||
}
|
||||
return usersWithOrg, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetUsersByRoleInOrg(ctx context.Context, orgID string, role types.Role) ([]*types.GettableUser, error) {
|
||||
users := new([]*types.User)
|
||||
err := s.sqlstore.BunDB().NewSelect().
|
||||
Model(users).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("role = ?", role).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, s.sqlstore.WrapNotFoundErrf(err, types.ErrUserNotFound, "user with role: %s does not exist in org: %s", role, orgID)
|
||||
}
|
||||
|
||||
// remove this in next PR
|
||||
orgName, err := s.getOrgNameByID(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usersWithOrg := []*types.GettableUser{}
|
||||
for _, user := range *users {
|
||||
usersWithOrg = append(usersWithOrg, &types.GettableUser{User: *user, Organization: orgName})
|
||||
}
|
||||
return usersWithOrg, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateUser(ctx context.Context, orgID string, id string, user *types.User) (*types.User, error) {
|
||||
user.UpdatedAt = time.Now()
|
||||
_, err := s.sqlstore.BunDB().NewUpdate().
|
||||
Model(user).
|
||||
Column("display_name").
|
||||
Column("role").
|
||||
Column("updated_at").
|
||||
Where("id = ?", id).
|
||||
Where("org_id = ?", orgID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return nil, s.sqlstore.WrapNotFoundErrf(err, types.ErrUserNotFound, "user with id: %s does not exist in org: %s", id, orgID)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListUsers(ctx context.Context, orgID string) ([]*types.GettableUser, error) {
|
||||
users := []*types.User{}
|
||||
err := s.sqlstore.BunDB().NewSelect().
|
||||
Model(&users).
|
||||
Where("org_id = ?", orgID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, s.sqlstore.WrapNotFoundErrf(err, types.ErrUserNotFound, "users with org id: %s does not exist", orgID)
|
||||
}
|
||||
|
||||
// remove this in next PR
|
||||
orgName, err := s.getOrgNameByID(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usersWithOrg := []*types.GettableUser{}
|
||||
for _, user := range users {
|
||||
usersWithOrg = append(usersWithOrg, &types.GettableUser{User: *user, Organization: orgName})
|
||||
}
|
||||
return usersWithOrg, nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteUser(ctx context.Context, orgID string, id string) error {
|
||||
|
||||
tx, err := s.sqlstore.BunDB().BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to start transaction")
|
||||
}
|
||||
|
||||
defer tx.Rollback()
|
||||
|
||||
// get the password id
|
||||
|
||||
var password types.FactorPassword
|
||||
err = tx.NewSelect().
|
||||
Model(&password).
|
||||
Where("user_id = ?", id).
|
||||
Scan(ctx)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete password")
|
||||
}
|
||||
|
||||
// delete reset password request
|
||||
_, err = tx.NewDelete().
|
||||
Model(new(types.ResetPasswordRequest)).
|
||||
Where("password_id = ?", password.ID.String()).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete reset password request")
|
||||
}
|
||||
|
||||
// delete factor password
|
||||
_, err = tx.NewDelete().
|
||||
Model(new(types.FactorPassword)).
|
||||
Where("user_id = ?", id).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete factor password")
|
||||
}
|
||||
|
||||
// delete user
|
||||
_, err = tx.NewDelete().
|
||||
Model(new(types.User)).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("id = ?", id).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete user")
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to commit transaction")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateResetPasswordToken(ctx context.Context, resetPasswordRequest *types.ResetPasswordRequest) error {
|
||||
_, err := s.sqlstore.BunDB().NewInsert().
|
||||
Model(resetPasswordRequest).
|
||||
Exec(ctx)
|
||||
|
||||
if err != nil {
|
||||
return s.sqlstore.WrapAlreadyExistsErrf(err, types.ErrResetPasswordTokenAlreadyExists, "reset password token with password id: %s already exists", resetPasswordRequest.PasswordID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) GetPasswordByID(ctx context.Context, id string) (*types.FactorPassword, error) {
|
||||
password := new(types.FactorPassword)
|
||||
err := s.sqlstore.BunDB().NewSelect().
|
||||
Model(password).
|
||||
Where("id = ?", id).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, s.sqlstore.WrapNotFoundErrf(err, types.ErrPasswordNotFound, "password with id: %s does not exist", id)
|
||||
}
|
||||
return password, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetPasswordByUserID(ctx context.Context, id string) (*types.FactorPassword, error) {
|
||||
password := new(types.FactorPassword)
|
||||
err := s.sqlstore.BunDB().NewSelect().
|
||||
Model(password).
|
||||
Where("user_id = ?", id).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, s.sqlstore.WrapNotFoundErrf(err, types.ErrPasswordNotFound, "password with user id: %s does not exist", id)
|
||||
}
|
||||
return password, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetResetPasswordByPasswordID(ctx context.Context, passwordID string) (*types.ResetPasswordRequest, error) {
|
||||
resetPasswordRequest := new(types.ResetPasswordRequest)
|
||||
err := s.sqlstore.BunDB().NewSelect().
|
||||
Model(resetPasswordRequest).
|
||||
Where("password_id = ?", passwordID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, s.sqlstore.WrapNotFoundErrf(err, types.ErrResetPasswordTokenNotFound, "reset password token with password id: %s does not exist", passwordID)
|
||||
}
|
||||
return resetPasswordRequest, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetResetPassword(ctx context.Context, token string) (*types.ResetPasswordRequest, error) {
|
||||
resetPasswordRequest := new(types.ResetPasswordRequest)
|
||||
err := s.sqlstore.BunDB().NewSelect().
|
||||
Model(resetPasswordRequest).
|
||||
Where("token = ?", token).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, s.sqlstore.WrapNotFoundErrf(err, types.ErrResetPasswordTokenNotFound, "reset password token with token: %s does not exist", token)
|
||||
}
|
||||
return resetPasswordRequest, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdatePasswordAndDeleteResetPasswordEntry(ctx context.Context, userID string, password string) error {
|
||||
tx, err := s.sqlstore.BunDB().BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to start transaction")
|
||||
}
|
||||
|
||||
defer tx.Rollback()
|
||||
|
||||
factorPassword := &types.FactorPassword{
|
||||
UserID: userID,
|
||||
Password: password,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
}
|
||||
_, err = tx.NewUpdate().
|
||||
Model(factorPassword).
|
||||
Column("password").
|
||||
Column("updated_at").
|
||||
Where("user_id = ?", userID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return s.sqlstore.WrapNotFoundErrf(err, types.ErrPasswordNotFound, "password with user id: %s does not exist", userID)
|
||||
}
|
||||
|
||||
_, err = tx.NewDelete().
|
||||
Model(&types.ResetPasswordRequest{}).
|
||||
Where("password_id = ?", userID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return s.sqlstore.WrapNotFoundErrf(err, types.ErrResetPasswordTokenNotFound, "reset password token with password id: %s does not exist", userID)
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to commit transaction")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdatePassword(ctx context.Context, userID string, password string) error {
|
||||
factorPassword := &types.FactorPassword{
|
||||
UserID: userID,
|
||||
Password: password,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
}
|
||||
_, err := s.sqlstore.BunDB().NewUpdate().
|
||||
Model(factorPassword).
|
||||
Column("password").
|
||||
Column("updated_at").
|
||||
Where("user_id = ?", userID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return s.sqlstore.WrapNotFoundErrf(err, types.ErrPasswordNotFound, "password with user id: %s does not exist", userID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) GetDomainByName(ctx context.Context, name string) (*types.StorableOrgDomain, error) {
|
||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not supported")
|
||||
}
|
75
pkg/modules/user/user.go
Normal file
75
pkg/modules/user/user.go
Normal file
@ -0,0 +1,75 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Module interface {
|
||||
// invite
|
||||
CreateBulkInvite(ctx context.Context, orgID, userID string, bulkInvites *types.PostableBulkInviteRequest) ([]*types.Invite, error)
|
||||
ListInvite(ctx context.Context, orgID string) ([]*types.Invite, error)
|
||||
DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error
|
||||
GetInviteByToken(ctx context.Context, token string) (*types.GettableInvite, error)
|
||||
GetInviteByEmailInOrg(ctx context.Context, orgID string, email string) (*types.Invite, error)
|
||||
|
||||
// user
|
||||
CreateUserWithPassword(ctx context.Context, user *types.User, password *types.FactorPassword) (*types.User, error)
|
||||
CreateUser(ctx context.Context, user *types.User) error
|
||||
GetUserByID(ctx context.Context, orgID string, id string) (*types.GettableUser, error)
|
||||
GetUsersByEmail(ctx context.Context, email string) ([]*types.GettableUser, error) // public function
|
||||
GetUserByEmailInOrg(ctx context.Context, orgID string, email string) (*types.GettableUser, error)
|
||||
GetUsersByRoleInOrg(ctx context.Context, orgID string, role types.Role) ([]*types.GettableUser, error)
|
||||
ListUsers(ctx context.Context, orgID string) ([]*types.GettableUser, error)
|
||||
UpdateUser(ctx context.Context, orgID string, id string, user *types.User) (*types.User, error)
|
||||
DeleteUser(ctx context.Context, orgID string, id string) error
|
||||
|
||||
// login
|
||||
GetAuthenticatedUser(ctx context.Context, orgID, email, password, refreshToken string) (*types.User, error)
|
||||
GetJWTForUser(ctx context.Context, user *types.User) (types.GettableUserJwt, error)
|
||||
CreateUserForSAMLRequest(ctx context.Context, email string) (*types.User, error)
|
||||
LoginPrecheck(ctx context.Context, orgID, email, sourceUrl string) (*types.GettableLoginPrecheck, error)
|
||||
|
||||
// sso
|
||||
PrepareSsoRedirect(ctx context.Context, redirectUri, email string, jwt *authtypes.JWT) (string, error)
|
||||
CanUsePassword(ctx context.Context, email string) (bool, error)
|
||||
|
||||
// password
|
||||
CreateResetPasswordToken(ctx context.Context, userID string) (*types.ResetPasswordRequest, error)
|
||||
GetPasswordByUserID(ctx context.Context, id string) (*types.FactorPassword, error)
|
||||
GetResetPassword(ctx context.Context, token string) (*types.ResetPasswordRequest, error)
|
||||
UpdatePassword(ctx context.Context, userID string, password string) error
|
||||
UpdatePasswordAndDeleteResetPasswordEntry(ctx context.Context, passwordID string, password string) error
|
||||
|
||||
// Auth Domain
|
||||
GetAuthDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, error)
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
// invite
|
||||
CreateInvite(http.ResponseWriter, *http.Request)
|
||||
AcceptInvite(http.ResponseWriter, *http.Request)
|
||||
GetInvite(http.ResponseWriter, *http.Request) // public function
|
||||
ListInvite(http.ResponseWriter, *http.Request)
|
||||
DeleteInvite(http.ResponseWriter, *http.Request)
|
||||
CreateBulkInvite(http.ResponseWriter, *http.Request)
|
||||
|
||||
GetUser(http.ResponseWriter, *http.Request)
|
||||
GetCurrentUserFromJWT(http.ResponseWriter, *http.Request)
|
||||
ListUsers(http.ResponseWriter, *http.Request)
|
||||
UpdateUser(http.ResponseWriter, *http.Request)
|
||||
DeleteUser(http.ResponseWriter, *http.Request)
|
||||
|
||||
// Login
|
||||
LoginPrecheck(http.ResponseWriter, *http.Request)
|
||||
Login(http.ResponseWriter, *http.Request)
|
||||
|
||||
// Reset Password
|
||||
GetResetPasswordToken(http.ResponseWriter, *http.Request)
|
||||
ResetPassword(http.ResponseWriter, *http.Request)
|
||||
ChangePassword(http.ResponseWriter, *http.Request)
|
||||
}
|
@ -28,7 +28,7 @@ func (r *Repo) GetConfigHistory(
|
||||
element_type,
|
||||
COALESCE(created_by, -1) as created_by,
|
||||
created_at,
|
||||
COALESCE((SELECT NAME FROM users
|
||||
COALESCE((SELECT display_name FROM users
|
||||
WHERE id = v.created_by), "unknown") created_by_name,
|
||||
active,
|
||||
is_valid,
|
||||
@ -67,7 +67,7 @@ func (r *Repo) GetConfigVersion(
|
||||
element_type,
|
||||
COALESCE(created_by, -1) as created_by,
|
||||
created_at,
|
||||
COALESCE((SELECT NAME FROM users
|
||||
COALESCE((SELECT display_name FROM users
|
||||
WHERE id = v.created_by), "unknown") created_by_name,
|
||||
active,
|
||||
is_valid,
|
||||
@ -100,7 +100,7 @@ func (r *Repo) GetLatestVersion(
|
||||
element_type,
|
||||
COALESCE(created_by, -1) as created_by,
|
||||
created_at,
|
||||
COALESCE((SELECT NAME FROM users
|
||||
COALESCE((SELECT display_name FROM users
|
||||
WHERE id = v.created_by), "unknown") created_by_name,
|
||||
active,
|
||||
is_valid,
|
||||
|
@ -6,11 +6,11 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/dao"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -22,7 +22,8 @@ func TestRegenerateConnectionUrlWithUpdatedConfig(t *testing.T) {
|
||||
require.NoError(err)
|
||||
|
||||
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
|
||||
user, apiErr := createTestUser(organizationModule)
|
||||
userModule := impluser.NewModule(impluser.NewStore(sqlStore))
|
||||
user, apiErr := createTestUser(organizationModule, userModule)
|
||||
require.Nil(apiErr)
|
||||
|
||||
// should be able to generate connection url for
|
||||
@ -69,7 +70,8 @@ func TestAgentCheckIns(t *testing.T) {
|
||||
controller, err := NewController(sqlStore)
|
||||
require.NoError(err)
|
||||
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
|
||||
user, apiErr := createTestUser(organizationModule)
|
||||
userModule := impluser.NewModule(impluser.NewStore(sqlStore))
|
||||
user, apiErr := createTestUser(organizationModule, userModule)
|
||||
require.Nil(apiErr)
|
||||
|
||||
// An agent should be able to check in from a cloud account even
|
||||
@ -156,7 +158,8 @@ func TestCantDisconnectNonExistentAccount(t *testing.T) {
|
||||
require.NoError(err)
|
||||
|
||||
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
|
||||
user, apiErr := createTestUser(organizationModule)
|
||||
userModule := impluser.NewModule(impluser.NewStore(sqlStore))
|
||||
user, apiErr := createTestUser(organizationModule, userModule)
|
||||
require.Nil(apiErr)
|
||||
|
||||
// Attempting to disconnect a non-existent account should return error
|
||||
@ -175,7 +178,8 @@ func TestConfigureService(t *testing.T) {
|
||||
require.NoError(err)
|
||||
|
||||
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
|
||||
user, apiErr := createTestUser(organizationModule)
|
||||
userModule := impluser.NewModule(impluser.NewStore(sqlStore))
|
||||
user, apiErr := createTestUser(organizationModule, userModule)
|
||||
require.Nil(apiErr)
|
||||
|
||||
// create a connected account
|
||||
@ -290,7 +294,7 @@ func makeTestConnectedAccount(t *testing.T, orgId string, controller *Controller
|
||||
return acc
|
||||
}
|
||||
|
||||
func createTestUser(organizationModule organization.Module) (*types.User, *model.ApiError) {
|
||||
func createTestUser(organizationModule organization.Module, userModule user.Module) (*types.User, *model.ApiError) {
|
||||
// Create a test user for auth
|
||||
ctx := context.Background()
|
||||
organization := types.NewOrganization("test")
|
||||
@ -299,17 +303,18 @@ func createTestUser(organizationModule organization.Module) (*types.User, *model
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
|
||||
userId := uuid.NewString()
|
||||
return dao.DB().CreateUser(
|
||||
ctx,
|
||||
&types.User{
|
||||
ID: userId,
|
||||
Name: "test",
|
||||
Email: userId[:8] + "test@test.com",
|
||||
Password: "test",
|
||||
OrgID: organization.ID.StringValue(),
|
||||
Role: authtypes.RoleAdmin.String(),
|
||||
},
|
||||
true,
|
||||
)
|
||||
random, err := utils.RandomHex(3)
|
||||
if err != nil {
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
|
||||
user, err := types.NewUser("test", random+"test@test.com", types.RoleAdmin.String(), organization.ID.StringValue())
|
||||
if err != nil {
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
err = userModule.CreateUser(ctx, user)
|
||||
if err != nil {
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
@ -268,17 +268,20 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
|
||||
}
|
||||
aH.queryBuilder = queryBuilder.NewQueryBuilder(builderOpts)
|
||||
|
||||
// check if at least one user is created
|
||||
hasUsers, err := aH.appDao.GetUsersWithOpts(context.Background(), 1)
|
||||
if err.Error() != "" {
|
||||
// raise warning but no panic as this is a recoverable condition
|
||||
zap.L().Warn("unexpected error while fetch user count while initializing base api handler", zap.Error(err))
|
||||
// TODO(nitya): remote this in later for multitenancy.
|
||||
orgs, err := opts.Signoz.Modules.Organization.GetAll(context.Background())
|
||||
if err != nil {
|
||||
zap.L().Warn("unexpected error while fetching orgs while initializing base api handler", zap.Error(err))
|
||||
}
|
||||
if len(hasUsers) != 0 {
|
||||
// first user is already created, we can mark the app ready for general use.
|
||||
// this means, we disable self-registration and expect new users
|
||||
// to signup signoz through invite link only.
|
||||
aH.SetupCompleted = true
|
||||
// if the first org with the first user is created then the setup is complete.
|
||||
if len(orgs) == 1 {
|
||||
users, err := opts.Signoz.Modules.User.ListUsers(context.Background(), orgs[0].ID.String())
|
||||
if err != nil {
|
||||
zap.L().Warn("unexpected error while fetch user count while initializing base api handler", zap.Error(err))
|
||||
}
|
||||
if len(users) > 0 {
|
||||
aH.SetupCompleted = true
|
||||
}
|
||||
}
|
||||
|
||||
aH.Upgrader = &websocket.Upgrader{
|
||||
@ -583,32 +586,29 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
router.HandleFunc("/api/v1/orgs/me/filters", am.AdminAccess(aH.QuickFilters.UpdateQuickFilters)).Methods(http.MethodPut)
|
||||
|
||||
// === Authentication APIs ===
|
||||
router.HandleFunc("/api/v1/invite", am.AdminAccess(aH.inviteUser)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/invite/bulk", am.AdminAccess(aH.inviteUsers)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(aH.getInvite)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/invite/{email}", am.AdminAccess(aH.revokeInvite)).Methods(http.MethodDelete)
|
||||
router.HandleFunc("/api/v1/invite", am.AdminAccess(aH.listPendingInvites)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/invite", am.AdminAccess(aH.Signoz.Handlers.User.CreateInvite)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/invite/bulk", am.AdminAccess(aH.Signoz.Handlers.User.CreateBulkInvite)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(aH.Signoz.Handlers.User.GetInvite)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/invite/{id}", am.AdminAccess(aH.Signoz.Handlers.User.DeleteInvite)).Methods(http.MethodDelete)
|
||||
router.HandleFunc("/api/v1/invite", am.AdminAccess(aH.Signoz.Handlers.User.ListInvite)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/invite/accept", am.OpenAccess(aH.Signoz.Handlers.User.AcceptInvite)).Methods(http.MethodPost)
|
||||
|
||||
router.HandleFunc("/api/v1/register", am.OpenAccess(aH.registerUser)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/login", am.OpenAccess(aH.loginUser)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/loginPrecheck", am.OpenAccess(aH.precheckLogin)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/login", am.OpenAccess(aH.Signoz.Handlers.User.Login)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/loginPrecheck", am.OpenAccess(aH.Signoz.Handlers.User.LoginPrecheck)).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc("/api/v1/user", am.AdminAccess(aH.listUsers)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/user/{id}", am.SelfAccess(aH.getUser)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/user/{id}", am.SelfAccess(aH.editUser)).Methods(http.MethodPut)
|
||||
router.HandleFunc("/api/v1/user/{id}", am.AdminAccess(aH.deleteUser)).Methods(http.MethodDelete)
|
||||
|
||||
router.HandleFunc("/api/v1/rbac/role/{id}", am.SelfAccess(aH.getRole)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/rbac/role/{id}", am.AdminAccess(aH.editRole)).Methods(http.MethodPut)
|
||||
|
||||
router.HandleFunc("/api/v1/orgUsers/{id}", am.AdminAccess(aH.getOrgUsers)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/user", am.AdminAccess(aH.Signoz.Handlers.User.ListUsers)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/user/me", am.OpenAccess(aH.Signoz.Handlers.User.GetCurrentUserFromJWT)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/user/{id}", am.SelfAccess(aH.Signoz.Handlers.User.GetUser)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/user/{id}", am.SelfAccess(aH.Signoz.Handlers.User.UpdateUser)).Methods(http.MethodPut)
|
||||
router.HandleFunc("/api/v1/user/{id}", am.AdminAccess(aH.Signoz.Handlers.User.DeleteUser)).Methods(http.MethodDelete)
|
||||
|
||||
router.HandleFunc("/api/v2/orgs/me", am.AdminAccess(aH.Signoz.Handlers.Organization.Get)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v2/orgs/me", am.AdminAccess(aH.Signoz.Handlers.Organization.Update)).Methods(http.MethodPut)
|
||||
|
||||
router.HandleFunc("/api/v1/getResetPasswordToken/{id}", am.AdminAccess(aH.getResetPasswordToken)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/resetPassword", am.OpenAccess(aH.resetPassword)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/changePassword/{id}", am.SelfAccess(aH.changePassword)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/getResetPasswordToken/{id}", am.AdminAccess(aH.Signoz.Handlers.User.GetResetPasswordToken)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/resetPassword", am.OpenAccess(aH.Signoz.Handlers.User.ResetPassword)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/changePassword/{id}", am.SelfAccess(aH.Signoz.Handlers.User.ChangePassword)).Methods(http.MethodPost)
|
||||
|
||||
router.HandleFunc("/api/v3/licenses", am.ViewAccess(func(rw http.ResponseWriter, req *http.Request) {
|
||||
render.Success(rw, http.StatusOK, []any{})
|
||||
@ -2016,451 +2016,31 @@ func (aH *APIHandler) getHealth(w http.ResponseWriter, r *http.Request) {
|
||||
aH.WriteJSON(w, r, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
// inviteUser is used to invite a user. It is used by an admin api.
|
||||
func (aH *APIHandler) inviteUser(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := parseInviteRequest(r)
|
||||
if aH.HandleError(w, err, http.StatusBadRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := auth.Invite(r.Context(), req)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
aH.WriteJSON(w, r, resp)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) inviteUsers(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
req, err := parseInviteUsersRequest(r)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
response, err := auth.InviteUsers(ctx, req)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
// Check the response status and set the appropriate HTTP status code
|
||||
if response.Status == "failure" {
|
||||
w.WriteHeader(http.StatusBadRequest) // 400 Bad Request for failure
|
||||
} else if response.Status == "partial_success" {
|
||||
w.WriteHeader(http.StatusPartialContent) // 206 Partial Content
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK) // 200 OK for success
|
||||
}
|
||||
|
||||
aH.WriteJSON(w, r, response)
|
||||
}
|
||||
|
||||
// getInvite returns the invite object details for the given invite token. We do not need to
|
||||
// protect this API because invite token itself is meant to be private.
|
||||
func (aH *APIHandler) getInvite(w http.ResponseWriter, r *http.Request) {
|
||||
token := mux.Vars(r)["token"]
|
||||
|
||||
resp, err := auth.GetInvite(context.Background(), token, aH.Signoz.Modules.Organization)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Err: err, Typ: model.ErrorNotFound}, nil)
|
||||
return
|
||||
}
|
||||
aH.WriteJSON(w, r, resp)
|
||||
}
|
||||
|
||||
// revokeInvite is used to revoke an invite.
|
||||
func (aH *APIHandler) revokeInvite(w http.ResponseWriter, r *http.Request) {
|
||||
email := mux.Vars(r)["email"]
|
||||
|
||||
if err := auth.RevokeInvite(r.Context(), email); err != nil {
|
||||
RespondError(w, &model.ApiError{Err: err, Typ: model.ErrorInternal}, nil)
|
||||
return
|
||||
}
|
||||
aH.WriteJSON(w, r, map[string]string{"data": "invite revoked successfully"})
|
||||
}
|
||||
|
||||
// listPendingInvites is used to list the pending invites.
|
||||
func (aH *APIHandler) listPendingInvites(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
claims, errv2 := authtypes.ClaimsFromContext(ctx)
|
||||
if errv2 != nil {
|
||||
render.Error(w, errv2)
|
||||
return
|
||||
}
|
||||
invites, err := dao.DB().GetInvites(ctx, claims.OrgID)
|
||||
if err != nil {
|
||||
RespondError(w, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(Ahsan): Querying org name based on orgId for each invite is not a good idea. Either
|
||||
// we should include org name field in the invite table, or do a join query.
|
||||
var resp []*model.InvitationResponseObject
|
||||
for _, inv := range invites {
|
||||
orgID, err := valuer.NewUUID(inv.OrgID)
|
||||
if err != nil {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, "invalid org_id in the invite"))
|
||||
}
|
||||
org, err := aH.Signoz.Modules.Organization.Get(ctx, orgID)
|
||||
if err != nil {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeInternal, errorsV2.CodeInternal, err.Error()))
|
||||
}
|
||||
resp = append(resp, &model.InvitationResponseObject{
|
||||
Name: inv.Name,
|
||||
Email: inv.Email,
|
||||
Token: inv.Token,
|
||||
CreatedAt: inv.CreatedAt.Unix(),
|
||||
Role: inv.Role,
|
||||
Organization: org.Name,
|
||||
})
|
||||
}
|
||||
aH.WriteJSON(w, r, resp)
|
||||
}
|
||||
|
||||
// Register extends registerUser for non-internal packages
|
||||
func (aH *APIHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
aH.registerUser(w, r)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := parseRegisterRequest(r)
|
||||
if aH.HandleError(w, err, http.StatusBadRequest) {
|
||||
if aH.SetupCompleted {
|
||||
RespondError(w, &model.ApiError{Err: errors.New("self-registration is disabled"), Typ: model.ErrorBadData}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
_, apiErr := auth.Register(context.Background(), req, aH.Signoz.Alertmanager, aH.Signoz.Modules.Organization, aH.QuickFilterModule)
|
||||
var req types.PostableRegisterOrgAndAdmin
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
RespondError(w, &model.ApiError{Err: err, Typ: model.ErrorBadData}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
_, apiErr := auth.Register(context.Background(), &req, aH.Signoz.Alertmanager, aH.Signoz.Modules.Organization, aH.Signoz.Modules.User, aH.QuickFilterModule)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if !aH.SetupCompleted {
|
||||
// since the first user is now created, we can disable self-registration as
|
||||
// from here onwards, we expect admin (owner) to invite other users.
|
||||
aH.SetupCompleted = true
|
||||
}
|
||||
// since the first user is now created, we can disable self-registration as
|
||||
// from here onwards, we expect admin (owner) to invite other users.
|
||||
aH.SetupCompleted = true
|
||||
|
||||
aH.Respond(w, nil)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) precheckLogin(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
email := r.URL.Query().Get("email")
|
||||
sourceUrl := r.URL.Query().Get("ref")
|
||||
|
||||
resp, apierr := aH.appDao.PrecheckLogin(context.Background(), email, sourceUrl)
|
||||
if apierr != nil {
|
||||
RespondError(w, apierr, resp)
|
||||
return
|
||||
}
|
||||
|
||||
aH.Respond(w, resp)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) loginUser(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := parseLoginRequest(r)
|
||||
if aH.HandleError(w, err, http.StatusBadRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
// c, err := r.Cookie("refresh-token")
|
||||
// if err != nil {
|
||||
// if err != http.ErrNoCookie {
|
||||
// w.WriteHeader(http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
// if c != nil {
|
||||
// req.RefreshToken = c.Value
|
||||
// }
|
||||
|
||||
resp, err := auth.Login(context.Background(), req, aH.JWT)
|
||||
if aH.HandleError(w, err, http.StatusUnauthorized) {
|
||||
return
|
||||
}
|
||||
|
||||
// http.SetCookie(w, &http.Cookie{
|
||||
// Name: "refresh-token",
|
||||
// Value: resp.RefreshJwt,
|
||||
// Expires: time.Unix(resp.RefreshJwtExpiry, 0),
|
||||
// HttpOnly: true,
|
||||
// })
|
||||
|
||||
aH.WriteJSON(w, r, resp)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) listUsers(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := dao.DB().GetUsers(context.Background())
|
||||
if err != nil {
|
||||
zap.L().Error("[listUsers] Failed to query list of users", zap.Error(err))
|
||||
RespondError(w, err, nil)
|
||||
return
|
||||
}
|
||||
// mask the password hash
|
||||
for i := range users {
|
||||
users[i].Password = ""
|
||||
}
|
||||
aH.WriteJSON(w, r, users)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) getUser(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
ctx := context.Background()
|
||||
user, err := dao.DB().GetUser(ctx, id)
|
||||
if err != nil {
|
||||
zap.L().Error("[getUser] Failed to query user", zap.Error(err))
|
||||
RespondError(w, err, "Failed to get user")
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
RespondError(w, &model.ApiError{
|
||||
Typ: model.ErrorInternal,
|
||||
Err: errors.New("user not found"),
|
||||
}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// No need to send password hash for the user object.
|
||||
user.Password = ""
|
||||
aH.WriteJSON(w, r, user)
|
||||
}
|
||||
|
||||
// editUser only changes the user's Name and ProfilePictureURL. It is intentionally designed
|
||||
// to not support update of orgId, Password, createdAt for the sucurity reasons.
|
||||
func (aH *APIHandler) editUser(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
update, err := parseUserRequest(r)
|
||||
if aH.HandleError(w, err, http.StatusBadRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
old, apiErr := dao.DB().GetUser(ctx, id)
|
||||
if apiErr != nil {
|
||||
zap.L().Error("[editUser] Failed to query user", zap.Error(err))
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if len(update.Name) > 0 {
|
||||
old.Name = update.Name
|
||||
}
|
||||
if len(update.ProfilePictureURL) > 0 {
|
||||
old.ProfilePictureURL = update.ProfilePictureURL
|
||||
}
|
||||
|
||||
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(old.Email)) {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, "integration user cannot be updated"))
|
||||
return
|
||||
}
|
||||
|
||||
_, apiErr = dao.DB().EditUser(ctx, &types.User{
|
||||
ID: old.ID,
|
||||
Name: old.Name,
|
||||
OrgID: old.OrgID,
|
||||
Email: old.Email,
|
||||
Password: old.Password,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: old.CreatedAt,
|
||||
},
|
||||
ProfilePictureURL: old.ProfilePictureURL,
|
||||
})
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
}
|
||||
aH.WriteJSON(w, r, map[string]string{"data": "user updated successfully"})
|
||||
}
|
||||
|
||||
func (aH *APIHandler) deleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
// Query for the user's group, and the admin's group. If the user belongs to the admin group
|
||||
// and is the last user then don't let the deletion happen. Otherwise, the system will become
|
||||
// admin less and hence inaccessible.
|
||||
ctx := context.Background()
|
||||
user, apiErr := dao.DB().GetUser(ctx, id)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, "Failed to get user's group")
|
||||
return
|
||||
}
|
||||
|
||||
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(user.Email)) {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, "integration user cannot be updated"))
|
||||
return
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
RespondError(w, &model.ApiError{
|
||||
Typ: model.ErrorNotFound,
|
||||
Err: errors.New("no user found"),
|
||||
}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
adminUsers, apiErr := dao.DB().GetUsersByRole(ctx, authtypes.RoleAdmin)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, "Failed to get admin group users")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Role == authtypes.RoleAdmin.String() && len(adminUsers) == 1 {
|
||||
RespondError(w, &model.ApiError{
|
||||
Typ: model.ErrorInternal,
|
||||
Err: errors.New("cannot delete the last admin user")}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
err := dao.DB().DeleteUser(ctx, id)
|
||||
if err != nil {
|
||||
RespondError(w, err, "Failed to delete user")
|
||||
return
|
||||
}
|
||||
|
||||
aH.WriteJSON(w, r, map[string]string{"data": "user deleted successfully"})
|
||||
}
|
||||
|
||||
func (aH *APIHandler) getRole(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
user, err := dao.DB().GetUser(context.Background(), id)
|
||||
if err != nil {
|
||||
RespondError(w, err, "Failed to get user's group")
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
RespondError(w, &model.ApiError{
|
||||
Typ: model.ErrorNotFound,
|
||||
Err: errors.New("no user found"),
|
||||
}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
aH.WriteJSON(w, r, &model.UserRole{UserId: id, GroupName: user.Role})
|
||||
}
|
||||
|
||||
func (aH *APIHandler) editRole(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
|
||||
req, err := parseUserRoleRequest(r)
|
||||
if aH.HandleError(w, err, http.StatusBadRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
role, err := authtypes.NewRole(req.GroupName)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: errors.New("invalid role")}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
user, apiErr := dao.DB().GetUser(ctx, id)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, "Failed to fetch user group")
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure that the request is not demoting the last admin user.
|
||||
if user.Role == authtypes.RoleAdmin.String() {
|
||||
adminUsers, apiErr := dao.DB().GetUsersByRole(ctx, authtypes.RoleAdmin)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, "Failed to fetch adminUsers")
|
||||
return
|
||||
}
|
||||
|
||||
if len(adminUsers) == 1 {
|
||||
RespondError(w, &model.ApiError{
|
||||
Err: errors.New("cannot demote the last admin"),
|
||||
Typ: model.ErrorInternal}, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
apiErr = dao.DB().UpdateUserRole(context.Background(), user.ID, role)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, "Failed to add user to group")
|
||||
return
|
||||
}
|
||||
aH.WriteJSON(w, r, map[string]string{"data": "user group updated successfully"})
|
||||
}
|
||||
|
||||
func (aH *APIHandler) getOrgUsers(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
users, apiErr := dao.DB().GetUsersByOrg(context.Background(), id)
|
||||
if apiErr != nil {
|
||||
RespondError(w, apiErr, "Failed to fetch org users from the DB")
|
||||
return
|
||||
}
|
||||
// mask the password hash
|
||||
for i := range users {
|
||||
users[i].Password = ""
|
||||
}
|
||||
aH.WriteJSON(w, r, users)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) getResetPasswordToken(w http.ResponseWriter, r *http.Request) {
|
||||
id := mux.Vars(r)["id"]
|
||||
resp, err := auth.CreateResetPasswordToken(context.Background(), id)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{
|
||||
Typ: model.ErrorInternal,
|
||||
Err: err}, "Failed to create reset token entry in the DB")
|
||||
return
|
||||
}
|
||||
aH.WriteJSON(w, r, resp)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) resetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := parseResetPasswordRequest(r)
|
||||
if aH.HandleError(w, err, http.StatusBadRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := auth.ResetPassword(context.Background(), req); err != nil {
|
||||
zap.L().Error("resetPassword failed", zap.Error(err))
|
||||
if aH.HandleError(w, err, http.StatusInternalServerError) {
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
aH.WriteJSON(w, r, map[string]string{"data": "password reset successfully"})
|
||||
}
|
||||
|
||||
func (aH *APIHandler) changePassword(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := parseChangePasswordRequest(r)
|
||||
if aH.HandleError(w, err, http.StatusBadRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
if apiErr := auth.ChangePassword(context.Background(), req); apiErr != nil {
|
||||
RespondError(w, apiErr, nil)
|
||||
return
|
||||
|
||||
}
|
||||
aH.WriteJSON(w, r, map[string]string{"data": "password changed successfully"})
|
||||
}
|
||||
|
||||
// func (aH *APIHandler) getApplicationPercentiles(w http.ResponseWriter, r *http.Request) {
|
||||
// // vars := mux.Vars(r)
|
||||
|
||||
// query, err := parseApplicationPercentileRequest(r)
|
||||
// if aH.HandleError(w, err, http.StatusBadRequest) {
|
||||
// return
|
||||
// }
|
||||
|
||||
// result, err := aH.reader.GetApplicationPercentiles(context.Background(), query)
|
||||
// if aH.HandleError(w, err, http.StatusBadRequest) {
|
||||
// return
|
||||
// }
|
||||
// aH.WriteJSON(w, r, result)
|
||||
// }
|
||||
|
||||
func (aH *APIHandler) HandleError(w http.ResponseWriter, err error, statusCode int) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -16,7 +17,8 @@ func TestIntegrationLifecycle(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
organizationModule := implorganization.NewModule(implorganization.NewStore(store))
|
||||
user, apiErr := createTestUser(organizationModule)
|
||||
userModule := impluser.NewModule(impluser.NewStore(store))
|
||||
user, apiErr := createTestUser(organizationModule, userModule)
|
||||
if apiErr != nil {
|
||||
t.Fatalf("could not create test user: %v", apiErr)
|
||||
}
|
||||
|
@ -6,16 +6,14 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/dao"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func NewTestIntegrationsManager(t *testing.T) (*Manager, sqlstore.SQLStore) {
|
||||
@ -32,7 +30,7 @@ func NewTestIntegrationsManager(t *testing.T) (*Manager, sqlstore.SQLStore) {
|
||||
}, testDB
|
||||
}
|
||||
|
||||
func createTestUser(organizationModule organization.Module) (*types.User, *model.ApiError) {
|
||||
func createTestUser(organizationModule organization.Module, userModule user.Module) (*types.User, *model.ApiError) {
|
||||
// Create a test user for auth
|
||||
ctx := context.Background()
|
||||
organization := types.NewOrganization("test")
|
||||
@ -41,19 +39,21 @@ func createTestUser(organizationModule organization.Module) (*types.User, *model
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
|
||||
userId := uuid.NewString()
|
||||
return dao.DB().CreateUser(
|
||||
ctx,
|
||||
&types.User{
|
||||
ID: userId,
|
||||
Name: "test",
|
||||
Email: userId[:8] + "test@test.com",
|
||||
Password: "test",
|
||||
OrgID: organization.ID.StringValue(),
|
||||
Role: authtypes.RoleAdmin.String(),
|
||||
},
|
||||
true,
|
||||
)
|
||||
random, err := utils.RandomHex(3)
|
||||
if err != nil {
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
|
||||
user, err := types.NewUser("test", random+"test@test.com", types.RoleAdmin.String(), organization.ID.StringValue())
|
||||
if err != nil {
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
|
||||
err = userModule.CreateUser(ctx, user)
|
||||
if err != nil {
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
type TestAvailableIntegrationsRepo struct{}
|
||||
|
@ -16,7 +16,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations/messagingQueues/kafka"
|
||||
queues2 "github.com/SigNoz/signoz/pkg/query-service/app/integrations/messagingQueues/queues"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations/thirdPartyApi"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
|
||||
"github.com/SigNoz/govaluate"
|
||||
"github.com/gorilla/mux"
|
||||
@ -26,7 +25,6 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/metrics"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/queryBuilder"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/auth"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/common"
|
||||
baseconstants "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
@ -474,54 +472,6 @@ func parseGetTTL(r *http.Request) (*model.GetTTLParams, error) {
|
||||
return &model.GetTTLParams{Type: typeTTL}, nil
|
||||
}
|
||||
|
||||
func parseUserRequest(r *http.Request) (*types.User, error) {
|
||||
var req types.User
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func parseInviteRequest(r *http.Request) (*model.InviteRequest, error) {
|
||||
var req model.InviteRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Trim spaces from email
|
||||
req.Email = strings.TrimSpace(req.Email)
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func parseInviteUsersRequest(r *http.Request) (*model.BulkInviteRequest, error) {
|
||||
var req model.BulkInviteRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate that the request contains users
|
||||
if len(req.Users) == 0 {
|
||||
return nil, fmt.Errorf("no users provided for invitation")
|
||||
}
|
||||
|
||||
// Trim spaces and validate each user
|
||||
for i := range req.Users {
|
||||
req.Users[i].Email = strings.TrimSpace(req.Users[i].Email)
|
||||
if req.Users[i].Email == "" {
|
||||
return nil, fmt.Errorf("email is required for each user")
|
||||
}
|
||||
if req.Users[i].FrontendBaseUrl == "" {
|
||||
return nil, fmt.Errorf("frontendBaseUrl is required for each user")
|
||||
}
|
||||
|
||||
_, err := authtypes.NewRole(req.Users[i].Role)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid role for user: %s", req.Users[i].Email)
|
||||
}
|
||||
}
|
||||
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func parseSetApdexScoreRequest(r *http.Request) (*types.ApdexSettings, error) {
|
||||
var req types.ApdexSettings
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
@ -530,72 +480,6 @@ func parseSetApdexScoreRequest(r *http.Request) (*types.ApdexSettings, error) {
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func parseRegisterRequest(r *http.Request) (*auth.RegisterRequest, error) {
|
||||
var req auth.RegisterRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := auth.ValidatePassword(req.Password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func parseLoginRequest(r *http.Request) (*model.LoginRequest, error) {
|
||||
var req model.LoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func parseUserRoleRequest(r *http.Request) (*model.UserRole, error) {
|
||||
var req model.UserRole
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func parseEditOrgRequest(r *http.Request) (*types.Organization, error) {
|
||||
var req types.Organization
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func parseResetPasswordRequest(r *http.Request) (*model.ResetPasswordRequest, error) {
|
||||
var req model.ResetPasswordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := auth.ValidatePassword(req.Password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func parseChangePasswordRequest(r *http.Request) (*model.ChangePasswordRequest, error) {
|
||||
id := mux.Vars(r)["id"]
|
||||
var req model.ChangePasswordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.UserId = id
|
||||
if err := auth.ValidatePassword(req.NewPassword); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func parseAggregateAttributeRequest(r *http.Request) (*v3.AggregateAttributeRequest, error) {
|
||||
var req v3.AggregateAttributeRequest
|
||||
|
||||
|
@ -149,6 +149,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
}
|
||||
|
||||
telemetry.GetInstance().SetReader(reader)
|
||||
telemetry.GetInstance().SetSqlStore(serverOptions.SigNoz.SQLStore)
|
||||
quickfiltermodule := quickfilterscore.NewQuickFilters(quickfilterscore.NewStore(serverOptions.SigNoz.SQLStore))
|
||||
quickFilter := quickfilter.NewAPI(quickfiltermodule)
|
||||
apiHandler, err := NewAPIHandler(APIHandlerOpts{
|
||||
|
@ -1,405 +1,19 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||
"os"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/dao"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
smtpservice "github.com/SigNoz/signoz/pkg/query-service/utils/smtpService"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
)
|
||||
|
||||
const (
|
||||
opaqueTokenSize = 16
|
||||
minimumPasswordLength = 8
|
||||
)
|
||||
|
||||
var (
|
||||
ErrorInvalidCreds = fmt.Errorf("invalid credentials")
|
||||
ErrorEmptyRequest = errors.New("Empty request")
|
||||
ErrorInvalidRole = errors.New("Invalid role")
|
||||
ErrorInvalidInviteToken = errors.New("Invalid invite token")
|
||||
ErrorAskAdmin = errors.New("An invitation is needed to create an account. Please ask your admin (the person who has first installed SIgNoz) to send an invite.")
|
||||
)
|
||||
|
||||
type InviteEmailData struct {
|
||||
CustomerName string
|
||||
InviterName string
|
||||
InviterEmail string
|
||||
Link string
|
||||
}
|
||||
|
||||
// The root user should be able to invite people to create account on SigNoz cluster.
|
||||
func Invite(ctx context.Context, req *model.InviteRequest) (*model.InviteResponse, error) {
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := utils.RandomHex(opaqueTokenSize)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to generate invite token")
|
||||
}
|
||||
|
||||
user, apiErr := dao.DB().GetUserByEmail(ctx, req.Email)
|
||||
if apiErr != nil {
|
||||
return nil, errors.Wrap(apiErr.Err, "Failed to check already existing user")
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
return nil, errors.New("User already exists with the same email")
|
||||
}
|
||||
// Check if an invite already exists
|
||||
invite, apiErr := dao.DB().GetInviteFromEmail(ctx, req.Email)
|
||||
if apiErr != nil {
|
||||
return nil, errors.Wrap(apiErr.Err, "Failed to check existing invite")
|
||||
}
|
||||
|
||||
if invite != nil {
|
||||
return nil, errors.New("An invite already exists for this email")
|
||||
}
|
||||
|
||||
role, err := authtypes.NewRole(req.Role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
au, apiErr := dao.DB().GetUser(ctx, claims.UserID)
|
||||
if apiErr != nil {
|
||||
return nil, errors.Wrap(err, "failed to query admin user from the DB")
|
||||
}
|
||||
|
||||
inv := &types.Invite{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
Token: token,
|
||||
Role: role.String(),
|
||||
OrgID: au.OrgID,
|
||||
}
|
||||
|
||||
if err := dao.DB().CreateInviteEntry(ctx, inv); err != nil {
|
||||
return nil, errors.Wrap(err.Err, "failed to write to DB")
|
||||
}
|
||||
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_USER_INVITATION_SENT, map[string]interface{}{
|
||||
"invited user email": req.Email,
|
||||
}, au.Email, true, false)
|
||||
|
||||
// send email if SMTP is enabled
|
||||
if os.Getenv("SMTP_ENABLED") == "true" && req.FrontendBaseUrl != "" {
|
||||
inviteEmail(req, au, token)
|
||||
}
|
||||
|
||||
return &model.InviteResponse{Email: inv.Email, InviteToken: inv.Token}, nil
|
||||
}
|
||||
|
||||
func InviteUsers(ctx context.Context, req *model.BulkInviteRequest) (*model.BulkInviteResponse, error) {
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &model.BulkInviteResponse{
|
||||
Status: "success",
|
||||
Summary: model.InviteSummary{TotalInvites: len(req.Users)},
|
||||
SuccessfulInvites: []model.SuccessfulInvite{},
|
||||
FailedInvites: []model.FailedInvite{},
|
||||
}
|
||||
|
||||
au, apiErr := dao.DB().GetUser(ctx, claims.UserID)
|
||||
if apiErr != nil {
|
||||
return nil, errors.Wrap(apiErr.Err, "failed to query admin user from the DB")
|
||||
}
|
||||
|
||||
for _, inviteReq := range req.Users {
|
||||
inviteResp, err := inviteUser(ctx, &inviteReq, au)
|
||||
if err != nil {
|
||||
response.FailedInvites = append(response.FailedInvites, model.FailedInvite{
|
||||
Email: inviteReq.Email,
|
||||
Error: err.Error(),
|
||||
})
|
||||
response.Summary.FailedInvites++
|
||||
} else {
|
||||
response.SuccessfulInvites = append(response.SuccessfulInvites, model.SuccessfulInvite{
|
||||
Email: inviteResp.Email,
|
||||
InviteLink: fmt.Sprintf("%s/signup?token=%s", inviteReq.FrontendBaseUrl, inviteResp.InviteToken),
|
||||
Status: "sent",
|
||||
})
|
||||
response.Summary.SuccessfulInvites++
|
||||
}
|
||||
}
|
||||
|
||||
// Update the status based on the results
|
||||
if response.Summary.FailedInvites == response.Summary.TotalInvites {
|
||||
response.Status = "failure"
|
||||
} else if response.Summary.FailedInvites > 0 {
|
||||
response.Status = "partial_success"
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Helper function to handle individual invites
|
||||
func inviteUser(ctx context.Context, req *model.InviteRequest, au *types.GettableUser) (*model.InviteResponse, error) {
|
||||
token, err := utils.RandomHex(opaqueTokenSize)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to generate invite token")
|
||||
}
|
||||
|
||||
user, apiErr := dao.DB().GetUserByEmail(ctx, req.Email)
|
||||
if apiErr != nil {
|
||||
return nil, errors.Wrap(apiErr.Err, "Failed to check already existing user")
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
return nil, errors.New("User already exists with the same email")
|
||||
}
|
||||
|
||||
// Check if an invite already exists
|
||||
invite, apiErr := dao.DB().GetInviteFromEmail(ctx, req.Email)
|
||||
if apiErr != nil {
|
||||
return nil, errors.Wrap(apiErr.Err, "Failed to check existing invite")
|
||||
}
|
||||
|
||||
if invite != nil {
|
||||
return nil, errors.New("An invite already exists for this email")
|
||||
}
|
||||
|
||||
role, err := authtypes.NewRole(req.Role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inv := &types.Invite{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
Token: token,
|
||||
Role: role.String(),
|
||||
OrgID: au.OrgID,
|
||||
}
|
||||
|
||||
if err := dao.DB().CreateInviteEntry(ctx, inv); err != nil {
|
||||
return nil, errors.Wrap(err.Err, "failed to write to DB")
|
||||
}
|
||||
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_USER_INVITATION_SENT, map[string]interface{}{
|
||||
"invited user email": req.Email,
|
||||
}, au.Email, true, false)
|
||||
|
||||
// send email if SMTP is enabled
|
||||
if os.Getenv("SMTP_ENABLED") == "true" && req.FrontendBaseUrl != "" {
|
||||
inviteEmail(req, au, token)
|
||||
}
|
||||
|
||||
return &model.InviteResponse{Email: inv.Email, InviteToken: inv.Token}, nil
|
||||
}
|
||||
|
||||
func inviteEmail(req *model.InviteRequest, au *types.GettableUser, token string) {
|
||||
smtp := smtpservice.GetInstance()
|
||||
data := InviteEmailData{
|
||||
CustomerName: req.Name,
|
||||
InviterName: au.Name,
|
||||
InviterEmail: au.Email,
|
||||
Link: fmt.Sprintf("%s/signup?token=%s", req.FrontendBaseUrl, token),
|
||||
}
|
||||
|
||||
tmpl, err := template.ParseFiles(constants.InviteEmailTemplate)
|
||||
if err != nil {
|
||||
zap.L().Error("failed to send email", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
var body bytes.Buffer
|
||||
if err := tmpl.Execute(&body, data); err != nil {
|
||||
zap.L().Error("failed to send email", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
err = smtp.SendEmail(
|
||||
req.Email,
|
||||
au.Name+" has invited you to their team in SigNoz",
|
||||
body.String(),
|
||||
)
|
||||
if err != nil {
|
||||
zap.L().Error("failed to send email", zap.Error(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// RevokeInvite is used to revoke the invitation for the given email.
|
||||
func RevokeInvite(ctx context.Context, email string) error {
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := dao.DB().DeleteInvitation(ctx, claims.OrgID, email); err != nil {
|
||||
return errors.Wrap(err.Err, "failed to write to DB")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInvite returns an invitation object for the given token.
|
||||
func GetInvite(ctx context.Context, token string, organizationModule organization.Module) (*model.InvitationResponseObject, error) {
|
||||
zap.L().Debug("GetInvite method invoked for token", zap.String("token", token))
|
||||
|
||||
inv, apiErr := dao.DB().GetInviteFromToken(ctx, token)
|
||||
if apiErr != nil {
|
||||
return nil, errors.Wrap(apiErr.Err, "failed to query the DB")
|
||||
}
|
||||
|
||||
if inv == nil {
|
||||
return nil, errors.New("user is not invited")
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(inv.OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
org, err := organizationModule.Get(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to query the DB")
|
||||
}
|
||||
return &model.InvitationResponseObject{
|
||||
Name: inv.Name,
|
||||
Email: inv.Email,
|
||||
Token: inv.Token,
|
||||
CreatedAt: inv.CreatedAt.Unix(),
|
||||
Role: inv.Role,
|
||||
Organization: org.DisplayName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ValidateInvite(ctx context.Context, req *RegisterRequest) (*types.Invite, error) {
|
||||
invitation, err := dao.DB().GetInviteFromEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err.Err, "Failed to read from DB")
|
||||
}
|
||||
|
||||
if invitation == nil {
|
||||
return nil, ErrorAskAdmin
|
||||
}
|
||||
|
||||
if invitation.Token != req.InviteToken {
|
||||
return nil, ErrorInvalidInviteToken
|
||||
}
|
||||
|
||||
return invitation, nil
|
||||
}
|
||||
|
||||
func CreateResetPasswordToken(ctx context.Context, userId string) (*types.ResetPasswordRequest, error) {
|
||||
token, err := utils.RandomHex(opaqueTokenSize)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to generate reset password token")
|
||||
}
|
||||
|
||||
req := &types.ResetPasswordRequest{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
UserID: userId,
|
||||
Token: token,
|
||||
}
|
||||
if apiErr := dao.DB().CreateResetPasswordEntry(ctx, req); err != nil {
|
||||
return nil, errors.Wrap(apiErr.Err, "failed to write to DB")
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func ResetPassword(ctx context.Context, req *model.ResetPasswordRequest) error {
|
||||
entry, apiErr := dao.DB().GetResetPasswordEntry(ctx, req.Token)
|
||||
if apiErr != nil {
|
||||
return errors.Wrap(apiErr.Err, "failed to query the DB")
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
return errors.New("Invalid reset password request")
|
||||
}
|
||||
|
||||
hash, err := PasswordHash(req.Password)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to generate password hash")
|
||||
}
|
||||
|
||||
if apiErr := dao.DB().UpdateUserPassword(ctx, hash, entry.UserID); apiErr != nil {
|
||||
return apiErr.Err
|
||||
}
|
||||
|
||||
if apiErr := dao.DB().DeleteResetPasswordEntry(ctx, req.Token); apiErr != nil {
|
||||
return errors.Wrap(apiErr.Err, "failed to delete reset token from DB")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ChangePassword(ctx context.Context, req *model.ChangePasswordRequest) *model.ApiError {
|
||||
user, apiErr := dao.DB().GetUser(ctx, req.UserId)
|
||||
if apiErr != nil {
|
||||
return apiErr
|
||||
}
|
||||
|
||||
if user == nil || !passwordMatch(user.Password, req.OldPassword) {
|
||||
return model.ForbiddenError(ErrorInvalidCreds)
|
||||
}
|
||||
|
||||
hash, err := PasswordHash(req.NewPassword)
|
||||
if err != nil {
|
||||
return model.InternalError(errors.New("Failed to generate password hash"))
|
||||
}
|
||||
|
||||
if apiErr := dao.DB().UpdateUserPassword(ctx, hash, user.ID); apiErr != nil {
|
||||
return apiErr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Name string `json:"name"`
|
||||
OrgID string `json:"orgId"`
|
||||
OrgDisplayName string `json:"orgDisplayName"`
|
||||
OrgName string `json:"orgName"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
InviteToken string `json:"token"`
|
||||
|
||||
// reference URL to track where the register request is coming from
|
||||
SourceUrl string `json:"sourceUrl"`
|
||||
}
|
||||
|
||||
func RegisterFirstUser(ctx context.Context, req *RegisterRequest, organizationModule organization.Module) (*types.User, *model.ApiError) {
|
||||
func RegisterOrgAndFirstUser(ctx context.Context, req *types.PostableRegisterOrgAndAdmin, organizationModule organization.Module, userModule user.Module) (*types.User, *model.ApiError) {
|
||||
if req.Email == "" {
|
||||
return nil, model.BadRequest(model.ErrEmailRequired{})
|
||||
}
|
||||
@ -414,245 +28,38 @@ func RegisterFirstUser(ctx context.Context, req *RegisterRequest, organizationMo
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
|
||||
var hash string
|
||||
hash, err = PasswordHash(req.Password)
|
||||
user, err := types.NewUser(req.Name, req.Email, types.RoleAdmin.String(), organization.ID.StringValue())
|
||||
if err != nil {
|
||||
zap.L().Error("failed to generate password hash when registering a user", zap.Error(err))
|
||||
return nil, model.InternalError(model.ErrSignupFailed{})
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
|
||||
user := &types.User{
|
||||
ID: uuid.New().String(),
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
Password: hash,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
ProfilePictureURL: "", // Currently unused
|
||||
Role: authtypes.RoleAdmin.String(),
|
||||
OrgID: organization.ID.StringValue(),
|
||||
}
|
||||
|
||||
return dao.DB().CreateUser(ctx, user, true)
|
||||
}
|
||||
|
||||
// RegisterInvitedUser handles registering a invited user
|
||||
func RegisterInvitedUser(ctx context.Context, req *RegisterRequest, nopassword bool) (*types.User, *model.ApiError) {
|
||||
|
||||
if req.InviteToken == "" {
|
||||
return nil, model.BadRequest(ErrorAskAdmin)
|
||||
}
|
||||
|
||||
if !nopassword && req.Password == "" {
|
||||
return nil, model.BadRequest(model.ErrPasswordRequired{})
|
||||
}
|
||||
|
||||
invite, err := ValidateInvite(ctx, req)
|
||||
password, err := types.NewFactorPassword(req.Password)
|
||||
if err != nil {
|
||||
zap.L().Error("failed to validate invite token", zap.Error(err))
|
||||
return nil, model.BadRequest(model.ErrSignupFailed{})
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
|
||||
// checking if user email already exists, this is defensive but
|
||||
// required as delete invitation and user creation dont happen
|
||||
// in the same transaction at the end of this function
|
||||
userPayload, apierr := dao.DB().GetUserByEmail(ctx, invite.Email)
|
||||
if apierr != nil {
|
||||
zap.L().Error("failed to get user by email", zap.Error(apierr.Err))
|
||||
return nil, apierr
|
||||
user, err = userModule.CreateUserWithPassword(ctx, user, password)
|
||||
if err != nil {
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
|
||||
if userPayload != nil {
|
||||
// user already exists
|
||||
return &userPayload.User, nil
|
||||
}
|
||||
|
||||
if invite.OrgID == "" {
|
||||
zap.L().Error("failed to find org in the invite")
|
||||
return nil, model.InternalError(fmt.Errorf("invalid invite, org not found"))
|
||||
}
|
||||
|
||||
if invite.Role == "" {
|
||||
// if role is not provided, default to viewer
|
||||
invite.Role = authtypes.RoleViewer.String()
|
||||
}
|
||||
|
||||
var hash string
|
||||
|
||||
// check if password is not empty, as for SSO case it can be
|
||||
if req.Password != "" {
|
||||
hash, err = PasswordHash(req.Password)
|
||||
if err != nil {
|
||||
zap.L().Error("failed to generate password hash when registering a user", zap.Error(err))
|
||||
return nil, model.InternalError(model.ErrSignupFailed{})
|
||||
}
|
||||
} else {
|
||||
hash, err = PasswordHash(utils.GeneratePassowrd())
|
||||
if err != nil {
|
||||
zap.L().Error("failed to generate password hash when registering a user", zap.Error(err))
|
||||
return nil, model.InternalError(model.ErrSignupFailed{})
|
||||
}
|
||||
}
|
||||
|
||||
user := &types.User{
|
||||
ID: uuid.New().String(),
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
Password: hash,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
ProfilePictureURL: "", // Currently unused
|
||||
Role: invite.Role,
|
||||
OrgID: invite.OrgID,
|
||||
}
|
||||
|
||||
// TODO(Ahsan): Ideally create user and delete invitation should happen in a txn.
|
||||
user, apiErr := dao.DB().CreateUser(ctx, user, false)
|
||||
if apiErr != nil {
|
||||
zap.L().Error("CreateUser failed", zap.Error(apiErr.Err))
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
apiErr = dao.DB().DeleteInvitation(ctx, user.OrgID, user.Email)
|
||||
if apiErr != nil {
|
||||
zap.L().Error("delete invitation failed", zap.Error(apiErr.Err))
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
telemetry.GetInstance().IdentifyUser(user)
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_USER_INVITATION_ACCEPTED, nil, req.Email, true, false)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Register registers a new user. For the first register request, it doesn't need an invite token
|
||||
// and also the first registration is an enforced ADMIN registration. Every subsequent request will
|
||||
// need an invite token to go through.
|
||||
func Register(ctx context.Context, req *RegisterRequest, alertmanager alertmanager.Alertmanager, organizationModule organization.Module, quickfiltermodule quickfilter.Usecase) (*types.User, *model.ApiError) {
|
||||
users, err := dao.DB().GetUsers(ctx)
|
||||
// First user registration
|
||||
func Register(ctx context.Context, req *types.PostableRegisterOrgAndAdmin, alertmanager alertmanager.Alertmanager, organizationModule organization.Module, userModule user.Module, quickfiltermodule quickfilter.Usecase) (*types.User, *model.ApiError) {
|
||||
user, err := RegisterOrgAndFirstUser(ctx, req, organizationModule, userModule)
|
||||
if err != nil {
|
||||
return nil, model.InternalError(fmt.Errorf("failed to get user count"))
|
||||
}
|
||||
|
||||
switch len(users) {
|
||||
case 0:
|
||||
user, err := RegisterFirstUser(ctx, req, organizationModule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := alertmanager.SetDefaultConfig(ctx, user.OrgID); err != nil {
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
if err := quickfiltermodule.SetDefaultConfig(ctx, valuer.MustNewUUID(user.OrgID)); err != nil {
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
return user, nil
|
||||
default:
|
||||
return RegisterInvitedUser(ctx, req, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Login method returns access and refresh tokens on successful login, else it errors out.
|
||||
func Login(ctx context.Context, request *model.LoginRequest, jwt *authtypes.JWT) (*model.LoginResponse, error) {
|
||||
user, err := authenticateLogin(ctx, request, jwt)
|
||||
if err != nil {
|
||||
zap.L().Error("Failed to authenticate login request", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userjwt, err := GenerateJWTForUser(&user.User, jwt)
|
||||
if err != nil {
|
||||
zap.L().Error("Failed to generate JWT against login creds", zap.Error(err))
|
||||
return nil, err
|
||||
if err := alertmanager.SetDefaultConfig(ctx, user.OrgID); err != nil {
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
|
||||
// ignoring identity for unnamed users as a patch for #3863
|
||||
if user.Name != "" {
|
||||
telemetry.GetInstance().IdentifyUser(&user.User)
|
||||
if err := quickfiltermodule.SetDefaultConfig(ctx, valuer.MustNewUUID(user.OrgID)); err != nil {
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
|
||||
return &model.LoginResponse{
|
||||
UserJwtObject: userjwt,
|
||||
UserId: user.User.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// authenticateLogin is responsible for querying the DB and validating the credentials.
|
||||
func authenticateLogin(ctx context.Context, req *model.LoginRequest, jwt *authtypes.JWT) (*types.GettableUser, error) {
|
||||
// If refresh token is valid, then simply authorize the login request.
|
||||
if len(req.RefreshToken) > 0 {
|
||||
// parse the refresh token
|
||||
claims, err := jwt.Claims(req.RefreshToken)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse refresh token")
|
||||
}
|
||||
|
||||
user := &types.GettableUser{
|
||||
User: types.User{
|
||||
ID: claims.UserID,
|
||||
Role: claims.Role.String(),
|
||||
Email: claims.Email,
|
||||
OrgID: claims.OrgID,
|
||||
},
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
user, err := dao.DB().GetUserByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err.Err, "user not found")
|
||||
}
|
||||
if user == nil || !passwordMatch(user.Password, req.Password) {
|
||||
return nil, ErrorInvalidCreds
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Generate hash from the password.
|
||||
func PasswordHash(pass string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
// Checks if the given password results in the given hash.
|
||||
func passwordMatch(hash, password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func GenerateJWTForUser(user *types.User, jwt *authtypes.JWT) (model.UserJwtObject, error) {
|
||||
role, err := authtypes.NewRole(user.Role)
|
||||
if err != nil {
|
||||
return model.UserJwtObject{}, err
|
||||
}
|
||||
|
||||
accessJwt, accessClaims, err := jwt.AccessToken(user.OrgID, user.ID, user.Email, role)
|
||||
if err != nil {
|
||||
return model.UserJwtObject{}, err
|
||||
}
|
||||
|
||||
refreshJwt, refreshClaims, err := jwt.RefreshToken(user.OrgID, user.ID, user.Email, role)
|
||||
if err != nil {
|
||||
return model.UserJwtObject{}, err
|
||||
}
|
||||
|
||||
return model.UserJwtObject{
|
||||
AccessJwt: accessJwt,
|
||||
RefreshJwt: refreshJwt,
|
||||
AccessJwtExpiry: accessClaims.ExpiresAt.Unix(),
|
||||
RefreshJwtExpiry: refreshClaims.ExpiresAt.Unix(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ValidatePassword(password string) error {
|
||||
if len(password) < minimumPasswordLength {
|
||||
return errors.Errorf("Password should be atleast %d characters.", minimumPasswordLength)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
)
|
||||
|
||||
type ModelDao interface {
|
||||
@ -14,37 +13,10 @@ type ModelDao interface {
|
||||
}
|
||||
|
||||
type Queries interface {
|
||||
GetInviteFromEmail(ctx context.Context, email string) (*types.Invite, *model.ApiError)
|
||||
GetInviteFromToken(ctx context.Context, token string) (*types.Invite, *model.ApiError)
|
||||
GetInvites(ctx context.Context, orgID string) ([]types.Invite, *model.ApiError)
|
||||
|
||||
GetUser(ctx context.Context, id string) (*types.GettableUser, *model.ApiError)
|
||||
GetUserByEmail(ctx context.Context, email string) (*types.GettableUser, *model.ApiError)
|
||||
GetUsers(ctx context.Context) ([]types.GettableUser, *model.ApiError)
|
||||
GetUsersWithOpts(ctx context.Context, limit int) ([]types.GettableUser, *model.ApiError)
|
||||
|
||||
GetResetPasswordEntry(ctx context.Context, token string) (*types.ResetPasswordRequest, *model.ApiError)
|
||||
GetUsersByOrg(ctx context.Context, orgId string) ([]types.GettableUser, *model.ApiError)
|
||||
GetUsersByRole(ctx context.Context, role authtypes.Role) ([]types.GettableUser, *model.ApiError)
|
||||
|
||||
GetApdexSettings(ctx context.Context, orgID string, services []string) ([]types.ApdexSettings, *model.ApiError)
|
||||
|
||||
PrecheckLogin(ctx context.Context, email, sourceUrl string) (*model.PrecheckResponse, model.BaseApiError)
|
||||
}
|
||||
|
||||
type Mutations interface {
|
||||
CreateInviteEntry(ctx context.Context, req *types.Invite) *model.ApiError
|
||||
DeleteInvitation(ctx context.Context, orgID string, email string) *model.ApiError
|
||||
|
||||
CreateUser(ctx context.Context, user *types.User, isFirstUser bool) (*types.User, *model.ApiError)
|
||||
EditUser(ctx context.Context, update *types.User) (*types.User, *model.ApiError)
|
||||
DeleteUser(ctx context.Context, id string) *model.ApiError
|
||||
|
||||
CreateResetPasswordEntry(ctx context.Context, req *types.ResetPasswordRequest) *model.ApiError
|
||||
DeleteResetPasswordEntry(ctx context.Context, token string) *model.ApiError
|
||||
|
||||
UpdateUserPassword(ctx context.Context, hash, userId string) *model.ApiError
|
||||
UpdateUserRole(ctx context.Context, userId string, role authtypes.Role) *model.ApiError
|
||||
|
||||
UpdateUserRole(ctx context.Context, userId string, role types.Role) *model.ApiError
|
||||
SetApdexSettings(ctx context.Context, orgID string, set *types.ApdexSettings) *model.ApiError
|
||||
}
|
||||
|
@ -24,8 +24,8 @@ func InitDB(sqlStore sqlstore.SQLStore) (*ModelDaoSqlite, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
telemetry.GetInstance().SetUserCountCallback(mds.GetUserCount)
|
||||
telemetry.GetInstance().SetGetUsersCallback(mds.GetUsers)
|
||||
telemetry.GetInstance().SetGetUsersCallback(telemetry.GetUsers)
|
||||
telemetry.GetInstance().SetUserCountCallback(telemetry.GetUserCount)
|
||||
|
||||
return mds, nil
|
||||
}
|
||||
@ -62,12 +62,5 @@ func (mds *ModelDaoSqlite) initializeOrgPreferences(ctx context.Context) error {
|
||||
// set telemetry fields from userPreferences
|
||||
telemetry.GetInstance().SetDistinctId(org.ID.StringValue())
|
||||
|
||||
users, _ := mds.GetUsers(ctx)
|
||||
countUsers := len(users)
|
||||
if countUsers > 0 {
|
||||
telemetry.GetInstance().SetCompanyDomain(users[countUsers-1].Email)
|
||||
telemetry.GetInstance().SetUserEmail(users[countUsers-1].Email)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -2,98 +2,11 @@ package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (mds *ModelDaoSqlite) CreateInviteEntry(ctx context.Context, req *types.Invite) *model.ApiError {
|
||||
_, err := mds.bundb.NewInsert().
|
||||
Model(req).
|
||||
Exec(ctx)
|
||||
|
||||
if err != nil {
|
||||
return &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mds *ModelDaoSqlite) DeleteInvitation(ctx context.Context, orgID string, email string) *model.ApiError {
|
||||
_, err := mds.bundb.NewDelete().
|
||||
Model(&types.Invite{}).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("email = ?", email).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: Make this work with org id
|
||||
func (mds *ModelDaoSqlite) GetInviteFromEmail(ctx context.Context, email string,
|
||||
) (*types.Invite, *model.ApiError) {
|
||||
|
||||
invites := []types.Invite{}
|
||||
err := mds.bundb.NewSelect().
|
||||
Model(&invites).
|
||||
Where("email = ?", email).
|
||||
Scan(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
if len(invites) > 1 {
|
||||
return nil, &model.ApiError{
|
||||
Typ: model.ErrorInternal,
|
||||
Err: errors.Errorf("Found multiple invites for the email: %s", email)}
|
||||
}
|
||||
|
||||
if len(invites) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return &invites[0], nil
|
||||
}
|
||||
|
||||
func (mds *ModelDaoSqlite) GetInviteFromToken(ctx context.Context, token string,
|
||||
) (*types.Invite, *model.ApiError) {
|
||||
// This won't take org id because it's a public facing API
|
||||
|
||||
invites := []types.Invite{}
|
||||
err := mds.bundb.NewSelect().
|
||||
Model(&invites).
|
||||
Where("token = ?", token).
|
||||
Scan(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
if len(invites) > 1 {
|
||||
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
|
||||
if len(invites) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return &invites[0], nil
|
||||
}
|
||||
|
||||
func (mds *ModelDaoSqlite) GetInvites(ctx context.Context, orgID string) ([]types.Invite, *model.ApiError) {
|
||||
invites := []types.Invite{}
|
||||
err := mds.bundb.NewSelect().
|
||||
Model(&invites).
|
||||
Where("org_id = ?", orgID).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
return invites, nil
|
||||
}
|
||||
|
||||
func (mds *ModelDaoSqlite) GetOrgs(ctx context.Context) ([]types.Organization, *model.ApiError) {
|
||||
var orgs []types.Organization
|
||||
err := mds.bundb.NewSelect().
|
||||
@ -106,64 +19,7 @@ func (mds *ModelDaoSqlite) GetOrgs(ctx context.Context) ([]types.Organization, *
|
||||
return orgs, nil
|
||||
}
|
||||
|
||||
func (mds *ModelDaoSqlite) CreateUser(ctx context.Context,
|
||||
user *types.User, isFirstUser bool) (*types.User, *model.ApiError) {
|
||||
_, err := mds.bundb.NewInsert().
|
||||
Model(user).
|
||||
Exec(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"name": user.Name,
|
||||
"email": user.Email,
|
||||
"firstRegistration": false,
|
||||
}
|
||||
|
||||
if isFirstUser {
|
||||
data["firstRegistration"] = true
|
||||
}
|
||||
|
||||
telemetry.GetInstance().IdentifyUser(user)
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_USER, data, user.Email, true, false)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (mds *ModelDaoSqlite) EditUser(ctx context.Context,
|
||||
update *types.User) (*types.User, *model.ApiError) {
|
||||
_, err := mds.bundb.NewUpdate().
|
||||
Model(update).
|
||||
Column("name").
|
||||
Column("org_id").
|
||||
Column("email").
|
||||
Where("id = ?", update.ID).
|
||||
Exec(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
return update, nil
|
||||
}
|
||||
|
||||
func (mds *ModelDaoSqlite) UpdateUserPassword(ctx context.Context, passwordHash,
|
||||
userId string) *model.ApiError {
|
||||
|
||||
_, err := mds.bundb.NewUpdate().
|
||||
Model(&types.User{}).
|
||||
Set("password = ?", passwordHash).
|
||||
Where("id = ?", userId).
|
||||
Exec(ctx)
|
||||
|
||||
if err != nil {
|
||||
return &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mds *ModelDaoSqlite) UpdateUserRole(ctx context.Context, userId string, role authtypes.Role) *model.ApiError {
|
||||
func (mds *ModelDaoSqlite) UpdateUserRole(ctx context.Context, userId string, role types.Role) *model.ApiError {
|
||||
|
||||
_, err := mds.bundb.NewUpdate().
|
||||
Model(&types.User{}).
|
||||
@ -176,223 +32,3 @@ func (mds *ModelDaoSqlite) UpdateUserRole(ctx context.Context, userId string, ro
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mds *ModelDaoSqlite) DeleteUser(ctx context.Context, id string) *model.ApiError {
|
||||
result, err := mds.bundb.NewDelete().
|
||||
Model(&types.User{}).
|
||||
Where("id = ?", id).
|
||||
Exec(ctx)
|
||||
|
||||
if err != nil {
|
||||
return &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
|
||||
affectedRows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return &model.ApiError{Typ: model.ErrorExec, Err: err}
|
||||
}
|
||||
if affectedRows == 0 {
|
||||
return &model.ApiError{
|
||||
Typ: model.ErrorNotFound,
|
||||
Err: fmt.Errorf("no user found with id: %s", id),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mds *ModelDaoSqlite) GetUser(ctx context.Context,
|
||||
id string) (*types.GettableUser, *model.ApiError) {
|
||||
|
||||
users := []types.GettableUser{}
|
||||
query := mds.bundb.NewSelect().
|
||||
Table("users").
|
||||
Column("users.id", "users.name", "users.email", "users.password", "users.created_at", "users.profile_picture_url", "users.org_id", "users.role").
|
||||
ColumnExpr("o.display_name as organization").
|
||||
Join("JOIN organizations o ON o.id = users.org_id").
|
||||
Where("users.id = ?", id)
|
||||
|
||||
if err := query.Scan(ctx, &users); err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
if len(users) > 1 {
|
||||
return nil, &model.ApiError{
|
||||
Typ: model.ErrorInternal,
|
||||
Err: errors.New("Found multiple users with same ID"),
|
||||
}
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &users[0], nil
|
||||
}
|
||||
|
||||
func (mds *ModelDaoSqlite) GetUserByEmail(ctx context.Context,
|
||||
email string) (*types.GettableUser, *model.ApiError) {
|
||||
|
||||
if email == "" {
|
||||
return nil, &model.ApiError{
|
||||
Typ: model.ErrorBadData,
|
||||
Err: fmt.Errorf("empty email address"),
|
||||
}
|
||||
}
|
||||
|
||||
users := []types.GettableUser{}
|
||||
query := mds.bundb.NewSelect().
|
||||
Table("users").
|
||||
Column("users.id", "users.name", "users.email", "users.password", "users.created_at", "users.profile_picture_url", "users.org_id", "users.role").
|
||||
ColumnExpr("o.display_name as organization").
|
||||
Join("JOIN organizations o ON o.id = users.org_id").
|
||||
Where("users.email = ?", email)
|
||||
|
||||
if err := query.Scan(ctx, &users); err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
|
||||
if len(users) > 1 {
|
||||
return nil, &model.ApiError{
|
||||
Typ: model.ErrorInternal,
|
||||
Err: errors.New("Found multiple users with same ID."),
|
||||
}
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return &users[0], nil
|
||||
}
|
||||
|
||||
// GetUsers fetches total user count
|
||||
func (mds *ModelDaoSqlite) GetUsers(ctx context.Context) ([]types.GettableUser, *model.ApiError) {
|
||||
return mds.GetUsersWithOpts(ctx, 0)
|
||||
}
|
||||
|
||||
// GetUsersWithOpts fetches users and supports additional search options
|
||||
func (mds *ModelDaoSqlite) GetUsersWithOpts(ctx context.Context, limit int) ([]types.GettableUser, *model.ApiError) {
|
||||
users := []types.GettableUser{}
|
||||
|
||||
query := mds.bundb.NewSelect().
|
||||
Table("users").
|
||||
Column("users.id", "users.name", "users.email", "users.password", "users.created_at", "users.profile_picture_url", "users.org_id", "users.role").
|
||||
ColumnExpr("users.role as role").
|
||||
ColumnExpr("o.display_name as organization").
|
||||
Join("JOIN organizations o ON o.id = users.org_id")
|
||||
|
||||
if limit > 0 {
|
||||
query.Limit(limit)
|
||||
}
|
||||
err := query.Scan(ctx, &users)
|
||||
|
||||
if err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (mds *ModelDaoSqlite) GetUsersByOrg(ctx context.Context,
|
||||
orgId string) ([]types.GettableUser, *model.ApiError) {
|
||||
|
||||
users := []types.GettableUser{}
|
||||
|
||||
query := mds.bundb.NewSelect().
|
||||
Table("users").
|
||||
Column("users.id", "users.name", "users.email", "users.password", "users.created_at", "users.profile_picture_url", "users.org_id", "users.role").
|
||||
ColumnExpr("users.role as role").
|
||||
ColumnExpr("o.display_name as organization").
|
||||
Join("JOIN organizations o ON o.id = users.org_id").
|
||||
Where("users.org_id = ?", orgId)
|
||||
|
||||
err := query.Scan(ctx, &users)
|
||||
if err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (mds *ModelDaoSqlite) GetUsersByRole(ctx context.Context, role authtypes.Role) ([]types.GettableUser, *model.ApiError) {
|
||||
users := []types.GettableUser{}
|
||||
|
||||
query := mds.bundb.NewSelect().
|
||||
Table("users").
|
||||
Column("users.id", "users.name", "users.email", "users.password", "users.created_at", "users.profile_picture_url", "users.org_id", "users.role").
|
||||
ColumnExpr("users.role as role").
|
||||
ColumnExpr("o.display_name as organization").
|
||||
Join("JOIN organizations o ON o.id = users.org_id").
|
||||
Where("users.role = ?", role)
|
||||
|
||||
err := query.Scan(ctx, &users)
|
||||
if err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (mds *ModelDaoSqlite) CreateResetPasswordEntry(ctx context.Context, req *types.ResetPasswordRequest) *model.ApiError {
|
||||
|
||||
if _, err := mds.bundb.NewInsert().
|
||||
Model(req).
|
||||
Exec(ctx); err != nil {
|
||||
return &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mds *ModelDaoSqlite) DeleteResetPasswordEntry(ctx context.Context, token string) *model.ApiError {
|
||||
_, err := mds.bundb.NewDelete().
|
||||
Model(&types.ResetPasswordRequest{}).
|
||||
Where("token = ?", token).
|
||||
Exec(ctx)
|
||||
|
||||
if err != nil {
|
||||
return &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mds *ModelDaoSqlite) GetResetPasswordEntry(ctx context.Context, token string) (*types.ResetPasswordRequest, *model.ApiError) {
|
||||
|
||||
entries := []types.ResetPasswordRequest{}
|
||||
|
||||
if err := mds.bundb.NewSelect().
|
||||
Model(&entries).
|
||||
Where("token = ?", token).
|
||||
Scan(ctx); err != nil {
|
||||
return nil, &model.ApiError{Typ: model.ErrorInternal, Err: err}
|
||||
}
|
||||
if len(entries) > 1 {
|
||||
return nil, &model.ApiError{Typ: model.ErrorInternal,
|
||||
Err: errors.New("Multiple entries for reset token is found")}
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return &entries[0], nil
|
||||
}
|
||||
|
||||
func (mds *ModelDaoSqlite) PrecheckLogin(ctx context.Context, email, sourceUrl string) (*model.PrecheckResponse, model.BaseApiError) {
|
||||
// assume user is valid unless proven otherwise and assign default values for rest of the fields
|
||||
resp := &model.PrecheckResponse{IsUser: true, CanSelfRegister: false, SSO: false, SsoUrl: "", SsoError: ""}
|
||||
|
||||
// check if email is a valid user
|
||||
userPayload, baseApiErr := mds.GetUserByEmail(ctx, email)
|
||||
if baseApiErr != nil {
|
||||
return resp, baseApiErr
|
||||
}
|
||||
|
||||
if userPayload == nil {
|
||||
resp.IsUser = false
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (mds *ModelDaoSqlite) GetUserCount(ctx context.Context) (int, error) {
|
||||
users, err := mds.GetUsers(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(users), nil
|
||||
}
|
||||
|
@ -9,9 +9,12 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/config"
|
||||
"github.com/SigNoz/signoz/pkg/config/envprovider"
|
||||
"github.com/SigNoz/signoz/pkg/config/fileprovider"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
@ -108,6 +111,12 @@ func main() {
|
||||
signoz.NewWebProviderFactories(),
|
||||
signoz.NewSQLStoreProviderFactories(),
|
||||
signoz.NewTelemetryStoreProviderFactories(),
|
||||
func(sqlstore sqlstore.SQLStore) user.Module {
|
||||
return impluser.NewModule(impluser.NewStore(sqlstore))
|
||||
},
|
||||
func(userModule user.Module) user.Handler {
|
||||
return impluser.NewHandler(userModule)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
zap.L().Fatal("Failed to create signoz", zap.Error(err))
|
||||
|
@ -1,99 +0,0 @@
|
||||
package model
|
||||
|
||||
import "github.com/pkg/errors"
|
||||
|
||||
var (
|
||||
ErrorTokenExpired = errors.New("Token is expired")
|
||||
)
|
||||
|
||||
type InviteRequest struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
FrontendBaseUrl string `json:"frontendBaseUrl"`
|
||||
}
|
||||
|
||||
type InviteResponse struct {
|
||||
Email string `json:"email"`
|
||||
InviteToken string `json:"inviteToken"`
|
||||
}
|
||||
|
||||
type InvitationResponseObject struct {
|
||||
Email string `json:"email" db:"email"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Token string `json:"token" db:"token"`
|
||||
CreatedAt int64 `json:"createdAt" db:"created_at"`
|
||||
Role string `json:"role" db:"role"`
|
||||
Organization string `json:"organization" db:"organization"`
|
||||
}
|
||||
|
||||
type BulkInviteRequest struct {
|
||||
Users []InviteRequest `json:"users"`
|
||||
}
|
||||
|
||||
type BulkInviteResponse struct {
|
||||
Status string `json:"status"`
|
||||
Summary InviteSummary `json:"summary"`
|
||||
SuccessfulInvites []SuccessfulInvite `json:"successful_invites"`
|
||||
FailedInvites []FailedInvite `json:"failed_invites"`
|
||||
}
|
||||
|
||||
type InviteSummary struct {
|
||||
TotalInvites int `json:"total_invites"`
|
||||
SuccessfulInvites int `json:"successful_invites"`
|
||||
FailedInvites int `json:"failed_invites"`
|
||||
}
|
||||
|
||||
type SuccessfulInvite struct {
|
||||
Email string `json:"email"`
|
||||
InviteLink string `json:"invite_link"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type FailedInvite struct {
|
||||
Email string `json:"email"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
}
|
||||
|
||||
// PrecheckResponse contains login precheck response
|
||||
type PrecheckResponse struct {
|
||||
SSO bool `json:"sso"`
|
||||
SsoUrl string `json:"ssoUrl"`
|
||||
CanSelfRegister bool `json:"canSelfRegister"`
|
||||
IsUser bool `json:"isUser"`
|
||||
SsoError string `json:"ssoError"`
|
||||
}
|
||||
|
||||
type UserJwtObject struct {
|
||||
AccessJwt string `json:"accessJwt"`
|
||||
AccessJwtExpiry int64 `json:"accessJwtExpiry"`
|
||||
RefreshJwt string `json:"refreshJwt"`
|
||||
RefreshJwtExpiry int64 `json:"refreshJwtExpiry"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
UserJwtObject
|
||||
UserId string `json:"userId"`
|
||||
}
|
||||
|
||||
type ChangePasswordRequest struct {
|
||||
UserId string `json:"userId"`
|
||||
OldPassword string `json:"oldPassword"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
}
|
||||
|
||||
type ResetPasswordRequest struct {
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type UserRole struct {
|
||||
UserId string `json:"user_id"`
|
||||
GroupName string `json:"group_name"`
|
||||
}
|
@ -19,6 +19,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/version"
|
||||
)
|
||||
@ -196,6 +197,7 @@ type Telemetry struct {
|
||||
isAnonymous bool
|
||||
distinctId string
|
||||
reader interfaces.Reader
|
||||
sqlStore sqlstore.SQLStore
|
||||
companyDomain string
|
||||
minRandInt int
|
||||
maxRandInt int
|
||||
@ -205,8 +207,8 @@ type Telemetry struct {
|
||||
mutex sync.RWMutex
|
||||
|
||||
alertsInfoCallback func(ctx context.Context) (*model.AlertsInfo, error)
|
||||
userCountCallback func(ctx context.Context) (int, error)
|
||||
getUsersCallback func(ctx context.Context) ([]types.GettableUser, *model.ApiError)
|
||||
userCountCallback func(ctx context.Context, store sqlstore.SQLStore) (int, error)
|
||||
getUsersCallback func(ctx context.Context, store sqlstore.SQLStore) ([]TelemetryUser, error)
|
||||
dashboardsInfoCallback func(ctx context.Context) (*model.DashboardsInfo, error)
|
||||
savedViewsInfoCallback func(ctx context.Context) (*model.SavedViewsInfo, error)
|
||||
}
|
||||
@ -215,11 +217,11 @@ func (a *Telemetry) SetAlertsInfoCallback(callback func(ctx context.Context) (*m
|
||||
a.alertsInfoCallback = callback
|
||||
}
|
||||
|
||||
func (a *Telemetry) SetUserCountCallback(callback func(ctx context.Context) (int, error)) {
|
||||
func (a *Telemetry) SetUserCountCallback(callback func(ctx context.Context, store sqlstore.SQLStore) (int, error)) {
|
||||
a.userCountCallback = callback
|
||||
}
|
||||
|
||||
func (a *Telemetry) SetGetUsersCallback(callback func(ctx context.Context) ([]types.GettableUser, *model.ApiError)) {
|
||||
func (a *Telemetry) SetGetUsersCallback(callback func(ctx context.Context, store sqlstore.SQLStore) ([]TelemetryUser, error)) {
|
||||
a.getUsersCallback = callback
|
||||
}
|
||||
|
||||
@ -317,7 +319,7 @@ func createTelemetry() {
|
||||
metricsTTL, _ := telemetry.reader.GetTTL(ctx, "", &model.GetTTLParams{Type: constants.MetricsTTL})
|
||||
logsTTL, _ := telemetry.reader.GetTTL(ctx, "", &model.GetTTLParams{Type: constants.LogsTTL})
|
||||
|
||||
userCount, _ := telemetry.userCountCallback(ctx)
|
||||
userCount, _ := telemetry.userCountCallback(ctx, telemetry.sqlStore)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"totalSpans": totalSpans,
|
||||
@ -340,7 +342,7 @@ func createTelemetry() {
|
||||
data[key] = value
|
||||
}
|
||||
|
||||
users, apiErr := telemetry.getUsersCallback(ctx)
|
||||
users, apiErr := telemetry.getUsersCallback(ctx, telemetry.sqlStore)
|
||||
if apiErr == nil {
|
||||
for _, user := range users {
|
||||
if user.Email == DEFAULT_CLOUD_EMAIL {
|
||||
@ -351,6 +353,9 @@ func createTelemetry() {
|
||||
}
|
||||
|
||||
alertsInfo, err := telemetry.alertsInfoCallback(ctx)
|
||||
if err != nil {
|
||||
telemetry.SendEvent(TELEMETRY_EVENT_DASHBOARDS_ALERTS, map[string]interface{}{"error": err.Error()}, "", true, false)
|
||||
}
|
||||
if err == nil {
|
||||
dashboardsInfo, err := telemetry.dashboardsInfoCallback(ctx)
|
||||
if err == nil {
|
||||
@ -442,9 +447,6 @@ func createTelemetry() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil || apiErr != nil {
|
||||
telemetry.SendEvent(TELEMETRY_EVENT_DASHBOARDS_ALERTS, map[string]interface{}{"error": err.Error()}, "", true, false)
|
||||
}
|
||||
|
||||
if totalLogs > 0 {
|
||||
telemetry.SendIdentifyEvent(map[string]interface{}{"sent_logs": true}, "")
|
||||
@ -554,7 +556,7 @@ func (a *Telemetry) IdentifyUser(user *types.User) {
|
||||
if a.saasOperator != nil {
|
||||
_ = a.saasOperator.Enqueue(analytics.Identify{
|
||||
UserId: a.userEmail,
|
||||
Traits: analytics.NewTraits().SetName(user.Name).SetEmail(user.Email).Set("role", user.Role),
|
||||
Traits: analytics.NewTraits().SetName(user.DisplayName).SetEmail(user.Email).Set("role", user.Role),
|
||||
})
|
||||
|
||||
_ = a.saasOperator.Enqueue(analytics.Group{
|
||||
@ -567,7 +569,7 @@ func (a *Telemetry) IdentifyUser(user *types.User) {
|
||||
if a.ossOperator != nil {
|
||||
_ = a.ossOperator.Enqueue(analytics.Identify{
|
||||
UserId: a.ipAddress,
|
||||
Traits: analytics.NewTraits().SetName(user.Name).SetEmail(user.Email).Set("ip", a.ipAddress),
|
||||
Traits: analytics.NewTraits().SetName(user.DisplayName).SetEmail(user.Email).Set("ip", a.ipAddress),
|
||||
})
|
||||
// Updating a groups properties
|
||||
_ = a.ossOperator.Enqueue(analytics.Group{
|
||||
@ -799,6 +801,10 @@ func (a *Telemetry) SetReader(reader interfaces.Reader) {
|
||||
a.reader = reader
|
||||
}
|
||||
|
||||
func (a *Telemetry) SetSqlStore(store sqlstore.SQLStore) {
|
||||
a.sqlStore = store
|
||||
}
|
||||
|
||||
func GetInstance() *Telemetry {
|
||||
|
||||
once.Do(func() {
|
||||
|
46
pkg/query-service/telemetry/user.go
Normal file
46
pkg/query-service/telemetry/user.go
Normal file
@ -0,0 +1,46 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
)
|
||||
|
||||
type TelemetryUser struct {
|
||||
types.User
|
||||
Organization string `json:"organization"`
|
||||
}
|
||||
|
||||
func GetUsers(ctx context.Context, sqlstore sqlstore.SQLStore) ([]TelemetryUser, error) {
|
||||
return GetUsersWithOpts(ctx, 0, sqlstore)
|
||||
}
|
||||
|
||||
func GetUserCount(ctx context.Context, sqlstore sqlstore.SQLStore) (int, error) {
|
||||
users, err := GetUsersWithOpts(ctx, 0, sqlstore)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(users), nil
|
||||
}
|
||||
|
||||
// GetUsersWithOpts fetches users and supports additional search options
|
||||
func GetUsersWithOpts(ctx context.Context, limit int, sqlstore sqlstore.SQLStore) ([]TelemetryUser, error) {
|
||||
users := []TelemetryUser{}
|
||||
|
||||
query := sqlstore.BunDB().NewSelect().
|
||||
Table("user").
|
||||
Column("user.id", "user.display_name", "user.email", "user.created_at", "user.org_id").
|
||||
ColumnExpr("o.display_name as organization").
|
||||
Join("JOIN organizations o ON o.id = user.org_id")
|
||||
|
||||
if limit > 0 {
|
||||
query.Limit(limit)
|
||||
}
|
||||
err := query.Scan(ctx, &users)
|
||||
if err != nil {
|
||||
return nil, errors.WrapNotFoundf(err, errors.CodeNotFound, "failed to get users")
|
||||
}
|
||||
return users, nil
|
||||
}
|
@ -4,16 +4,19 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||
quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||
quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/dao"
|
||||
@ -267,6 +270,7 @@ type FilterSuggestionsTestBed struct {
|
||||
testUser *types.User
|
||||
qsHttpHandler http.Handler
|
||||
mockClickhouse mockhouse.ClickConnMockCommon
|
||||
userModule user.Module
|
||||
}
|
||||
|
||||
func (tb *FilterSuggestionsTestBed) GetQBFilterSuggestionsForLogs(
|
||||
@ -300,7 +304,9 @@ func NewFilterSuggestionsTestBed(t *testing.T) *FilterSuggestionsTestBed {
|
||||
reader, mockClickhouse := NewMockClickhouseReader(t, testDB)
|
||||
mockClickhouse.MatchExpectationsInOrder(false)
|
||||
|
||||
modules := signoz.NewModules(testDB)
|
||||
userModule := impluser.NewModule(impluser.NewStore(testDB))
|
||||
userHandler := impluser.NewHandler(userModule)
|
||||
modules := signoz.NewModules(testDB, userModule)
|
||||
quickFilterModule := quickfilter.NewAPI(quickfilterscore.NewQuickFilters(quickfilterscore.NewStore(testDB)))
|
||||
|
||||
apiHandler, err := app.NewAPIHandler(app.APIHandlerOpts{
|
||||
@ -310,7 +316,7 @@ func NewFilterSuggestionsTestBed(t *testing.T) *FilterSuggestionsTestBed {
|
||||
JWT: jwt,
|
||||
Signoz: &signoz.SigNoz{
|
||||
Modules: modules,
|
||||
Handlers: signoz.NewHandlers(modules),
|
||||
Handlers: signoz.NewHandlers(modules, userHandler),
|
||||
},
|
||||
QuickFilters: quickFilterModule,
|
||||
})
|
||||
@ -326,7 +332,7 @@ func NewFilterSuggestionsTestBed(t *testing.T) *FilterSuggestionsTestBed {
|
||||
apiHandler.RegisterQueryRangeV3Routes(router, am)
|
||||
|
||||
organizationModule := implorganization.NewModule(implorganization.NewStore(testDB))
|
||||
user, apiErr := createTestUser(organizationModule)
|
||||
user, apiErr := createTestUser(organizationModule, userModule)
|
||||
if apiErr != nil {
|
||||
t.Fatalf("could not create a test user: %v", apiErr)
|
||||
}
|
||||
@ -343,6 +349,7 @@ func NewFilterSuggestionsTestBed(t *testing.T) *FilterSuggestionsTestBed {
|
||||
testUser: user,
|
||||
qsHttpHandler: router,
|
||||
mockClickhouse: mockClickhouse,
|
||||
userModule: userModule,
|
||||
}
|
||||
}
|
||||
|
||||
@ -359,7 +366,7 @@ func (tb *FilterSuggestionsTestBed) QSGetRequest(
|
||||
}
|
||||
|
||||
req, err := AuthenticatedRequestForTest(
|
||||
tb.testUser, path, nil,
|
||||
tb.userModule, tb.testUser, path, nil,
|
||||
)
|
||||
if err != nil {
|
||||
tb.t.Fatalf("couldn't create authenticated test request: %v", err)
|
||||
|
@ -10,6 +10,10 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||
quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
||||
@ -21,6 +25,7 @@ import (
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/queryBuilderToExpr"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
@ -448,6 +453,7 @@ type LogPipelinesTestBed struct {
|
||||
agentConfMgr *agentConf.Manager
|
||||
opampServer *opamp.Server
|
||||
opampClientConn *opamp.MockOpAmpConnection
|
||||
userModule user.Module
|
||||
}
|
||||
|
||||
// testDB can be injected for sharing a DB across multiple integration testbeds.
|
||||
@ -471,17 +477,28 @@ func NewTestbedWithoutOpamp(t *testing.T, sqlStore sqlstore.SQLStore) *LogPipeli
|
||||
t.Fatalf("could not create a logparsingpipelines controller: %v", err)
|
||||
}
|
||||
|
||||
userModule := impluser.NewModule(impluser.NewStore(sqlStore))
|
||||
userHandler := impluser.NewHandler(userModule)
|
||||
modules := signoz.NewModules(sqlStore, userModule)
|
||||
handlers := signoz.NewHandlers(modules, userHandler)
|
||||
quickFilterModule := quickfilter.NewAPI(quickfilterscore.NewQuickFilters(quickfilterscore.NewStore(sqlStore)))
|
||||
|
||||
apiHandler, err := app.NewAPIHandler(app.APIHandlerOpts{
|
||||
AppDao: dao.DB(),
|
||||
LogsParsingPipelineController: controller,
|
||||
JWT: jwt,
|
||||
Signoz: &signoz.SigNoz{
|
||||
Modules: modules,
|
||||
Handlers: handlers,
|
||||
},
|
||||
QuickFilters: quickFilterModule,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("could not create a new ApiHandler: %v", err)
|
||||
}
|
||||
|
||||
organizationModule := implorganization.NewModule(implorganization.NewStore(sqlStore))
|
||||
user, apiErr := createTestUser(organizationModule)
|
||||
user, apiErr := createTestUser(organizationModule, userModule)
|
||||
if apiErr != nil {
|
||||
t.Fatalf("could not create a test user: %v", apiErr)
|
||||
}
|
||||
@ -502,6 +519,7 @@ func NewTestbedWithoutOpamp(t *testing.T, sqlStore sqlstore.SQLStore) *LogPipeli
|
||||
testUser: user,
|
||||
apiHandler: apiHandler,
|
||||
agentConfMgr: agentConfMgr,
|
||||
userModule: userModule,
|
||||
}
|
||||
}
|
||||
|
||||
@ -539,7 +557,7 @@ func (tb *LogPipelinesTestBed) PostPipelinesToQSExpectingStatusCode(
|
||||
expectedStatusCode int,
|
||||
) *logparsingpipeline.PipelinesResponse {
|
||||
req, err := AuthenticatedRequestForTest(
|
||||
tb.testUser, "/api/v1/logs/pipelines", postablePipelines,
|
||||
tb.userModule, tb.testUser, "/api/v1/logs/pipelines", postablePipelines,
|
||||
)
|
||||
if err != nil {
|
||||
tb.t.Fatalf("couldn't create authenticated test request: %v", err)
|
||||
@ -594,7 +612,7 @@ func (tb *LogPipelinesTestBed) PostPipelinesToQS(
|
||||
|
||||
func (tb *LogPipelinesTestBed) GetPipelinesFromQS() *logparsingpipeline.PipelinesResponse {
|
||||
req, err := AuthenticatedRequestForTest(
|
||||
tb.testUser, "/api/v1/logs/pipelines/latest", nil,
|
||||
tb.userModule, tb.testUser, "/api/v1/logs/pipelines/latest", nil,
|
||||
)
|
||||
if err != nil {
|
||||
tb.t.Fatalf("couldn't create authenticated test request: %v", err)
|
||||
|
@ -3,15 +3,18 @@ package tests
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||
quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||
quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
@ -346,6 +349,7 @@ type CloudIntegrationsTestBed struct {
|
||||
testUser *types.User
|
||||
qsHttpHandler http.Handler
|
||||
mockClickhouse mockhouse.ClickConnMockCommon
|
||||
userModule user.Module
|
||||
}
|
||||
|
||||
// testDB can be injected for sharing a DB across multiple integration testbeds.
|
||||
@ -363,8 +367,10 @@ func NewCloudIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *CloudI
|
||||
reader, mockClickhouse := NewMockClickhouseReader(t, testDB)
|
||||
mockClickhouse.MatchExpectationsInOrder(false)
|
||||
|
||||
modules := signoz.NewModules(testDB)
|
||||
handlers := signoz.NewHandlers(modules)
|
||||
userModule := impluser.NewModule(impluser.NewStore(testDB))
|
||||
userHandler := impluser.NewHandler(userModule)
|
||||
modules := signoz.NewModules(testDB, userModule)
|
||||
handlers := signoz.NewHandlers(modules, userHandler)
|
||||
quickFilterModule := quickfilter.NewAPI(quickfilterscore.NewQuickFilters(quickfilterscore.NewStore(testDB)))
|
||||
|
||||
apiHandler, err := app.NewAPIHandler(app.APIHandlerOpts{
|
||||
@ -390,7 +396,7 @@ func NewCloudIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *CloudI
|
||||
apiHandler.RegisterCloudIntegrationsRoutes(router, am)
|
||||
|
||||
organizationModule := implorganization.NewModule(implorganization.NewStore(testDB))
|
||||
user, apiErr := createTestUser(organizationModule)
|
||||
user, apiErr := createTestUser(organizationModule, userModule)
|
||||
if apiErr != nil {
|
||||
t.Fatalf("could not create a test user: %v", apiErr)
|
||||
}
|
||||
@ -400,6 +406,7 @@ func NewCloudIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *CloudI
|
||||
testUser: user,
|
||||
qsHttpHandler: router,
|
||||
mockClickhouse: mockClickhouse,
|
||||
userModule: userModule,
|
||||
}
|
||||
}
|
||||
|
||||
@ -556,7 +563,7 @@ func (tb *CloudIntegrationsTestBed) RequestQS(
|
||||
postData interface{},
|
||||
) (responseDataJson []byte) {
|
||||
req, err := AuthenticatedRequestForTest(
|
||||
tb.testUser, path, postData,
|
||||
tb.userModule, tb.testUser, path, postData,
|
||||
)
|
||||
if err != nil {
|
||||
tb.t.Fatalf("couldn't create authenticated test request: %v", err)
|
||||
|
@ -3,16 +3,19 @@ package tests
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||
quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
|
||||
"net/http"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||
quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
||||
@ -374,6 +377,7 @@ type IntegrationsTestBed struct {
|
||||
testUser *types.User
|
||||
qsHttpHandler http.Handler
|
||||
mockClickhouse mockhouse.ClickConnMockCommon
|
||||
userModule user.Module
|
||||
}
|
||||
|
||||
func (tb *IntegrationsTestBed) GetAvailableIntegrationsFromQS() *integrations.IntegrationsListResponse {
|
||||
@ -506,7 +510,7 @@ func (tb *IntegrationsTestBed) RequestQS(
|
||||
postData interface{},
|
||||
) *app.ApiResponse {
|
||||
req, err := AuthenticatedRequestForTest(
|
||||
tb.testUser, path, postData,
|
||||
tb.userModule, tb.testUser, path, postData,
|
||||
)
|
||||
if err != nil {
|
||||
tb.t.Fatalf("couldn't create authenticated test request: %v", err)
|
||||
@ -569,8 +573,11 @@ func NewIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *Integration
|
||||
t.Fatalf("could not create cloud integrations controller: %v", err)
|
||||
}
|
||||
|
||||
modules := signoz.NewModules(testDB)
|
||||
handlers := signoz.NewHandlers(modules)
|
||||
userModule := impluser.NewModule(impluser.NewStore(testDB))
|
||||
userHandler := impluser.NewHandler(userModule)
|
||||
modules := signoz.NewModules(testDB, userModule)
|
||||
handlers := signoz.NewHandlers(modules, userHandler)
|
||||
|
||||
quickFilterModule := quickfilter.NewAPI(quickfilterscore.NewQuickFilters(quickfilterscore.NewStore(testDB)))
|
||||
|
||||
apiHandler, err := app.NewAPIHandler(app.APIHandlerOpts{
|
||||
@ -597,7 +604,7 @@ func NewIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *Integration
|
||||
apiHandler.RegisterIntegrationRoutes(router, am)
|
||||
|
||||
organizationModule := implorganization.NewModule(implorganization.NewStore(testDB))
|
||||
user, apiErr := createTestUser(organizationModule)
|
||||
user, apiErr := createTestUser(organizationModule, userModule)
|
||||
if apiErr != nil {
|
||||
t.Fatalf("could not create a test user: %v", apiErr)
|
||||
}
|
||||
@ -607,6 +614,7 @@ func NewIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *Integration
|
||||
testUser: user,
|
||||
qsHttpHandler: router,
|
||||
mockClickhouse: mockClickhouse,
|
||||
userModule: userModule,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"testing"
|
||||
"time"
|
||||
@ -15,18 +16,18 @@ import (
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus/prometheustest"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/auth"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/dao"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/google/uuid"
|
||||
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza/entry"
|
||||
mockhouse "github.com/srikanthccv/ClickHouse-go-mock"
|
||||
@ -34,7 +35,7 @@ import (
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
var jwt = authtypes.NewJWT("secret", 1*time.Hour, 2*time.Hour)
|
||||
var jwt = authtypes.NewJWT(os.Getenv("SIGNOZ_JWT_SECRET"), 1*time.Hour, 2*time.Hour)
|
||||
|
||||
func NewMockClickhouseReader(t *testing.T, testDB sqlstore.SQLStore) (*clickhouseReader.ClickHouseReader, mockhouse.ClickConnMockCommon) {
|
||||
require.NotNil(t, testDB)
|
||||
@ -146,7 +147,7 @@ func makeTestSignozLog(
|
||||
return testLog
|
||||
}
|
||||
|
||||
func createTestUser(organizationModule organization.Module) (*types.User, *model.ApiError) {
|
||||
func createTestUser(organizationModule organization.Module, userModule user.Module) (*types.User, *model.ApiError) {
|
||||
// Create a test user for auth
|
||||
ctx := context.Background()
|
||||
organization := types.NewOrganization("test")
|
||||
@ -155,28 +156,28 @@ func createTestUser(organizationModule organization.Module) (*types.User, *model
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
|
||||
userId := uuid.NewString()
|
||||
userId := valuer.GenerateUUID()
|
||||
|
||||
return dao.DB().CreateUser(
|
||||
ctx,
|
||||
&types.User{
|
||||
ID: userId,
|
||||
Name: "test",
|
||||
Email: userId[:8] + "test@test.com",
|
||||
Password: "test",
|
||||
OrgID: organization.ID.StringValue(),
|
||||
Role: authtypes.RoleAdmin.String(),
|
||||
},
|
||||
true,
|
||||
)
|
||||
user, err := types.NewUser("test", userId.String()+"test@test.com", types.RoleAdmin.String(), organization.ID.StringValue())
|
||||
if err != nil {
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
|
||||
err = userModule.CreateUser(ctx, user)
|
||||
if err != nil {
|
||||
return nil, model.InternalError(err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func AuthenticatedRequestForTest(
|
||||
userModule user.Module,
|
||||
user *types.User,
|
||||
path string,
|
||||
postData interface{},
|
||||
) (*http.Request, error) {
|
||||
userJwt, err := auth.GenerateJWTForUser(user, jwt)
|
||||
userJwt, err := userModule.GetJWTForUser(context.Background(), user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -55,10 +55,18 @@ func NewTestSqliteDB(t *testing.T) (sqlStore sqlstore.SQLStore, testDBFilePath s
|
||||
sqlmigration.NewDropLicensesSitesFactory(sqlStore),
|
||||
sqlmigration.NewUpdateInvitesFactory(sqlStore),
|
||||
sqlmigration.NewUpdatePatFactory(sqlStore),
|
||||
sqlmigration.NewUpdateAlertmanagerFactory(sqlStore),
|
||||
sqlmigration.NewUpdatePreferencesFactory(sqlStore),
|
||||
sqlmigration.NewUpdateApdexTtlFactory(sqlStore),
|
||||
sqlmigration.NewUpdateResetPasswordFactory(sqlStore),
|
||||
sqlmigration.NewUpdateRulesFactory(sqlStore),
|
||||
sqlmigration.NewAddVirtualFieldsFactory(),
|
||||
sqlmigration.NewUpdateIntegrationsFactory(sqlStore),
|
||||
sqlmigration.NewUpdateOrganizationsFactory(sqlStore),
|
||||
sqlmigration.NewDropGroupsFactory(sqlStore),
|
||||
sqlmigration.NewCreateQuickFiltersFactory(sqlStore),
|
||||
sqlmigration.NewUpdateQuickFiltersFactory(sqlStore),
|
||||
sqlmigration.NewAuthRefactorFactory(sqlStore),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -5,16 +5,19 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference/implpreference"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
)
|
||||
|
||||
type Handlers struct {
|
||||
Organization organization.Handler
|
||||
Preference preference.Handler
|
||||
User user.Handler
|
||||
}
|
||||
|
||||
func NewHandlers(modules Modules) Handlers {
|
||||
func NewHandlers(modules Modules, user user.Handler) Handlers {
|
||||
return Handlers{
|
||||
Organization: implorganization.NewHandler(modules.Organization),
|
||||
Preference: implpreference.NewHandler(modules.Preference),
|
||||
User: user,
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference/implpreference"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/preferencetypes"
|
||||
)
|
||||
@ -12,11 +13,13 @@ import (
|
||||
type Modules struct {
|
||||
Organization organization.Module
|
||||
Preference preference.Module
|
||||
User user.Module
|
||||
}
|
||||
|
||||
func NewModules(sqlstore sqlstore.SQLStore) Modules {
|
||||
func NewModules(sqlstore sqlstore.SQLStore, user user.Module) Modules {
|
||||
return Modules{
|
||||
Organization: implorganization.NewModule(implorganization.NewStore(sqlstore)),
|
||||
Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewDefaultPreferenceMap()),
|
||||
User: user,
|
||||
}
|
||||
}
|
||||
|
@ -76,6 +76,7 @@ func NewSQLMigrationProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedM
|
||||
sqlmigration.NewDropGroupsFactory(sqlstore),
|
||||
sqlmigration.NewCreateQuickFiltersFactory(sqlstore),
|
||||
sqlmigration.NewUpdateQuickFiltersFactory(sqlstore),
|
||||
sqlmigration.NewAuthRefactorFactory(sqlstore),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/instrumentation"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/sqlmigration"
|
||||
"github.com/SigNoz/signoz/pkg/sqlmigrator"
|
||||
@ -41,6 +42,8 @@ func New(
|
||||
webProviderFactories factory.NamedMap[factory.ProviderFactory[web.Web, web.Config]],
|
||||
sqlstoreProviderFactories factory.NamedMap[factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config]],
|
||||
telemetrystoreProviderFactories factory.NamedMap[factory.ProviderFactory[telemetrystore.TelemetryStore, telemetrystore.Config]],
|
||||
diModules func(sqlstore.SQLStore) user.Module,
|
||||
diHandlers func(user.Module) user.Handler,
|
||||
) (*SigNoz, error) {
|
||||
// Initialize instrumentation
|
||||
instrumentation, err := instrumentation.New(ctx, config.Instrumentation, version.Info, "signoz")
|
||||
@ -153,11 +156,14 @@ func New(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userModule := diModules(sqlstore)
|
||||
userHandler := diHandlers(userModule)
|
||||
|
||||
// Initialize all modules
|
||||
modules := NewModules(sqlstore)
|
||||
modules := NewModules(sqlstore, userModule)
|
||||
|
||||
// Initialize all handlers for the modules
|
||||
handlers := NewHandlers(modules)
|
||||
handlers := NewHandlers(modules, userHandler)
|
||||
|
||||
registry, err := factory.NewRegistry(
|
||||
instrumentation.Logger(),
|
||||
|
233
pkg/sqlmigration/032_auth_refactor.go
Normal file
233
pkg/sqlmigration/032_auth_refactor.go
Normal file
@ -0,0 +1,233 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type authRefactor struct {
|
||||
store sqlstore.SQLStore
|
||||
}
|
||||
|
||||
func NewAuthRefactorFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("auth_refactor"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return newAuthRefactor(ctx, ps, c, sqlstore)
|
||||
})
|
||||
}
|
||||
|
||||
func newAuthRefactor(_ context.Context, _ factory.ProviderSettings, _ Config, store sqlstore.SQLStore) (SQLMigration, error) {
|
||||
return &authRefactor{store: store}, nil
|
||||
}
|
||||
|
||||
func (migration *authRefactor) Register(migrations *migrate.Migrations) error {
|
||||
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type existingUser32 struct {
|
||||
bun.BaseModel `bun:"table:users"`
|
||||
|
||||
types.TimeAuditable
|
||||
ID string `bun:"id,pk,type:text" json:"id"`
|
||||
Name string `bun:"name,type:text,notnull" json:"name"`
|
||||
Email string `bun:"email,type:text,notnull,unique" json:"email"`
|
||||
Password string `bun:"password,type:text,notnull" json:"-"`
|
||||
ProfilePictureURL string `bun:"profile_picture_url,type:text" json:"profilePictureURL"`
|
||||
Role string `bun:"role,type:text,notnull" json:"role"`
|
||||
OrgID string `bun:"org_id,type:text,notnull" json:"orgId"`
|
||||
}
|
||||
|
||||
type factorPassword32 struct {
|
||||
bun.BaseModel `bun:"table:factor_password"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
Password string `bun:"password,type:text,notnull" json:"password"`
|
||||
Temporary bool `bun:"temporary,type:boolean,notnull" json:"temporary"`
|
||||
UserID string `bun:"user_id,type:text,notnull" json:"userID"`
|
||||
}
|
||||
|
||||
type existingResetPasswordRequest32 struct {
|
||||
bun.BaseModel `bun:"table:reset_password_request"`
|
||||
|
||||
types.Identifiable
|
||||
Token string `bun:"token,type:text,notnull" json:"token"`
|
||||
UserID string `bun:"user_id,type:text,notnull,unique" json:"userId"`
|
||||
}
|
||||
|
||||
type newResetPasswordRequest32 struct {
|
||||
bun.BaseModel `bun:"table:reset_password_token"`
|
||||
|
||||
types.Identifiable
|
||||
Token string `bun:"token,type:text,notnull" json:"token"`
|
||||
PasswordID string `bun:"password_id,type:text,notnull" json:"passwordID"`
|
||||
}
|
||||
|
||||
func (migration *authRefactor) Up(ctx context.Context, db *bun.DB) error {
|
||||
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer tx.Rollback()
|
||||
|
||||
if _, err := tx.NewCreateTable().
|
||||
Model(new(factorPassword32)).
|
||||
ForeignKey(`("user_id") REFERENCES "users" ("id")`).
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// copy passwords from users table to factor_password table
|
||||
err = migration.CopyOldPasswordToNewPassword(ctx, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete profile picture url
|
||||
err = migration.store.Dialect().DropColumn(ctx, tx, "users", "profile_picture_url")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// delete password
|
||||
err = migration.store.Dialect().DropColumn(ctx, tx, "users", "password")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// rename name to display name
|
||||
_, err = migration.store.Dialect().RenameColumn(ctx, tx, "users", "name", "display_name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = migration.
|
||||
store.
|
||||
Dialect().
|
||||
RenameTableAndModifyModel(ctx, tx, new(existingResetPasswordRequest32), new(newResetPasswordRequest32), []string{FactorPasswordReference}, func(ctx context.Context) error {
|
||||
existingRequests := make([]*existingResetPasswordRequest32, 0)
|
||||
err = tx.
|
||||
NewSelect().
|
||||
Model(&existingRequests).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
if err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil && len(existingRequests) > 0 {
|
||||
// copy users and their passwords to new table
|
||||
newRequests, err := migration.
|
||||
CopyOldResetPasswordToNewResetPassword(ctx, tx, existingRequests)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.
|
||||
NewInsert().
|
||||
Model(&newRequests).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *authRefactor) Down(context.Context, *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *authRefactor) CopyOldPasswordToNewPassword(ctx context.Context, tx bun.IDB) error {
|
||||
// check if data already in factor_password table
|
||||
var count int64
|
||||
err := tx.NewSelect().Model(new(factorPassword32)).ColumnExpr("COUNT(*)").Scan(ctx, &count)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// check if password column exist in the users table.
|
||||
exists, err := migration.store.Dialect().ColumnExists(ctx, tx, "users", "password")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
// get all users from users table
|
||||
existingUsers := make([]*existingUser32, 0)
|
||||
err = tx.NewSelect().Model(&existingUsers).Scan(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newPasswords := make([]*factorPassword32, 0)
|
||||
for _, user := range existingUsers {
|
||||
newPasswords = append(newPasswords, &factorPassword32{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
Password: user.Password,
|
||||
Temporary: false,
|
||||
UserID: user.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// insert
|
||||
if len(newPasswords) > 0 {
|
||||
_, err = tx.NewInsert().Model(&newPasswords).Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *authRefactor) CopyOldResetPasswordToNewResetPassword(ctx context.Context, tx bun.IDB, existingRequests []*existingResetPasswordRequest32) ([]*newResetPasswordRequest32, error) {
|
||||
newRequests := make([]*newResetPasswordRequest32, 0)
|
||||
for _, request := range existingRequests {
|
||||
// get password id from user id
|
||||
var passwordID string
|
||||
err := tx.NewSelect().Table("factor_password").Column("id").Where("user_id = ?", request.UserID).Scan(ctx, &passwordID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newRequests = append(newRequests, &newResetPasswordRequest32{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
Token: request.Token,
|
||||
PasswordID: passwordID,
|
||||
})
|
||||
}
|
||||
return newRequests, nil
|
||||
}
|
@ -27,6 +27,7 @@ var (
|
||||
var (
|
||||
OrgReference = "org"
|
||||
UserReference = "user"
|
||||
FactorPasswordReference = "factor_password"
|
||||
CloudIntegrationReference = "cloud_integration"
|
||||
)
|
||||
|
||||
|
@ -20,12 +20,14 @@ const (
|
||||
const (
|
||||
Org string = "org"
|
||||
User string = "user"
|
||||
FactorPassword string = "factor_password"
|
||||
CloudIntegration string = "cloud_integration"
|
||||
)
|
||||
|
||||
const (
|
||||
OrgReference string = `("org_id") REFERENCES "organizations" ("id")`
|
||||
UserReference string = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE`
|
||||
FactorPasswordReference string = `("password_id") REFERENCES "factor_password" ("id")`
|
||||
CloudIntegrationReference string = `("cloud_integration_id") REFERENCES "cloud_integration" ("id") ON DELETE CASCADE`
|
||||
)
|
||||
|
||||
@ -259,6 +261,8 @@ func (dialect *dialect) RenameTableAndModifyModel(ctx context.Context, bun bun.I
|
||||
fkReferences = append(fkReferences, OrgReference)
|
||||
} else if reference == User && !slices.Contains(fkReferences, UserReference) {
|
||||
fkReferences = append(fkReferences, UserReference)
|
||||
} else if reference == FactorPassword && !slices.Contains(fkReferences, FactorPasswordReference) {
|
||||
fkReferences = append(fkReferences, FactorPasswordReference)
|
||||
} else if reference == CloudIntegration && !slices.Contains(fkReferences, CloudIntegrationReference) {
|
||||
fkReferences = append(fkReferences, CloudIntegrationReference)
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
@ -12,10 +13,10 @@ var _ jwt.ClaimsValidator = (*Claims)(nil)
|
||||
|
||||
type Claims struct {
|
||||
jwt.RegisteredClaims
|
||||
UserID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Role Role `json:"role"`
|
||||
OrgID string `json:"orgId"`
|
||||
UserID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Role types.Role `json:"role"`
|
||||
OrgID string `json:"orgId"`
|
||||
}
|
||||
|
||||
func (c *Claims) Validate() error {
|
||||
@ -47,7 +48,7 @@ func (c *Claims) LogValue() slog.Value {
|
||||
}
|
||||
|
||||
func (c *Claims) IsViewer() error {
|
||||
if slices.Contains([]Role{RoleViewer, RoleEditor, RoleAdmin}, c.Role) {
|
||||
if slices.Contains([]types.Role{types.RoleViewer, types.RoleEditor, types.RoleAdmin}, c.Role) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -55,7 +56,7 @@ func (c *Claims) IsViewer() error {
|
||||
}
|
||||
|
||||
func (c *Claims) IsEditor() error {
|
||||
if slices.Contains([]Role{RoleEditor, RoleAdmin}, c.Role) {
|
||||
if slices.Contains([]types.Role{types.RoleEditor, types.RoleAdmin}, c.Role) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -63,7 +64,7 @@ func (c *Claims) IsEditor() error {
|
||||
}
|
||||
|
||||
func (c *Claims) IsAdmin() error {
|
||||
if c.Role == RoleAdmin {
|
||||
if c.Role == types.RoleAdmin {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -75,7 +76,7 @@ func (c *Claims) IsSelfAccess(id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if c.Role == RoleAdmin {
|
||||
if c.Role == types.RoleAdmin {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
@ -81,7 +82,7 @@ func (j *JWT) signToken(claims Claims) (string, error) {
|
||||
}
|
||||
|
||||
// AccessToken creates an access token with the provided claims
|
||||
func (j *JWT) AccessToken(orgId, userId, email string, role Role) (string, Claims, error) {
|
||||
func (j *JWT) AccessToken(orgId, userId, email string, role types.Role) (string, Claims, error) {
|
||||
claims := Claims{
|
||||
UserID: userId,
|
||||
Role: role,
|
||||
@ -102,7 +103,7 @@ func (j *JWT) AccessToken(orgId, userId, email string, role Role) (string, Claim
|
||||
}
|
||||
|
||||
// RefreshToken creates a refresh token with the provided claims
|
||||
func (j *JWT) RefreshToken(orgId, userId, email string, role Role) (string, Claims, error) {
|
||||
func (j *JWT) RefreshToken(orgId, userId, email string, role types.Role) (string, Claims, error) {
|
||||
claims := Claims{
|
||||
UserID: userId,
|
||||
Role: role,
|
||||
|
@ -5,13 +5,14 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestJwtAccessToken(t *testing.T) {
|
||||
jwtService := NewJWT("secret", time.Minute, time.Hour)
|
||||
token, _, err := jwtService.AccessToken("orgId", "userId", "email@example.com", RoleAdmin)
|
||||
token, _, err := jwtService.AccessToken("orgId", "userId", "email@example.com", types.RoleAdmin)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
@ -19,7 +20,7 @@ func TestJwtAccessToken(t *testing.T) {
|
||||
|
||||
func TestJwtRefreshToken(t *testing.T) {
|
||||
jwtService := NewJWT("secret", time.Minute, time.Hour)
|
||||
token, _, err := jwtService.RefreshToken("orgId", "userId", "email@example.com", RoleAdmin)
|
||||
token, _, err := jwtService.RefreshToken("orgId", "userId", "email@example.com", types.RoleAdmin)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
@ -31,7 +32,7 @@ func TestJwtClaims(t *testing.T) {
|
||||
// Create a valid token
|
||||
claims := Claims{
|
||||
UserID: "userId",
|
||||
Role: RoleAdmin,
|
||||
Role: types.RoleAdmin,
|
||||
Email: "email@example.com",
|
||||
OrgID: "orgId",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
@ -65,7 +66,7 @@ func TestJwtClaimsExpiredToken(t *testing.T) {
|
||||
// Create an expired token
|
||||
claims := Claims{
|
||||
UserID: "userId",
|
||||
Role: RoleAdmin,
|
||||
Role: types.RoleAdmin,
|
||||
Email: "email@example.com",
|
||||
OrgID: "orgId",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
@ -87,7 +88,7 @@ func TestJwtClaimsInvalidSignature(t *testing.T) {
|
||||
// Create a valid token
|
||||
claims := Claims{
|
||||
UserID: "userId",
|
||||
Role: RoleAdmin,
|
||||
Role: types.RoleAdmin,
|
||||
Email: "email@example.com",
|
||||
OrgID: "orgId",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
@ -131,7 +132,7 @@ func TestJwtClaimsMissingUserID(t *testing.T) {
|
||||
|
||||
claims := Claims{
|
||||
UserID: "",
|
||||
Role: RoleAdmin,
|
||||
Role: types.RoleAdmin,
|
||||
Email: "email@example.com",
|
||||
OrgID: "orgId",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
@ -171,7 +172,7 @@ func TestJwtClaimsMissingOrgID(t *testing.T) {
|
||||
|
||||
claims := Claims{
|
||||
UserID: "userId",
|
||||
Role: RoleAdmin,
|
||||
Role: types.RoleAdmin,
|
||||
Email: "email@example.com",
|
||||
OrgID: "",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
|
@ -6,9 +6,7 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/sso"
|
||||
"github.com/SigNoz/signoz/ee/query-service/sso/saml"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/ssotypes"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
saml2 "github.com/russellhaering/gosaml2"
|
||||
@ -19,7 +17,7 @@ import (
|
||||
type StorableOrgDomain struct {
|
||||
bun.BaseModel `bun:"table:org_domains"`
|
||||
|
||||
types.TimeAuditable
|
||||
TimeAuditable
|
||||
ID uuid.UUID `json:"id" bun:"id,pk,type:text"`
|
||||
OrgID string `json:"orgId" bun:"org_id,type:text,notnull"`
|
||||
Name string `json:"name" bun:"name,type:varchar(50),notnull,unique"`
|
||||
@ -40,10 +38,10 @@ type GettableOrgDomain struct {
|
||||
SsoEnabled bool `json:"ssoEnabled"`
|
||||
SsoType SSOType `json:"ssoType"`
|
||||
|
||||
SamlConfig *SamlConfig `json:"samlConfig"`
|
||||
GoogleAuthConfig *GoogleOAuthConfig `json:"googleAuthConfig"`
|
||||
SamlConfig *ssotypes.SamlConfig `json:"samlConfig"`
|
||||
GoogleAuthConfig *ssotypes.GoogleOAuthConfig `json:"googleAuthConfig"`
|
||||
|
||||
Org *types.Organization
|
||||
Org *Organization
|
||||
}
|
||||
|
||||
func (od *GettableOrgDomain) String() string {
|
||||
@ -112,7 +110,7 @@ func (od *GettableOrgDomain) GetSAMLCert() string {
|
||||
|
||||
// PrepareGoogleOAuthProvider creates GoogleProvider that is used in
|
||||
// requesting OAuth and also used in processing response from google
|
||||
func (od *GettableOrgDomain) PrepareGoogleOAuthProvider(siteUrl *url.URL) (sso.OAuthCallbackProvider, error) {
|
||||
func (od *GettableOrgDomain) PrepareGoogleOAuthProvider(siteUrl *url.URL) (ssotypes.OAuthCallbackProvider, error) {
|
||||
if od.GoogleAuthConfig == nil {
|
||||
return nil, fmt.Errorf("GOOGLE OAUTH is not setup correctly for this domain")
|
||||
}
|
||||
@ -143,7 +141,7 @@ func (od *GettableOrgDomain) PrepareSamlRequest(siteUrl *url.URL) (*saml2.SAMLSe
|
||||
// currently we default it to host from window.location (received from browser)
|
||||
issuer := siteUrl.Host
|
||||
|
||||
return saml.PrepareRequest(issuer, acs, sourceUrl, od.GetSAMLEntityID(), od.GetSAMLIdpURL(), od.GetSAMLCert())
|
||||
return ssotypes.PrepareRequest(issuer, acs, sourceUrl, od.GetSAMLEntityID(), od.GetSAMLIdpURL(), od.GetSAMLCert())
|
||||
}
|
||||
|
||||
func (od *GettableOrgDomain) BuildSsoUrl(siteUrl *url.URL) (ssoUrl string, err error) {
|
87
pkg/types/invite.go
Normal file
87
pkg/types/invite.go
Normal file
@ -0,0 +1,87 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInviteAlreadyExists = errors.MustNewCode("invite_already_exists")
|
||||
ErrInviteNotFound = errors.MustNewCode("invite_not_found")
|
||||
)
|
||||
|
||||
type GettableEEInvite struct {
|
||||
GettableInvite
|
||||
PreCheck *GettableLoginPrecheck `bun:"-" json:"precheck"`
|
||||
}
|
||||
|
||||
type GettableInvite struct {
|
||||
Invite
|
||||
Organization string `bun:"organization,type:text,notnull" json:"organization"`
|
||||
}
|
||||
|
||||
type Invite struct {
|
||||
bun.BaseModel `bun:"table:user_invite"`
|
||||
|
||||
Identifiable
|
||||
TimeAuditable
|
||||
OrgID string `bun:"org_id,type:text,notnull" json:"orgID"`
|
||||
Name string `bun:"name,type:text,notnull" json:"name"`
|
||||
Email string `bun:"email,type:text,notnull,unique" json:"email"`
|
||||
Token string `bun:"token,type:text,notnull" json:"token"`
|
||||
Role string `bun:"role,type:text,notnull" json:"role"`
|
||||
|
||||
InviteLink string `bun:"-" json:"inviteLink"`
|
||||
}
|
||||
|
||||
func NewInvite(orgID, role, name, email string) (*Invite, error) {
|
||||
if email == "" {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "email is required")
|
||||
}
|
||||
_, err := NewRole(role)
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, fmt.Sprintf("invalid role for user: %s", email))
|
||||
}
|
||||
|
||||
email = strings.TrimSpace(email)
|
||||
|
||||
invite := &Invite{
|
||||
Identifiable: Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
TimeAuditable: TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
Name: name,
|
||||
Email: email,
|
||||
Token: valuer.GenerateUUID().String(),
|
||||
Role: role,
|
||||
OrgID: orgID,
|
||||
}
|
||||
|
||||
return invite, nil
|
||||
}
|
||||
|
||||
type InviteEmailData struct {
|
||||
CustomerName string
|
||||
InviterName string
|
||||
InviterEmail string
|
||||
Link string
|
||||
}
|
||||
|
||||
type PostableInvite struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Role Role `json:"role"`
|
||||
FrontendBaseUrl string `json:"frontendBaseUrl"`
|
||||
}
|
||||
|
||||
type PostableBulkInviteRequest struct {
|
||||
Invites []PostableInvite `json:"invites"`
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package authtypes
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
@ -1,34 +1,34 @@
|
||||
package sso
|
||||
package ssotypes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type GoogleOAuthProvider struct {
|
||||
RedirectURI string
|
||||
OAuth2Config *oauth2.Config
|
||||
Verifier *oidc.IDTokenVerifier
|
||||
Cancel context.CancelFunc
|
||||
HostedDomain string
|
||||
RedirectURI string
|
||||
OAuth2Config *oauth2.Config
|
||||
Verifier *oidc.IDTokenVerifier
|
||||
Cancel context.CancelFunc
|
||||
HostedDomain string
|
||||
}
|
||||
|
||||
|
||||
func (g *GoogleOAuthProvider) BuildAuthURL(state string) (string, error) {
|
||||
var opts []oauth2.AuthCodeOption
|
||||
|
||||
|
||||
// set hosted domain. google supports multiple hosted domains but in our case
|
||||
// we have one config per host domain.
|
||||
// we have one config per host domain.
|
||||
opts = append(opts, oauth2.SetAuthURLParam("hd", g.HostedDomain))
|
||||
|
||||
return g.OAuth2Config.AuthCodeURL(state, opts...), nil
|
||||
}
|
||||
|
||||
type oauth2Error struct{
|
||||
type oauth2Error struct {
|
||||
error string
|
||||
errorDescription string
|
||||
}
|
||||
@ -54,7 +54,6 @@ func (g *GoogleOAuthProvider) HandleCallback(r *http.Request) (identity *SSOIden
|
||||
return g.createIdentity(r.Context(), token)
|
||||
}
|
||||
|
||||
|
||||
func (g *GoogleOAuthProvider) createIdentity(ctx context.Context, token *oauth2.Token) (identity *SSOIdentity, err error) {
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
@ -76,7 +75,7 @@ func (g *GoogleOAuthProvider) createIdentity(ctx context.Context, token *oauth2.
|
||||
}
|
||||
|
||||
if claims.HostedDomain != g.HostedDomain {
|
||||
return identity, fmt.Errorf("oidc: unexpected hd claim %v", claims.HostedDomain)
|
||||
return identity, fmt.Errorf("oidc: unexpected hd claim %v", claims.HostedDomain)
|
||||
}
|
||||
|
||||
identity = &SSOIdentity{
|
||||
@ -89,4 +88,3 @@ func (g *GoogleOAuthProvider) createIdentity(ctx context.Context, token *oauth2.
|
||||
|
||||
return identity, nil
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package saml
|
||||
package ssotypes
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
@ -1,15 +1,41 @@
|
||||
package types
|
||||
package ssotypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/sso"
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// SSOIdentity contains details of user received from SSO provider
|
||||
type SSOIdentity struct {
|
||||
UserID string
|
||||
Username string
|
||||
PreferredUsername string
|
||||
Email string
|
||||
EmailVerified bool
|
||||
ConnectorData []byte
|
||||
}
|
||||
|
||||
// OAuthCallbackProvider is an interface implemented by connectors which use an OAuth
|
||||
// style redirect flow to determine user information.
|
||||
type OAuthCallbackProvider interface {
|
||||
// The initial URL user would be redirect to.
|
||||
// OAuth2 implementations support various scopes but we only need profile and user as
|
||||
// the roles are still being managed in SigNoz.
|
||||
BuildAuthURL(state string) (string, error)
|
||||
|
||||
// Handle the callback to the server (after login at oauth provider site)
|
||||
// and return a email identity.
|
||||
// At the moment we dont support auto signup flow (based on domain), so
|
||||
// the full identity (including name, group etc) is not required outside of the
|
||||
// connector
|
||||
HandleCallback(r *http.Request) (identity *SSOIdentity, err error)
|
||||
}
|
||||
|
||||
type SamlConfig struct {
|
||||
SamlEntity string `json:"samlEntity"`
|
||||
SamlIdp string `json:"samlIdp"`
|
||||
@ -27,7 +53,7 @@ const (
|
||||
googleIssuerURL = "https://accounts.google.com"
|
||||
)
|
||||
|
||||
func (g *GoogleOAuthConfig) GetProvider(domain string, siteUrl *url.URL) (sso.OAuthCallbackProvider, error) {
|
||||
func (g *GoogleOAuthConfig) GetProvider(domain string, siteUrl *url.URL) (OAuthCallbackProvider, error) {
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
@ -47,7 +73,7 @@ func (g *GoogleOAuthConfig) GetProvider(domain string, siteUrl *url.URL) (sso.OA
|
||||
siteUrl.Host,
|
||||
"api/v1/complete/google")
|
||||
|
||||
return &sso.GoogleOAuthProvider{
|
||||
return &GoogleOAuthProvider{
|
||||
RedirectURI: g.RedirectURI,
|
||||
OAuth2Config: &oauth2.Config{
|
||||
ClientID: g.ClientID,
|
@ -1,19 +1,63 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type Invite struct {
|
||||
bun.BaseModel `bun:"table:user_invite"`
|
||||
const (
|
||||
SSOAvailable = "sso_available"
|
||||
)
|
||||
|
||||
Identifiable
|
||||
TimeAuditable
|
||||
OrgID string `bun:"org_id,type:text,notnull" json:"orgId"`
|
||||
Name string `bun:"name,type:text,notnull" json:"name"`
|
||||
Email string `bun:"email,type:text,notnull,unique" json:"email"`
|
||||
Token string `bun:"token,type:text,notnull" json:"token"`
|
||||
Role string `bun:"role,type:text,notnull" json:"role"`
|
||||
var (
|
||||
ErrUserAlreadyExists = errors.MustNewCode("user_already_exists")
|
||||
ErrPasswordAlreadyExists = errors.MustNewCode("password_already_exists")
|
||||
ErrUserNotFound = errors.MustNewCode("user_not_found")
|
||||
ErrResetPasswordTokenAlreadyExists = errors.MustNewCode("reset_password_token_already_exists")
|
||||
ErrPasswordNotFound = errors.MustNewCode("password_not_found")
|
||||
ErrResetPasswordTokenNotFound = errors.MustNewCode("reset_password_token_not_found")
|
||||
)
|
||||
|
||||
type UserStore interface {
|
||||
// invite
|
||||
CreateBulkInvite(ctx context.Context, invites []*Invite) error
|
||||
ListInvite(ctx context.Context, orgID string) ([]*Invite, error)
|
||||
DeleteInvite(ctx context.Context, orgID string, id valuer.UUID) error
|
||||
GetInviteByToken(ctx context.Context, token string) (*GettableInvite, error)
|
||||
GetInviteByEmailInOrg(ctx context.Context, orgID string, email string) (*Invite, error)
|
||||
|
||||
// user
|
||||
CreateUserWithPassword(ctx context.Context, user *User, password *FactorPassword) (*User, error)
|
||||
CreateUser(ctx context.Context, user *User) error
|
||||
GetUserByID(ctx context.Context, orgID string, id string) (*GettableUser, error)
|
||||
GetUserByEmailInOrg(ctx context.Context, orgID string, email string) (*GettableUser, error)
|
||||
GetUsersByEmail(ctx context.Context, email string) ([]*GettableUser, error)
|
||||
GetUsersByRoleInOrg(ctx context.Context, orgID string, role Role) ([]*GettableUser, error)
|
||||
ListUsers(ctx context.Context, orgID string) ([]*GettableUser, error)
|
||||
UpdateUser(ctx context.Context, orgID string, id string, user *User) (*User, error)
|
||||
DeleteUser(ctx context.Context, orgID string, id string) error
|
||||
|
||||
// password
|
||||
CreatePassword(ctx context.Context, password *FactorPassword) (*FactorPassword, error)
|
||||
CreateResetPasswordToken(ctx context.Context, resetPasswordRequest *ResetPasswordRequest) error
|
||||
GetPasswordByID(ctx context.Context, id string) (*FactorPassword, error)
|
||||
GetPasswordByUserID(ctx context.Context, id string) (*FactorPassword, error)
|
||||
GetResetPassword(ctx context.Context, token string) (*ResetPasswordRequest, error)
|
||||
GetResetPasswordByPasswordID(ctx context.Context, passwordID string) (*ResetPasswordRequest, error)
|
||||
UpdatePassword(ctx context.Context, userID string, password string) error
|
||||
UpdatePasswordAndDeleteResetPasswordEntry(ctx context.Context, userID string, password string) error
|
||||
|
||||
// Auth Domain
|
||||
GetDomainByName(ctx context.Context, name string) (*StorableOrgDomain, error)
|
||||
|
||||
// Temporary func for SSO
|
||||
GetDefaultOrgID(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
type GettableUser struct {
|
||||
@ -24,19 +68,173 @@ type GettableUser struct {
|
||||
type User struct {
|
||||
bun.BaseModel `bun:"table:users"`
|
||||
|
||||
Identifiable
|
||||
TimeAuditable
|
||||
ID string `bun:"id,pk,type:text" json:"id"`
|
||||
Name string `bun:"name,type:text,notnull" json:"name"`
|
||||
Email string `bun:"email,type:text,notnull,unique" json:"email"`
|
||||
Password string `bun:"password,type:text,notnull" json:"-"`
|
||||
ProfilePictureURL string `bun:"profile_picture_url,type:text" json:"profilePictureURL"`
|
||||
Role string `bun:"role,type:text,notnull" json:"role"`
|
||||
OrgID string `bun:"org_id,type:text,notnull" json:"orgId"`
|
||||
DisplayName string `bun:"display_name,type:text,notnull" json:"displayName"`
|
||||
Email string `bun:"email,type:text,notnull,unique:org_email" json:"email"`
|
||||
Role string `bun:"role,type:text,notnull" json:"role"`
|
||||
OrgID string `bun:"org_id,type:text,notnull,unique:org_email,references:org(id),on_delete:CASCADE" json:"orgId"`
|
||||
}
|
||||
|
||||
func NewUser(displayName string, email string, role string, orgID string) (*User, error) {
|
||||
if email == "" {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "email is required")
|
||||
}
|
||||
|
||||
if role == "" {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "role is required")
|
||||
}
|
||||
|
||||
if orgID == "" {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgID is required")
|
||||
}
|
||||
|
||||
return &User{
|
||||
Identifiable: Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
TimeAuditable: TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
DisplayName: displayName,
|
||||
Email: email,
|
||||
Role: role,
|
||||
OrgID: orgID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type PostableRegisterOrgAndAdmin struct {
|
||||
PostableAcceptInvite
|
||||
Name string `json:"name"`
|
||||
OrgID string `json:"orgId"`
|
||||
OrgDisplayName string `json:"orgDisplayName"`
|
||||
OrgName string `json:"orgName"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
type PostableAcceptInvite struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
InviteToken string `json:"token"`
|
||||
Password string `json:"password"`
|
||||
|
||||
// reference URL to track where the register request is coming from
|
||||
SourceURL string `json:"sourceUrl"`
|
||||
}
|
||||
|
||||
func (p *PostableAcceptInvite) Validate() error {
|
||||
if p.InviteToken == "" {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invite token is required")
|
||||
}
|
||||
|
||||
if p.Password == "" || len(p.Password) < 8 {
|
||||
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "password must be at least 8 characters long")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type FactorPassword struct {
|
||||
bun.BaseModel `bun:"table:factor_password"`
|
||||
|
||||
Identifiable
|
||||
TimeAuditable
|
||||
Password string `bun:"password,type:text,notnull" json:"password"`
|
||||
Temporary bool `bun:"temporary,type:boolean,notnull" json:"temporary"`
|
||||
UserID string `bun:"user_id,type:text,notnull,unique,references:user(id)" json:"userId"`
|
||||
}
|
||||
|
||||
func NewFactorPassword(password string) (*FactorPassword, error) {
|
||||
|
||||
if password == "" && len(password) < 8 {
|
||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "password must be at least 8 characters long")
|
||||
}
|
||||
|
||||
password = strings.TrimSpace(password)
|
||||
|
||||
hashedPassword, err := HashPassword(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &FactorPassword{
|
||||
Identifiable: Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
TimeAuditable: TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
Password: hashedPassword,
|
||||
Temporary: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
// bcrypt automatically handles salting and uses a secure work factor
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hashedPassword), nil
|
||||
}
|
||||
|
||||
func ComparePassword(hashedPassword, password string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) == nil
|
||||
}
|
||||
|
||||
type ResetPasswordRequest struct {
|
||||
bun.BaseModel `bun:"table:reset_password_request"`
|
||||
bun.BaseModel `bun:"table:reset_password_token"`
|
||||
|
||||
Identifiable
|
||||
Token string `bun:"token,type:text,notnull" json:"token"`
|
||||
UserID string `bun:"user_id,type:text,notnull" json:"userId"`
|
||||
Token string `bun:"token,type:text,notnull" json:"token"`
|
||||
PasswordID string `bun:"password_id,type:text,notnull,unique,references:factor_password(id)" json:"passwordId"`
|
||||
}
|
||||
|
||||
func NewResetPasswordRequest(passwordID string) (*ResetPasswordRequest, error) {
|
||||
return &ResetPasswordRequest{
|
||||
Identifiable: Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
Token: valuer.GenerateUUID().String(),
|
||||
PasswordID: passwordID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type PostableResetPassword struct {
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type ChangePasswordRequest struct {
|
||||
UserId string `json:"userId"`
|
||||
OldPassword string `json:"oldPassword"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
}
|
||||
|
||||
type PostableLoginRequest struct {
|
||||
OrgID string `json:"orgId"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
}
|
||||
|
||||
type GettableUserJwt struct {
|
||||
AccessJwt string `json:"accessJwt"`
|
||||
AccessJwtExpiry int64 `json:"accessJwtExpiry"`
|
||||
RefreshJwt string `json:"refreshJwt"`
|
||||
RefreshJwtExpiry int64 `json:"refreshJwtExpiry"`
|
||||
}
|
||||
|
||||
type GettableLoginResponse struct {
|
||||
GettableUserJwt
|
||||
UserID string `json:"userId"`
|
||||
}
|
||||
|
||||
type GettableLoginPrecheck struct {
|
||||
SSO bool `json:"sso"`
|
||||
SSOUrl string `json:"ssoUrl"`
|
||||
CanSelfRegister bool `json:"canSelfRegister"`
|
||||
IsUser bool `json:"isUser"`
|
||||
SSOError string `json:"ssoError"`
|
||||
SelectOrg bool `json:"selectOrg"`
|
||||
Orgs []string `json:"orgs"`
|
||||
}
|
||||
|
@ -39,6 +39,6 @@ def get_jwt_token(signoz: types.SigNoz) -> str:
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
return response.json()["accessJwt"]
|
||||
return response.json()["data"]["accessJwt"]
|
||||
|
||||
return _get_jwt_token
|
||||
|
@ -42,7 +42,7 @@ def test_register(signoz: types.SigNoz, get_jwt_token) -> None:
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
user_response = response.json()
|
||||
user_response = response.json()["data"]
|
||||
found_user = next(
|
||||
(user for user in user_response if user["email"] == "admin@integration.test"),
|
||||
None,
|
||||
@ -52,13 +52,13 @@ def test_register(signoz: types.SigNoz, get_jwt_token) -> None:
|
||||
assert found_user["role"] == "ADMIN"
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_config.get(f"/api/v1/rbac/role/{found_user["id"]}"),
|
||||
signoz.self.host_config.get(f"/api/v1/user/{found_user["id"]}"),
|
||||
timeout=2,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["group_name"] == "ADMIN"
|
||||
assert response.json()["data"]["role"] == "ADMIN"
|
||||
|
||||
|
||||
def test_invite_and_register(signoz: types.SigNoz, get_jwt_token) -> None:
|
||||
@ -72,22 +72,29 @@ def test_invite_and_register(signoz: types.SigNoz, get_jwt_token) -> None:
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.status_code == HTTPStatus.CREATED
|
||||
|
||||
invite_response = response.json()
|
||||
assert "email" in invite_response
|
||||
assert "inviteToken" in invite_response
|
||||
response = requests.get(
|
||||
signoz.self.host_config.get("/api/v1/invite"),
|
||||
timeout=2,
|
||||
headers={
|
||||
"Authorization": f"Bearer {get_jwt_token("admin@integration.test", "password")}" # pylint: disable=line-too-long
|
||||
},
|
||||
)
|
||||
|
||||
assert invite_response["email"] == "editor@integration.test"
|
||||
invite_response = response.json()["data"]
|
||||
found_invite = next(
|
||||
(invite for invite in invite_response if invite["email"] == "editor@integration.test"),
|
||||
None,
|
||||
)
|
||||
|
||||
# Register the editor user using the invite token
|
||||
response = requests.post(
|
||||
signoz.self.host_config.get("/api/v1/register"),
|
||||
signoz.self.host_config.get("/api/v1/invite/accept"),
|
||||
json={
|
||||
"email": "editor@integration.test",
|
||||
"password": "password",
|
||||
"name": "editor",
|
||||
"token": f"{invite_response["inviteToken"]}",
|
||||
"displayName": "editor",
|
||||
"token": f"{found_invite['token']}",
|
||||
},
|
||||
timeout=2,
|
||||
)
|
||||
@ -96,7 +103,7 @@ def test_invite_and_register(signoz: types.SigNoz, get_jwt_token) -> None:
|
||||
# Verify that the invite token has been deleted
|
||||
response = requests.get(
|
||||
signoz.self.host_config.get(
|
||||
f"/api/v1/invite/{invite_response["inviteToken"]}"
|
||||
f"/api/v1/invite/{found_invite['token']}"
|
||||
), # pylint: disable=line-too-long
|
||||
timeout=2,
|
||||
)
|
||||
@ -125,7 +132,7 @@ def test_invite_and_register(signoz: types.SigNoz, get_jwt_token) -> None:
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
user_response = response.json()
|
||||
user_response = response.json()["data"]
|
||||
found_user = next(
|
||||
(user for user in user_response if user["email"] == "editor@integration.test"),
|
||||
None,
|
||||
@ -133,7 +140,7 @@ def test_invite_and_register(signoz: types.SigNoz, get_jwt_token) -> None:
|
||||
|
||||
assert found_user is not None
|
||||
assert found_user["role"] == "EDITOR"
|
||||
assert found_user["name"] == "editor"
|
||||
assert found_user["displayName"] == "editor"
|
||||
assert found_user["email"] == "editor@integration.test"
|
||||
|
||||
|
||||
@ -149,28 +156,37 @@ def test_revoke_invite_and_register(signoz: types.SigNoz, get_jwt_token) -> None
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.status_code == HTTPStatus.CREATED
|
||||
|
||||
invite_response = response.json()
|
||||
assert "email" in invite_response
|
||||
assert "inviteToken" in invite_response
|
||||
response = requests.get(
|
||||
signoz.self.host_config.get("/api/v1/invite"),
|
||||
timeout=2,
|
||||
headers={
|
||||
"Authorization": f"Bearer {get_jwt_token("admin@integration.test", "password")}" # pylint: disable=line-too-long
|
||||
},
|
||||
)
|
||||
|
||||
invite_response = response.json()["data"]
|
||||
found_invite = next(
|
||||
(invite for invite in invite_response if invite["email"] == "viewer@integration.test"),
|
||||
None,
|
||||
)
|
||||
|
||||
response = requests.delete(
|
||||
signoz.self.host_config.get(f"/api/v1/invite/{invite_response['email']}"),
|
||||
signoz.self.host_config.get(f"/api/v1/invite/{found_invite['id']}"),
|
||||
timeout=2,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.status_code == HTTPStatus.NO_CONTENT
|
||||
|
||||
# Try registering the viewer user with the invite token
|
||||
response = requests.post(
|
||||
signoz.self.host_config.get("/api/v1/register"),
|
||||
signoz.self.host_config.get("/api/v1/invite/accept"),
|
||||
json={
|
||||
"email": "viewer@integration.test",
|
||||
"password": "password",
|
||||
"name": "viewer",
|
||||
"token": f"{invite_response["inviteToken"]}",
|
||||
"displayName": "viewer",
|
||||
"token": f"{found_invite["token"]}",
|
||||
},
|
||||
timeout=2,
|
||||
)
|
||||
@ -189,17 +205,17 @@ def test_self_access(signoz: types.SigNoz, get_jwt_token) -> None:
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
user_response = response.json()
|
||||
user_response = response.json()["data"]
|
||||
found_user = next(
|
||||
(user for user in user_response if user["email"] == "editor@integration.test"),
|
||||
None,
|
||||
)
|
||||
|
||||
response = requests.get(
|
||||
signoz.self.host_config.get(f"/api/v1/rbac/role/{found_user['id']}"),
|
||||
signoz.self.host_config.get(f"/api/v1/user/{found_user['id']}"),
|
||||
timeout=2,
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["group_name"] == "EDITOR"
|
||||
assert response.json()["data"]["role"] == "EDITOR"
|
||||
|
@ -33,7 +33,7 @@ def test_api_key(signoz: types.SigNoz, get_jwt_token) -> None:
|
||||
|
||||
user_response = response.json()
|
||||
found_user = next(
|
||||
(user for user in user_response if user["email"] == "admin@integration.test"),
|
||||
(user for user in user_response["data"] if user["email"] == "admin@integration.test"),
|
||||
None,
|
||||
)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user