mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 04:49:01 +08:00
chore: update API key (#7959)
* chore: update API key * fix: delete api key on user delete * fix: migration * fix: api * fix: address comments * fix: address comments * fix: update structs * fix: minor changes * fix: error message * fix: address comments * fix: integration tests * fix: minor issues * fix: integration tests
This commit is contained in:
parent
8d4c4dc5f2
commit
824302be38
@ -4,7 +4,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
eeTypes "github.com/SigNoz/signoz/ee/types"
|
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
@ -25,7 +24,7 @@ func (p *Pat) Wrap(next http.Handler) http.Handler {
|
|||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var values []string
|
var values []string
|
||||||
var patToken string
|
var patToken string
|
||||||
var pat eeTypes.StorablePersonalAccessToken
|
var pat types.StorableAPIKey
|
||||||
|
|
||||||
for _, header := range p.headers {
|
for _, header := range p.headers {
|
||||||
values = append(values, r.Header.Get(header))
|
values = append(values, r.Header.Get(header))
|
||||||
@ -48,7 +47,7 @@ func (p *Pat) Wrap(next http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if pat.ExpiresAt < time.Now().Unix() && pat.ExpiresAt != 0 {
|
if pat.ExpiresAt.Before(time.Now()) {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -61,15 +60,9 @@ func (p *Pat) Wrap(next http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
role, err := types.NewRole(user.Role)
|
|
||||||
if err != nil {
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt := authtypes.Claims{
|
jwt := authtypes.Claims{
|
||||||
UserID: user.ID.String(),
|
UserID: user.ID.String(),
|
||||||
Role: role,
|
Role: pat.Role,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
OrgID: user.OrgID,
|
OrgID: user.OrgID,
|
||||||
}
|
}
|
||||||
@ -80,7 +73,7 @@ func (p *Pat) Wrap(next http.Handler) http.Handler {
|
|||||||
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
|
|
||||||
pat.LastUsed = time.Now().Unix()
|
pat.LastUsed = time.Now()
|
||||||
_, err = p.store.BunDB().NewUpdate().Model(&pat).Column("last_used").Where("token = ?", patToken).Where("revoked = false").Exec(r.Context())
|
_, err = p.store.BunDB().NewUpdate().Model(&pat).Column("last_used").Where("token = ?", patToken).Where("revoked = false").Exec(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("Failed to update PAT last used in db, err: %v", zap.Error(err))
|
zap.L().Error("Failed to update PAT last used in db, err: %v", zap.Error(err))
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
@ -11,6 +12,8 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -201,3 +204,202 @@ func (h *Handler) GetInvite(w http.ResponseWriter, r *http.Request) {
|
|||||||
render.Success(w, http.StatusOK, gettableInvite)
|
render.Success(w, http.StatusOK, gettableInvite)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) CreateAPIKey(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
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := valuer.NewUUID(claims.UserID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "userId is not a valid uuid-v7"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(types.PostableAPIKey)
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
|
||||||
|
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode api key"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKey, err := types.NewStorableAPIKey(
|
||||||
|
req.Name,
|
||||||
|
userID,
|
||||||
|
req.Role,
|
||||||
|
req.ExpiresInDays,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.module.CreateAPIKey(ctx, apiKey)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// just corrected the status code, response is same,
|
||||||
|
render.Success(w, http.StatusCreated, apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ListAPIKeys(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
|
||||||
|
}
|
||||||
|
|
||||||
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is not a valid uuid-v7"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKeys, err := h.module.ListAPIKeys(ctx, orgID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// for backward compatibility
|
||||||
|
if len(apiKeys) == 0 {
|
||||||
|
render.Success(w, http.StatusOK, []types.GettableAPIKey{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]*types.GettableAPIKey, len(apiKeys))
|
||||||
|
for i, apiKey := range apiKeys {
|
||||||
|
result[i] = types.NewGettableAPIKeyFromStorableAPIKey(apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Success(w, http.StatusOK, result)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) UpdateAPIKey(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
|
||||||
|
}
|
||||||
|
|
||||||
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is not a valid uuid-v7"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := valuer.NewUUID(claims.UserID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "userId is not a valid uuid-v7"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := types.StorableAPIKey{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode api key"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
idStr := mux.Vars(r)["id"]
|
||||||
|
id, err := valuer.NewUUID(idStr)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//get the API Key
|
||||||
|
existingAPIKey, err := h.module.GetAPIKey(ctx, orgID, id)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the user
|
||||||
|
createdByUser, err := h.module.GetUserByID(ctx, orgID.String(), existingAPIKey.UserID.String())
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(createdByUser.Email)) {
|
||||||
|
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "API Keys for integration users cannot be revoked"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.module.UpdateAPIKey(ctx, id, &req, userID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Success(w, http.StatusNoContent, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) RevokeAPIKey(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
|
||||||
|
}
|
||||||
|
|
||||||
|
idStr := mux.Vars(r)["id"]
|
||||||
|
id, err := valuer.NewUUID(idStr)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is not a valid uuid-v7"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := valuer.NewUUID(claims.UserID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "userId is not a valid uuid-v7"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//get the API Key
|
||||||
|
existingAPIKey, err := h.module.GetAPIKey(ctx, orgID, id)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the user
|
||||||
|
createdByUser, err := h.module.GetUserByID(ctx, orgID.String(), existingAPIKey.UserID.String())
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(createdByUser.Email)) {
|
||||||
|
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "API Keys for integration users cannot be revoked"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.module.RevokeAPIKey(ctx, id, userID); err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Success(w, http.StatusNoContent, nil)
|
||||||
|
}
|
||||||
|
@ -12,17 +12,18 @@ import (
|
|||||||
baseimpl "github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
baseimpl "github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EnterpriseModule embeds the base module implementation
|
// EnterpriseModule embeds the base module implementation
|
||||||
type Module struct {
|
type Module struct {
|
||||||
*baseimpl.Module // Embed the base module implementation
|
user.Module // Embed the base module implementation
|
||||||
store types.UserStore
|
store types.UserStore
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModule(store types.UserStore) user.Module {
|
func NewModule(store types.UserStore) user.Module {
|
||||||
baseModule := baseimpl.NewModule(store).(*baseimpl.Module)
|
baseModule := baseimpl.NewModule(store)
|
||||||
return &Module{
|
return &Module{
|
||||||
Module: baseModule,
|
Module: baseModule,
|
||||||
store: store,
|
store: store,
|
||||||
@ -227,3 +228,23 @@ func (m *Module) GetAuthDomainByEmail(ctx context.Context, email string) (*types
|
|||||||
}
|
}
|
||||||
return gettableDomain, nil
|
return gettableDomain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Module) CreateAPIKey(ctx context.Context, apiKey *types.StorableAPIKey) error {
|
||||||
|
return m.store.CreateAPIKey(ctx, apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) UpdateAPIKey(ctx context.Context, id valuer.UUID, apiKey *types.StorableAPIKey, updaterID valuer.UUID) error {
|
||||||
|
return m.store.UpdateAPIKey(ctx, id, apiKey, updaterID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*types.StorableAPIKeyUser, error) {
|
||||||
|
return m.store.ListAPIKeys(ctx, orgID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) GetAPIKey(ctx context.Context, orgID, id valuer.UUID) (*types.StorableAPIKeyUser, error) {
|
||||||
|
return m.store.GetAPIKey(ctx, orgID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) RevokeAPIKey(ctx context.Context, id, removedByUserID valuer.UUID) error {
|
||||||
|
return m.store.RevokeAPIKey(ctx, id, removedByUserID)
|
||||||
|
}
|
||||||
|
@ -146,10 +146,10 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
|||||||
router.HandleFunc("/api/v1/login", am.OpenAccess(ah.loginUser)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v1/login", am.OpenAccess(ah.loginUser)).Methods(http.MethodPost)
|
||||||
|
|
||||||
// PAT APIs
|
// PAT APIs
|
||||||
router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.createPAT)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.Signoz.Handlers.User.CreateAPIKey)).Methods(http.MethodPost)
|
||||||
router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.getPATs)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.Signoz.Handlers.User.ListAPIKeys)).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/api/v1/pats/{id}", am.AdminAccess(ah.updatePAT)).Methods(http.MethodPut)
|
router.HandleFunc("/api/v1/pats/{id}", am.AdminAccess(ah.Signoz.Handlers.User.UpdateAPIKey)).Methods(http.MethodPut)
|
||||||
router.HandleFunc("/api/v1/pats/{id}", am.AdminAccess(ah.revokePAT)).Methods(http.MethodDelete)
|
router.HandleFunc("/api/v1/pats/{id}", am.AdminAccess(ah.Signoz.Handlers.User.RevokeAPIKey)).Methods(http.MethodDelete)
|
||||||
|
|
||||||
router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.checkout)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.checkout)).Methods(http.MethodPost)
|
||||||
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
|
||||||
|
@ -11,12 +11,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
"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/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/http/render"
|
"github.com/SigNoz/signoz/pkg/http/render"
|
||||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@ -116,14 +116,21 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
|
|||||||
return "", apiErr
|
return "", apiErr
|
||||||
}
|
}
|
||||||
|
|
||||||
allPats, err := ah.AppDao().ListPATs(ctx, orgId)
|
orgIdUUID, err := valuer.NewUUID(orgId)
|
||||||
|
if err != nil {
|
||||||
|
return "", basemodel.InternalError(fmt.Errorf(
|
||||||
|
"couldn't parse orgId: %w", err,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
allPats, err := ah.Signoz.Modules.User.ListAPIKeys(ctx, orgIdUUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", basemodel.InternalError(fmt.Errorf(
|
return "", basemodel.InternalError(fmt.Errorf(
|
||||||
"couldn't list PATs: %w", err,
|
"couldn't list PATs: %w", err,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
for _, p := range allPats {
|
for _, p := range allPats {
|
||||||
if p.UserID == integrationUser.ID.String() && p.Name == integrationPATName {
|
if p.UserID == integrationUser.ID && p.Name == integrationPATName {
|
||||||
return p.Token, nil
|
return p.Token, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,19 +140,25 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
|
|||||||
zap.String("cloudProvider", cloudProvider),
|
zap.String("cloudProvider", cloudProvider),
|
||||||
)
|
)
|
||||||
|
|
||||||
newPAT := eeTypes.NewGettablePAT(
|
newPAT, err := types.NewStorableAPIKey(
|
||||||
integrationPATName,
|
integrationPATName,
|
||||||
types.RoleViewer.String(),
|
integrationUser.ID,
|
||||||
integrationUser.ID.String(),
|
types.RoleViewer,
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
integrationPAT, err := ah.AppDao().CreatePAT(ctx, orgId, newPAT)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", basemodel.InternalError(fmt.Errorf(
|
return "", basemodel.InternalError(fmt.Errorf(
|
||||||
"couldn't create cloud integration PAT: %w", err,
|
"couldn't create cloud integration PAT: %w", err,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
return integrationPAT.Token, nil
|
|
||||||
|
err = ah.Signoz.Modules.User.CreateAPIKey(ctx, newPAT)
|
||||||
|
if err != nil {
|
||||||
|
return "", basemodel.InternalError(fmt.Errorf(
|
||||||
|
"couldn't create cloud integration PAT: %w", err,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return newPAT.Token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
||||||
|
@ -1,186 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"slices"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
|
||||||
eeTypes "github.com/SigNoz/signoz/ee/types"
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
|
||||||
errorsV2 "github.com/SigNoz/signoz/pkg/errors"
|
|
||||||
"github.com/SigNoz/signoz/pkg/http/render"
|
|
||||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
|
|
||||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req := model.CreatePATRequestBody{}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
RespondError(w, model.BadRequest(err), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pat := eeTypes.NewGettablePAT(
|
|
||||||
req.Name,
|
|
||||||
req.Role,
|
|
||||||
claims.UserID,
|
|
||||||
req.ExpiresInDays,
|
|
||||||
)
|
|
||||||
err = validatePATRequest(pat)
|
|
||||||
if err != nil {
|
|
||||||
RespondError(w, model.BadRequest(err), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
zap.L().Info("Got Create PAT request", zap.Any("pat", pat))
|
|
||||||
var apierr basemodel.BaseApiError
|
|
||||||
if pat, apierr = ah.AppDao().CreatePAT(r.Context(), claims.OrgID, pat); apierr != nil {
|
|
||||||
RespondError(w, apierr, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ah.Respond(w, &pat)
|
|
||||||
}
|
|
||||||
|
|
||||||
func validatePATRequest(req eeTypes.GettablePAT) error {
|
|
||||||
_, err := types.NewRole(req.Role)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.ExpiresAt < 0 {
|
|
||||||
return fmt.Errorf("valid expiresAt is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Name == "" {
|
|
||||||
return fmt.Errorf("valid name is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
|
|
||||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req := eeTypes.GettablePAT{}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
RespondError(w, model.BadRequest(err), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
idStr := mux.Vars(r)["id"]
|
|
||||||
id, err := valuer.NewUUID(idStr)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//get the pat
|
|
||||||
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, err := ah.Signoz.Modules.User.GetUserByID(r.Context(), claims.OrgID, existingPAT.UserID)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(createdByUser.Email)) {
|
|
||||||
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, "integration user pat cannot be updated"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = validatePATRequest(req)
|
|
||||||
if err != nil {
|
|
||||||
RespondError(w, model.BadRequest(err), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req.UpdatedByUserID = claims.UserID
|
|
||||||
req.UpdatedAt = time.Now()
|
|
||||||
var apierr basemodel.BaseApiError
|
|
||||||
if apierr = ah.AppDao().UpdatePAT(r.Context(), claims.OrgID, req, id); apierr != nil {
|
|
||||||
RespondError(w, apierr, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ah.Respond(w, map[string]string{"data": "pat updated successfully"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) getPATs(w http.ResponseWriter, r *http.Request) {
|
|
||||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pats, apierr := ah.AppDao().ListPATs(r.Context(), claims.OrgID)
|
|
||||||
if apierr != nil {
|
|
||||||
RespondError(w, apierr, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ah.Respond(w, pats)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) revokePAT(w http.ResponseWriter, r *http.Request) {
|
|
||||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
idStr := mux.Vars(r)["id"]
|
|
||||||
id, err := valuer.NewUUID(idStr)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//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()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the user
|
|
||||||
createdByUser, err := ah.Signoz.Modules.User.GetUserByID(r.Context(), claims.OrgID, existingPAT.UserID)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(createdByUser.Email)) {
|
|
||||||
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, "integration user pat cannot be updated"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
zap.L().Info("Revoke PAT with id", zap.String("id", id.StringValue()))
|
|
||||||
if apierr := ah.AppDao().RevokePAT(r.Context(), claims.OrgID, id, claims.UserID); apierr != nil {
|
|
||||||
RespondError(w, apierr, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ah.Respond(w, map[string]string{"data": "pat revoked successfully"})
|
|
||||||
}
|
|
@ -4,10 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
eeTypes "github.com/SigNoz/signoz/ee/types"
|
|
||||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -22,11 +20,4 @@ type ModelDao interface {
|
|||||||
UpdateDomain(ctx context.Context, domain *types.GettableOrgDomain) basemodel.BaseApiError
|
UpdateDomain(ctx context.Context, domain *types.GettableOrgDomain) basemodel.BaseApiError
|
||||||
DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.BaseApiError
|
DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.BaseApiError
|
||||||
GetDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, basemodel.BaseApiError)
|
GetDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, 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,201 +0,0 @@
|
|||||||
package sqlite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
|
||||||
"github.com/SigNoz/signoz/ee/types"
|
|
||||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
|
||||||
ossTypes "github.com/SigNoz/signoz/pkg/types"
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m *modelDao) CreatePAT(ctx context.Context, orgID string, p types.GettablePAT) (types.GettablePAT, basemodel.BaseApiError) {
|
|
||||||
p.StorablePersonalAccessToken.OrgID = orgID
|
|
||||||
p.StorablePersonalAccessToken.ID = valuer.GenerateUUID()
|
|
||||||
_, err := m.sqlStore.BunDB().NewInsert().
|
|
||||||
Model(&p.StorablePersonalAccessToken).
|
|
||||||
Exec(ctx)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("Failed to insert PAT in db, err: %v", zap.Error(err))
|
|
||||||
return types.GettablePAT{}, model.InternalError(fmt.Errorf("PAT insertion failed"))
|
|
||||||
}
|
|
||||||
|
|
||||||
createdByUser, _ := m.userModule.GetUserByID(ctx, orgID, p.UserID)
|
|
||||||
if createdByUser == nil {
|
|
||||||
p.CreatedByUser = types.PatUser{
|
|
||||||
NotFound: true,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
p.CreatedByUser = types.PatUser{
|
|
||||||
User: ossTypes.User{
|
|
||||||
Identifiable: ossTypes.Identifiable{
|
|
||||||
ID: createdByUser.ID,
|
|
||||||
},
|
|
||||||
DisplayName: createdByUser.DisplayName,
|
|
||||||
Email: createdByUser.Email,
|
|
||||||
TimeAuditable: ossTypes.TimeAuditable{
|
|
||||||
CreatedAt: createdByUser.CreatedAt,
|
|
||||||
UpdatedAt: createdByUser.UpdatedAt,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
NotFound: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modelDao) UpdatePAT(ctx context.Context, orgID string, p types.GettablePAT, id valuer.UUID) basemodel.BaseApiError {
|
|
||||||
_, err := m.sqlStore.BunDB().NewUpdate().
|
|
||||||
Model(&p.StorablePersonalAccessToken).
|
|
||||||
Column("role", "name", "updated_at", "updated_by_user_id").
|
|
||||||
Where("id = ?", id.StringValue()).
|
|
||||||
Where("org_id = ?", orgID).
|
|
||||||
Where("revoked = false").
|
|
||||||
Exec(ctx)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("Failed to update PAT in db, err: %v", zap.Error(err))
|
|
||||||
return model.InternalError(fmt.Errorf("PAT update failed"))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modelDao) ListPATs(ctx context.Context, orgID string) ([]types.GettablePAT, basemodel.BaseApiError) {
|
|
||||||
pats := []types.StorablePersonalAccessToken{}
|
|
||||||
|
|
||||||
if err := m.sqlStore.BunDB().NewSelect().
|
|
||||||
Model(&pats).
|
|
||||||
Where("revoked = false").
|
|
||||||
Where("org_id = ?", orgID).
|
|
||||||
Order("updated_at DESC").
|
|
||||||
Scan(ctx); err != nil {
|
|
||||||
zap.L().Error("Failed to fetch PATs err: %v", zap.Error(err))
|
|
||||||
return nil, model.InternalError(fmt.Errorf("failed to fetch PATs"))
|
|
||||||
}
|
|
||||||
|
|
||||||
patsWithUsers := []types.GettablePAT{}
|
|
||||||
for i := range pats {
|
|
||||||
patWithUser := types.GettablePAT{
|
|
||||||
StorablePersonalAccessToken: pats[i],
|
|
||||||
}
|
|
||||||
|
|
||||||
createdByUser, _ := m.userModule.GetUserByID(ctx, orgID, pats[i].UserID)
|
|
||||||
if createdByUser == nil {
|
|
||||||
patWithUser.CreatedByUser = types.PatUser{
|
|
||||||
NotFound: true,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
patWithUser.CreatedByUser = types.PatUser{
|
|
||||||
User: ossTypes.User{
|
|
||||||
Identifiable: ossTypes.Identifiable{
|
|
||||||
ID: createdByUser.ID,
|
|
||||||
},
|
|
||||||
DisplayName: createdByUser.DisplayName,
|
|
||||||
Email: createdByUser.Email,
|
|
||||||
TimeAuditable: ossTypes.TimeAuditable{
|
|
||||||
CreatedAt: createdByUser.CreatedAt,
|
|
||||||
UpdatedAt: createdByUser.UpdatedAt,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
NotFound: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedByUser, _ := m.userModule.GetUserByID(ctx, orgID, pats[i].UpdatedByUserID)
|
|
||||||
if updatedByUser == nil {
|
|
||||||
patWithUser.UpdatedByUser = types.PatUser{
|
|
||||||
NotFound: true,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
patWithUser.UpdatedByUser = types.PatUser{
|
|
||||||
User: ossTypes.User{
|
|
||||||
Identifiable: ossTypes.Identifiable{
|
|
||||||
ID: updatedByUser.ID,
|
|
||||||
},
|
|
||||||
DisplayName: updatedByUser.DisplayName,
|
|
||||||
Email: updatedByUser.Email,
|
|
||||||
TimeAuditable: ossTypes.TimeAuditable{
|
|
||||||
CreatedAt: updatedByUser.CreatedAt,
|
|
||||||
UpdatedAt: updatedByUser.UpdatedAt,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
NotFound: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
patsWithUsers = append(patsWithUsers, patWithUser)
|
|
||||||
}
|
|
||||||
return patsWithUsers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modelDao) RevokePAT(ctx context.Context, orgID string, id valuer.UUID, userID string) basemodel.BaseApiError {
|
|
||||||
updatedAt := time.Now().Unix()
|
|
||||||
_, err := m.sqlStore.BunDB().NewUpdate().
|
|
||||||
Model(&types.StorablePersonalAccessToken{}).
|
|
||||||
Set("revoked = ?", true).
|
|
||||||
Set("updated_by_user_id = ?", userID).
|
|
||||||
Set("updated_at = ?", updatedAt).
|
|
||||||
Where("id = ?", id.StringValue()).
|
|
||||||
Where("org_id = ?", orgID).
|
|
||||||
Exec(ctx)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("Failed to revoke PAT in db, err: %v", zap.Error(err))
|
|
||||||
return model.InternalError(fmt.Errorf("PAT revoke failed"))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modelDao) GetPAT(ctx context.Context, token string) (*types.GettablePAT, basemodel.BaseApiError) {
|
|
||||||
pats := []types.StorablePersonalAccessToken{}
|
|
||||||
|
|
||||||
if err := m.sqlStore.BunDB().NewSelect().
|
|
||||||
Model(&pats).
|
|
||||||
Where("token = ?", token).
|
|
||||||
Where("revoked = false").
|
|
||||||
Scan(ctx); err != nil {
|
|
||||||
return nil, model.InternalError(fmt.Errorf("failed to fetch PAT"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(pats) != 1 {
|
|
||||||
return nil, &model.ApiError{
|
|
||||||
Typ: model.ErrorInternal,
|
|
||||||
Err: fmt.Errorf("found zero or multiple PATs with same token, %s", token),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
patWithUser := types.GettablePAT{
|
|
||||||
StorablePersonalAccessToken: pats[0],
|
|
||||||
}
|
|
||||||
|
|
||||||
return &patWithUser, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modelDao) GetPATByID(ctx context.Context, orgID string, id valuer.UUID) (*types.GettablePAT, basemodel.BaseApiError) {
|
|
||||||
pats := []types.StorablePersonalAccessToken{}
|
|
||||||
|
|
||||||
if err := m.sqlStore.BunDB().NewSelect().
|
|
||||||
Model(&pats).
|
|
||||||
Where("id = ?", id.StringValue()).
|
|
||||||
Where("org_id = ?", orgID).
|
|
||||||
Where("revoked = false").
|
|
||||||
Scan(ctx); err != nil {
|
|
||||||
return nil, model.InternalError(fmt.Errorf("failed to fetch PAT"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(pats) != 1 {
|
|
||||||
return nil, &model.ApiError{
|
|
||||||
Typ: model.ErrorInternal,
|
|
||||||
Err: fmt.Errorf("found zero or multiple PATs with same token"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
patWithUser := types.GettablePAT{
|
|
||||||
StorablePersonalAccessToken: pats[0],
|
|
||||||
}
|
|
||||||
|
|
||||||
return &patWithUser, nil
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
type CreatePATRequestBody struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Role string `json:"role"`
|
|
||||||
ExpiresInDays int64 `json:"expiresInDays"`
|
|
||||||
}
|
|
@ -19,6 +19,7 @@ var (
|
|||||||
var (
|
var (
|
||||||
Org = "org"
|
Org = "org"
|
||||||
User = "user"
|
User = "user"
|
||||||
|
UserNoCascade = "user_no_cascade"
|
||||||
FactorPassword = "factor_password"
|
FactorPassword = "factor_password"
|
||||||
CloudIntegration = "cloud_integration"
|
CloudIntegration = "cloud_integration"
|
||||||
)
|
)
|
||||||
@ -26,6 +27,7 @@ var (
|
|||||||
var (
|
var (
|
||||||
OrgReference = `("org_id") REFERENCES "organizations" ("id")`
|
OrgReference = `("org_id") REFERENCES "organizations" ("id")`
|
||||||
UserReference = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE`
|
UserReference = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE`
|
||||||
|
UserReferenceNoCascade = `("user_id") REFERENCES "users" ("id")`
|
||||||
FactorPasswordReference = `("password_id") REFERENCES "factor_password" ("id")`
|
FactorPasswordReference = `("password_id") REFERENCES "factor_password" ("id")`
|
||||||
CloudIntegrationReference = `("cloud_integration_id") REFERENCES "cloud_integration" ("id") ON DELETE CASCADE`
|
CloudIntegrationReference = `("cloud_integration_id") REFERENCES "cloud_integration" ("id") ON DELETE CASCADE`
|
||||||
)
|
)
|
||||||
@ -266,6 +268,8 @@ func (dialect *dialect) RenameTableAndModifyModel(ctx context.Context, bun bun.I
|
|||||||
fkReferences = append(fkReferences, OrgReference)
|
fkReferences = append(fkReferences, OrgReference)
|
||||||
} else if reference == User && !slices.Contains(fkReferences, UserReference) {
|
} else if reference == User && !slices.Contains(fkReferences, UserReference) {
|
||||||
fkReferences = append(fkReferences, UserReference)
|
fkReferences = append(fkReferences, UserReference)
|
||||||
|
} else if reference == UserNoCascade && !slices.Contains(fkReferences, UserReferenceNoCascade) {
|
||||||
|
fkReferences = append(fkReferences, UserReferenceNoCascade)
|
||||||
} else if reference == FactorPassword && !slices.Contains(fkReferences, FactorPasswordReference) {
|
} else if reference == FactorPassword && !slices.Contains(fkReferences, FactorPasswordReference) {
|
||||||
fkReferences = append(fkReferences, FactorPasswordReference)
|
fkReferences = append(fkReferences, FactorPasswordReference)
|
||||||
} else if reference == CloudIntegration && !slices.Contains(fkReferences, CloudIntegrationReference) {
|
} else if reference == CloudIntegration && !slices.Contains(fkReferences, CloudIntegrationReference) {
|
||||||
|
@ -1,76 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
|
||||||
"github.com/uptrace/bun"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GettablePAT struct {
|
|
||||||
CreatedByUser PatUser `json:"createdByUser"`
|
|
||||||
UpdatedByUser PatUser `json:"updatedByUser"`
|
|
||||||
|
|
||||||
StorablePersonalAccessToken
|
|
||||||
}
|
|
||||||
|
|
||||||
type PatUser struct {
|
|
||||||
types.User
|
|
||||||
NotFound bool `json:"notFound"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewGettablePAT(name, role, userID string, expiresAt int64) GettablePAT {
|
|
||||||
return GettablePAT{
|
|
||||||
StorablePersonalAccessToken: NewStorablePersonalAccessToken(name, role, userID, expiresAt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type StorablePersonalAccessToken struct {
|
|
||||||
bun.BaseModel `bun:"table:personal_access_token"`
|
|
||||||
types.Identifiable
|
|
||||||
types.TimeAuditable
|
|
||||||
OrgID string `json:"orgId" bun:"org_id,type:text,notnull"`
|
|
||||||
Role string `json:"role" bun:"role,type:text,notnull,default:'ADMIN'"`
|
|
||||||
UserID string `json:"userId" bun:"user_id,type:text,notnull"`
|
|
||||||
Token string `json:"token" bun:"token,type:text,notnull,unique"`
|
|
||||||
Name string `json:"name" bun:"name,type:text,notnull"`
|
|
||||||
ExpiresAt int64 `json:"expiresAt" bun:"expires_at,notnull,default:0"`
|
|
||||||
LastUsed int64 `json:"lastUsed" bun:"last_used,notnull,default:0"`
|
|
||||||
Revoked bool `json:"revoked" bun:"revoked,notnull,default:false"`
|
|
||||||
UpdatedByUserID string `json:"updatedByUserId" bun:"updated_by_user_id,type:text,notnull,default:''"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStorablePersonalAccessToken(name, role, userID string, expiresAt int64) StorablePersonalAccessToken {
|
|
||||||
now := time.Now()
|
|
||||||
if expiresAt != 0 {
|
|
||||||
// convert expiresAt to unix timestamp from days
|
|
||||||
expiresAt = now.Unix() + (expiresAt * 24 * 60 * 60)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a 32-byte random token.
|
|
||||||
token := make([]byte, 32)
|
|
||||||
rand.Read(token)
|
|
||||||
// Encode the token in base64.
|
|
||||||
encodedToken := base64.StdEncoding.EncodeToString(token)
|
|
||||||
|
|
||||||
return StorablePersonalAccessToken{
|
|
||||||
Token: encodedToken,
|
|
||||||
Name: name,
|
|
||||||
Role: role,
|
|
||||||
UserID: userID,
|
|
||||||
ExpiresAt: expiresAt,
|
|
||||||
LastUsed: 0,
|
|
||||||
Revoked: false,
|
|
||||||
UpdatedByUserID: "",
|
|
||||||
TimeAuditable: types.TimeAuditable{
|
|
||||||
CreatedAt: now,
|
|
||||||
UpdatedAt: now,
|
|
||||||
},
|
|
||||||
Identifiable: types.Identifiable{
|
|
||||||
ID: valuer.GenerateUUID(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
@ -470,3 +470,23 @@ func (h *handler) GetCurrentUserFromJWT(w http.ResponseWriter, r *http.Request)
|
|||||||
render.Success(w, http.StatusOK, user)
|
render.Success(w, http.StatusOK, user)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateAPIKey implements user.Handler.
|
||||||
|
func (h *handler) CreateAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
render.Error(w, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAPIKeys implements user.Handler.
|
||||||
|
func (h *handler) ListAPIKeys(w http.ResponseWriter, r *http.Request) {
|
||||||
|
render.Error(w, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevokeAPIKey implements user.Handler.
|
||||||
|
func (h *handler) RevokeAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
render.Error(w, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "not implemented"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAPIKey implements user.Handler.
|
||||||
|
func (h *handler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
render.Error(w, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "not implemented"))
|
||||||
|
}
|
||||||
|
@ -392,3 +392,23 @@ func (m *Module) CanUsePassword(ctx context.Context, email string) (bool, error)
|
|||||||
func (m *Module) GetAuthDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, error) {
|
func (m *Module) GetAuthDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, error) {
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "SSO is not supported")
|
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "SSO is not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Module) CreateAPIKey(ctx context.Context, apiKey *types.StorableAPIKey) error {
|
||||||
|
return errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "API Keys are not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) UpdateAPIKey(ctx context.Context, id valuer.UUID, apiKey *types.StorableAPIKey, updaterID valuer.UUID) error {
|
||||||
|
return errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "API Keys are not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*types.StorableAPIKeyUser, error) {
|
||||||
|
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "API Keys are not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) GetAPIKey(ctx context.Context, orgID, id valuer.UUID) (*types.StorableAPIKeyUser, error) {
|
||||||
|
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "API Keys are not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Module) RevokeAPIKey(ctx context.Context, id, removedByUserID valuer.UUID) error {
|
||||||
|
return errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "API Keys are not supported")
|
||||||
|
}
|
||||||
|
@ -3,12 +3,14 @@ package impluser
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Store struct {
|
type Store struct {
|
||||||
@ -333,6 +335,15 @@ func (s *Store) DeleteUser(ctx context.Context, orgID string, id string) error {
|
|||||||
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete factor password")
|
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete factor password")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delete api keys
|
||||||
|
_, err = tx.NewDelete().
|
||||||
|
Model(&types.StorableAPIKey{}).
|
||||||
|
Where("user_id = ?", id).
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to delete API keys")
|
||||||
|
}
|
||||||
|
|
||||||
// delete user
|
// delete user
|
||||||
_, err = tx.NewDelete().
|
_, err = tx.NewDelete().
|
||||||
Model(new(types.User)).
|
Model(new(types.User)).
|
||||||
@ -474,3 +485,108 @@ func (s *Store) UpdatePassword(ctx context.Context, userID string, password stri
|
|||||||
func (s *Store) GetDomainByName(ctx context.Context, name string) (*types.StorableOrgDomain, error) {
|
func (s *Store) GetDomainByName(ctx context.Context, name string) (*types.StorableOrgDomain, error) {
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not supported")
|
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- API KEY ---
|
||||||
|
func (s *Store) CreateAPIKey(ctx context.Context, apiKey *types.StorableAPIKey) error {
|
||||||
|
_, err := s.sqlstore.BunDB().NewInsert().
|
||||||
|
Model(apiKey).
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return s.sqlstore.WrapAlreadyExistsErrf(err, types.ErrAPIKeyAlreadyExists, "API key with token: %s already exists", apiKey.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdateAPIKey(ctx context.Context, id valuer.UUID, apiKey *types.StorableAPIKey, updaterID valuer.UUID) error {
|
||||||
|
apiKey.UpdatedBy = updaterID.String()
|
||||||
|
apiKey.UpdatedAt = time.Now()
|
||||||
|
_, err := s.sqlstore.BunDB().NewUpdate().
|
||||||
|
Model(apiKey).
|
||||||
|
Column("role", "name", "updated_at", "updated_by").
|
||||||
|
Where("id = ?", id).
|
||||||
|
Where("revoked = false").
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return s.sqlstore.WrapNotFoundErrf(err, types.ErrAPIKeyNotFound, "API key with id: %s does not exist", id)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*types.StorableAPIKeyUser, error) {
|
||||||
|
orgUserAPIKeys := new(types.OrgUserAPIKey)
|
||||||
|
|
||||||
|
if err := s.sqlstore.BunDB().NewSelect().
|
||||||
|
Model(orgUserAPIKeys).
|
||||||
|
Relation("Users").
|
||||||
|
Relation("Users.APIKeys", func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||||
|
return q.Where("revoked = false")
|
||||||
|
},
|
||||||
|
).
|
||||||
|
Relation("Users.APIKeys.CreatedByUser").
|
||||||
|
Relation("Users.APIKeys.UpdatedByUser").
|
||||||
|
Where("id = ?", orgID).
|
||||||
|
Scan(ctx); err != nil {
|
||||||
|
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to fetch API keys")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flatten the API keys from all users
|
||||||
|
var allAPIKeys []*types.StorableAPIKeyUser
|
||||||
|
for _, user := range orgUserAPIKeys.Users {
|
||||||
|
if user.APIKeys != nil {
|
||||||
|
allAPIKeys = append(allAPIKeys, user.APIKeys...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort the API keys by updated_at
|
||||||
|
sort.Slice(allAPIKeys, func(i, j int) bool {
|
||||||
|
return allAPIKeys[i].UpdatedAt.After(allAPIKeys[j].UpdatedAt)
|
||||||
|
})
|
||||||
|
|
||||||
|
return allAPIKeys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) RevokeAPIKey(ctx context.Context, id, revokedByUserID valuer.UUID) error {
|
||||||
|
updatedAt := time.Now().Unix()
|
||||||
|
_, err := s.sqlstore.BunDB().NewUpdate().
|
||||||
|
Model(&types.StorableAPIKey{}).
|
||||||
|
Set("revoked = ?", true).
|
||||||
|
Set("updated_by = ?", revokedByUserID).
|
||||||
|
Set("updated_at = ?", updatedAt).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to revoke API key")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetAPIKey(ctx context.Context, orgID, id valuer.UUID) (*types.StorableAPIKeyUser, error) {
|
||||||
|
apiKey := new(types.OrgUserAPIKey)
|
||||||
|
if err := s.sqlstore.BunDB().NewSelect().
|
||||||
|
Model(apiKey).
|
||||||
|
Relation("Users").
|
||||||
|
Relation("Users.APIKeys", func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||||
|
return q.Where("revoked = false").Where("storable_api_key.id = ?", id).
|
||||||
|
OrderExpr("storable_api_key.updated_at DESC").Limit(1)
|
||||||
|
},
|
||||||
|
).
|
||||||
|
Relation("Users.APIKeys.CreatedByUser").
|
||||||
|
Relation("Users.APIKeys.UpdatedByUser").
|
||||||
|
Scan(ctx); err != nil {
|
||||||
|
return nil, s.sqlstore.WrapNotFoundErrf(err, types.ErrAPIKeyNotFound, "API key with id: %s does not exist", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// flatten the API keys
|
||||||
|
flattenedAPIKeys := []*types.StorableAPIKeyUser{}
|
||||||
|
for _, user := range apiKey.Users {
|
||||||
|
if user.APIKeys != nil {
|
||||||
|
flattenedAPIKeys = append(flattenedAPIKeys, user.APIKeys...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(flattenedAPIKeys) == 0 {
|
||||||
|
return nil, s.sqlstore.WrapNotFoundErrf(errors.New(errors.TypeNotFound, errors.CodeNotFound, "API key with id: %s does not exist"), types.ErrAPIKeyNotFound, "API key with id: %s does not exist", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return flattenedAPIKeys[0], nil
|
||||||
|
}
|
||||||
|
@ -47,6 +47,13 @@ type Module interface {
|
|||||||
|
|
||||||
// Auth Domain
|
// Auth Domain
|
||||||
GetAuthDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, error)
|
GetAuthDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, error)
|
||||||
|
|
||||||
|
// API KEY
|
||||||
|
CreateAPIKey(ctx context.Context, apiKey *types.StorableAPIKey) error
|
||||||
|
UpdateAPIKey(ctx context.Context, id valuer.UUID, apiKey *types.StorableAPIKey, updaterID valuer.UUID) error
|
||||||
|
ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*types.StorableAPIKeyUser, error)
|
||||||
|
RevokeAPIKey(ctx context.Context, id, removedByUserID valuer.UUID) error
|
||||||
|
GetAPIKey(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*types.StorableAPIKeyUser, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Handler interface {
|
type Handler interface {
|
||||||
@ -72,4 +79,10 @@ type Handler interface {
|
|||||||
GetResetPasswordToken(http.ResponseWriter, *http.Request)
|
GetResetPasswordToken(http.ResponseWriter, *http.Request)
|
||||||
ResetPassword(http.ResponseWriter, *http.Request)
|
ResetPassword(http.ResponseWriter, *http.Request)
|
||||||
ChangePassword(http.ResponseWriter, *http.Request)
|
ChangePassword(http.ResponseWriter, *http.Request)
|
||||||
|
|
||||||
|
// API KEY
|
||||||
|
CreateAPIKey(http.ResponseWriter, *http.Request)
|
||||||
|
ListAPIKeys(http.ResponseWriter, *http.Request)
|
||||||
|
UpdateAPIKey(http.ResponseWriter, *http.Request)
|
||||||
|
RevokeAPIKey(http.ResponseWriter, *http.Request)
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,7 @@ func NewTestSqliteDB(t *testing.T) (sqlStore sqlstore.SQLStore, testDBFilePath s
|
|||||||
sqlmigration.NewCreateQuickFiltersFactory(sqlStore),
|
sqlmigration.NewCreateQuickFiltersFactory(sqlStore),
|
||||||
sqlmigration.NewUpdateQuickFiltersFactory(sqlStore),
|
sqlmigration.NewUpdateQuickFiltersFactory(sqlStore),
|
||||||
sqlmigration.NewAuthRefactorFactory(sqlStore),
|
sqlmigration.NewAuthRefactorFactory(sqlStore),
|
||||||
|
sqlmigration.NewMigratePATToFactorAPIKey(sqlStore),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -77,6 +77,7 @@ func NewSQLMigrationProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedM
|
|||||||
sqlmigration.NewCreateQuickFiltersFactory(sqlstore),
|
sqlmigration.NewCreateQuickFiltersFactory(sqlstore),
|
||||||
sqlmigration.NewUpdateQuickFiltersFactory(sqlstore),
|
sqlmigration.NewUpdateQuickFiltersFactory(sqlstore),
|
||||||
sqlmigration.NewAuthRefactorFactory(sqlstore),
|
sqlmigration.NewAuthRefactorFactory(sqlstore),
|
||||||
|
sqlmigration.NewMigratePATToFactorAPIKey(sqlstore),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
162
pkg/sqlmigration/033_api_keys.go
Normal file
162
pkg/sqlmigration/033_api_keys.go
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
package sqlmigration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
"github.com/uptrace/bun/migrate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type migratePATToFactorAPIKey struct {
|
||||||
|
store sqlstore.SQLStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMigratePATToFactorAPIKey(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
|
||||||
|
return factory.NewProviderFactory(factory.MustNewName("migrate_pat_to_factor_api_key"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||||
|
return newMigratePATToFactorAPIKey(ctx, ps, c, sqlstore)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMigratePATToFactorAPIKey(_ context.Context, _ factory.ProviderSettings, _ Config, store sqlstore.SQLStore) (SQLMigration, error) {
|
||||||
|
return &migratePATToFactorAPIKey{store: store}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (migration *migratePATToFactorAPIKey) Register(migrations *migrate.Migrations) error {
|
||||||
|
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type existingPersonalAccessToken33 struct {
|
||||||
|
bun.BaseModel `bun:"table:personal_access_token"`
|
||||||
|
|
||||||
|
types.Identifiable
|
||||||
|
types.TimeAuditable
|
||||||
|
OrgID string `json:"orgId" bun:"org_id,type:text,notnull"`
|
||||||
|
Role string `json:"role" bun:"role,type:text,notnull,default:'ADMIN'"`
|
||||||
|
UserID string `json:"userId" bun:"user_id,type:text,notnull"`
|
||||||
|
Token string `json:"token" bun:"token,type:text,notnull,unique"`
|
||||||
|
Name string `json:"name" bun:"name,type:text,notnull"`
|
||||||
|
ExpiresAt int64 `json:"expiresAt" bun:"expires_at,notnull,default:0"`
|
||||||
|
LastUsed int64 `json:"lastUsed" bun:"last_used,notnull,default:0"`
|
||||||
|
Revoked bool `json:"revoked" bun:"revoked,notnull,default:false"`
|
||||||
|
UpdatedByUserID string `json:"updatedByUserId" bun:"updated_by_user_id,type:text,notnull,default:''"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// we are removing the connection with org,
|
||||||
|
// the reason we are doing this is the api keys should just have
|
||||||
|
// one foreign key, we don't want a dangling state where, an API key
|
||||||
|
// belongs to one org and some user which doesn't belong to that org.
|
||||||
|
// so going ahead with directly attaching it to user will help dangling states.
|
||||||
|
type newFactorAPIKey33 struct {
|
||||||
|
bun.BaseModel `bun:"table:factor_api_key"`
|
||||||
|
|
||||||
|
types.Identifiable
|
||||||
|
CreatedAt time.Time `bun:"created_at,notnull,nullzero,type:timestamptz" json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `bun:"updated_at,notnull,nullzero,type:timestamptz" json:"updatedAt"`
|
||||||
|
CreatedBy string `bun:"created_by,notnull" json:"createdBy"`
|
||||||
|
UpdatedBy string `bun:"updated_by,notnull" json:"updatedBy"`
|
||||||
|
Token string `json:"token" bun:"token,type:text,notnull,unique"`
|
||||||
|
Role string `json:"role" bun:"role,type:text,notnull"`
|
||||||
|
Name string `json:"name" bun:"name,type:text,notnull"`
|
||||||
|
ExpiresAt time.Time `json:"expiresAt" bun:"expires_at,notnull,nullzero,type:timestamptz"`
|
||||||
|
LastUsed time.Time `json:"lastUsed" bun:"last_used,notnull,nullzero,type:timestamptz"`
|
||||||
|
Revoked bool `json:"revoked" bun:"revoked,notnull,default:false"`
|
||||||
|
UserID string `json:"userId" bun:"user_id,type:text,notnull"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (migration *migratePATToFactorAPIKey) Up(ctx context.Context, db *bun.DB) error {
|
||||||
|
tx, err := db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
err = migration.
|
||||||
|
store.
|
||||||
|
Dialect().
|
||||||
|
RenameTableAndModifyModel(ctx, tx, new(existingPersonalAccessToken33), new(newFactorAPIKey33), []string{UserReferenceNoCascade}, func(ctx context.Context) error {
|
||||||
|
existingAPIKeys := make([]*existingPersonalAccessToken33, 0)
|
||||||
|
err = tx.
|
||||||
|
NewSelect().
|
||||||
|
Model(&existingAPIKeys).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil && len(existingAPIKeys) > 0 {
|
||||||
|
newAPIKeys, err := migration.
|
||||||
|
CopyOldPatToFactorAPIKey(ctx, tx, existingAPIKeys)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.
|
||||||
|
NewInsert().
|
||||||
|
Model(&newAPIKeys).
|
||||||
|
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 *migratePATToFactorAPIKey) Down(ctx context.Context, db *bun.DB) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (migration *migratePATToFactorAPIKey) CopyOldPatToFactorAPIKey(ctx context.Context, tx bun.IDB, existingAPIKeys []*existingPersonalAccessToken33) ([]*newFactorAPIKey33, error) {
|
||||||
|
newAPIKeys := make([]*newFactorAPIKey33, 0)
|
||||||
|
for _, apiKey := range existingAPIKeys {
|
||||||
|
|
||||||
|
if apiKey.CreatedAt.IsZero() {
|
||||||
|
apiKey.CreatedAt = time.Now()
|
||||||
|
}
|
||||||
|
if apiKey.UpdatedAt.IsZero() {
|
||||||
|
apiKey.UpdatedAt = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert expiresAt and lastUsed to time.Time
|
||||||
|
expiresAt := time.Unix(apiKey.ExpiresAt, 0)
|
||||||
|
lastUsed := time.Unix(apiKey.LastUsed, 0)
|
||||||
|
if apiKey.LastUsed == 0 {
|
||||||
|
lastUsed = apiKey.CreatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
newAPIKeys = append(newAPIKeys, &newFactorAPIKey33{
|
||||||
|
Identifiable: apiKey.Identifiable,
|
||||||
|
CreatedAt: apiKey.CreatedAt,
|
||||||
|
UpdatedAt: apiKey.UpdatedAt,
|
||||||
|
CreatedBy: apiKey.UserID,
|
||||||
|
UpdatedBy: apiKey.UpdatedByUserID,
|
||||||
|
Token: apiKey.Token,
|
||||||
|
Role: apiKey.Role,
|
||||||
|
Name: apiKey.Name,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
LastUsed: lastUsed,
|
||||||
|
Revoked: apiKey.Revoked,
|
||||||
|
UserID: apiKey.UserID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return newAPIKeys, nil
|
||||||
|
}
|
@ -27,6 +27,7 @@ var (
|
|||||||
var (
|
var (
|
||||||
OrgReference = "org"
|
OrgReference = "org"
|
||||||
UserReference = "user"
|
UserReference = "user"
|
||||||
|
UserReferenceNoCascade = "user_no_cascade"
|
||||||
FactorPasswordReference = "factor_password"
|
FactorPasswordReference = "factor_password"
|
||||||
CloudIntegrationReference = "cloud_integration"
|
CloudIntegrationReference = "cloud_integration"
|
||||||
)
|
)
|
||||||
|
@ -20,6 +20,7 @@ const (
|
|||||||
const (
|
const (
|
||||||
Org string = "org"
|
Org string = "org"
|
||||||
User string = "user"
|
User string = "user"
|
||||||
|
UserNoCascade string = "user_no_cascade"
|
||||||
FactorPassword string = "factor_password"
|
FactorPassword string = "factor_password"
|
||||||
CloudIntegration string = "cloud_integration"
|
CloudIntegration string = "cloud_integration"
|
||||||
)
|
)
|
||||||
@ -27,6 +28,7 @@ const (
|
|||||||
const (
|
const (
|
||||||
OrgReference string = `("org_id") REFERENCES "organizations" ("id")`
|
OrgReference string = `("org_id") REFERENCES "organizations" ("id")`
|
||||||
UserReference string = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE`
|
UserReference string = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE`
|
||||||
|
UserNoCascadeReference string = `("user_id") REFERENCES "users" ("id")`
|
||||||
FactorPasswordReference string = `("password_id") REFERENCES "factor_password" ("id")`
|
FactorPasswordReference string = `("password_id") REFERENCES "factor_password" ("id")`
|
||||||
CloudIntegrationReference string = `("cloud_integration_id") REFERENCES "cloud_integration" ("id") ON DELETE CASCADE`
|
CloudIntegrationReference string = `("cloud_integration_id") REFERENCES "cloud_integration" ("id") ON DELETE CASCADE`
|
||||||
)
|
)
|
||||||
@ -261,6 +263,8 @@ func (dialect *dialect) RenameTableAndModifyModel(ctx context.Context, bun bun.I
|
|||||||
fkReferences = append(fkReferences, OrgReference)
|
fkReferences = append(fkReferences, OrgReference)
|
||||||
} else if reference == User && !slices.Contains(fkReferences, UserReference) {
|
} else if reference == User && !slices.Contains(fkReferences, UserReference) {
|
||||||
fkReferences = append(fkReferences, UserReference)
|
fkReferences = append(fkReferences, UserReference)
|
||||||
|
} else if reference == UserNoCascade && !slices.Contains(fkReferences, UserNoCascadeReference) {
|
||||||
|
fkReferences = append(fkReferences, UserNoCascadeReference)
|
||||||
} else if reference == FactorPassword && !slices.Contains(fkReferences, FactorPasswordReference) {
|
} else if reference == FactorPassword && !slices.Contains(fkReferences, FactorPasswordReference) {
|
||||||
fkReferences = append(fkReferences, FactorPasswordReference)
|
fkReferences = append(fkReferences, FactorPasswordReference)
|
||||||
} else if reference == CloudIntegration && !slices.Contains(fkReferences, CloudIntegrationReference) {
|
} else if reference == CloudIntegration && !slices.Contains(fkReferences, CloudIntegrationReference) {
|
||||||
|
135
pkg/types/factor_api_key.go
Normal file
135
pkg/types/factor_api_key.go
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PostableAPIKey struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Role Role `json:"role"`
|
||||||
|
ExpiresInDays int64 `json:"expiresInDays"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GettableAPIKey struct {
|
||||||
|
Identifiable
|
||||||
|
TimeAuditable
|
||||||
|
UserAuditable
|
||||||
|
Token string `json:"token"`
|
||||||
|
Role Role `json:"role"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ExpiresAt int64 `json:"expiresAt"`
|
||||||
|
LastUsed int64 `json:"lastUsed"`
|
||||||
|
Revoked bool `json:"revoked"`
|
||||||
|
UserID string `json:"userId"`
|
||||||
|
CreatedBy *User `json:"createdBy"`
|
||||||
|
UpdatedBy *User `json:"updatedBy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrgUserAPIKey struct {
|
||||||
|
*Organization `bun:",extend"`
|
||||||
|
Users []*UserWithAPIKey `bun:"rel:has-many,join:id=org_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserWithAPIKey struct {
|
||||||
|
*User `bun:",extend"`
|
||||||
|
APIKeys []*StorableAPIKeyUser `bun:"rel:has-many,join:id=user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StorableAPIKeyUser struct {
|
||||||
|
StorableAPIKey `bun:",extend"`
|
||||||
|
|
||||||
|
CreatedByUser *User `json:"createdByUser" bun:"created_by_user,rel:belongs-to,join:created_by=id"`
|
||||||
|
UpdatedByUser *User `json:"updatedByUser" bun:"updated_by_user,rel:belongs-to,join:updated_by=id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StorableAPIKey struct {
|
||||||
|
bun.BaseModel `bun:"table:factor_api_key"`
|
||||||
|
|
||||||
|
Identifiable
|
||||||
|
TimeAuditable
|
||||||
|
UserAuditable
|
||||||
|
Token string `json:"token" bun:"token,type:text,notnull,unique"`
|
||||||
|
Role Role `json:"role" bun:"role,type:text,notnull,default:'ADMIN'"`
|
||||||
|
Name string `json:"name" bun:"name,type:text,notnull"`
|
||||||
|
ExpiresAt time.Time `json:"-" bun:"expires_at,notnull,nullzero,type:timestamptz"`
|
||||||
|
LastUsed time.Time `json:"-" bun:"last_used,notnull,nullzero,type:timestamptz"`
|
||||||
|
Revoked bool `json:"revoked" bun:"revoked,notnull,default:false"`
|
||||||
|
UserID valuer.UUID `json:"userId" bun:"user_id,type:text,notnull"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStorableAPIKey(name string, userID valuer.UUID, role Role, expiresAt int64) (*StorableAPIKey, error) {
|
||||||
|
// validate
|
||||||
|
if expiresAt <= 0 {
|
||||||
|
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "expiresAt must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "name cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if role == "" {
|
||||||
|
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "role cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
// convert expiresAt to unix timestamp from days
|
||||||
|
// expiresAt = now.Unix() + (expiresAt * 24 * 60 * 60)
|
||||||
|
expiresAtTime := now.AddDate(0, 0, int(expiresAt))
|
||||||
|
|
||||||
|
// Generate a 32-byte random token.
|
||||||
|
token := make([]byte, 32)
|
||||||
|
_, err := rand.Read(token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(errors.TypeInternal, errors.CodeInternal, "failed to generate token")
|
||||||
|
}
|
||||||
|
// Encode the token in base64.
|
||||||
|
encodedToken := base64.StdEncoding.EncodeToString(token)
|
||||||
|
|
||||||
|
return &StorableAPIKey{
|
||||||
|
Identifiable: Identifiable{
|
||||||
|
ID: valuer.GenerateUUID(),
|
||||||
|
},
|
||||||
|
TimeAuditable: TimeAuditable{
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
},
|
||||||
|
UserAuditable: UserAuditable{
|
||||||
|
CreatedBy: userID.String(),
|
||||||
|
UpdatedBy: userID.String(),
|
||||||
|
},
|
||||||
|
Token: encodedToken,
|
||||||
|
Name: name,
|
||||||
|
Role: role,
|
||||||
|
UserID: userID,
|
||||||
|
ExpiresAt: expiresAtTime,
|
||||||
|
LastUsed: now,
|
||||||
|
Revoked: false,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGettableAPIKeyFromStorableAPIKey(storableAPIKey *StorableAPIKeyUser) *GettableAPIKey {
|
||||||
|
lastUsed := storableAPIKey.LastUsed.Unix()
|
||||||
|
if storableAPIKey.LastUsed == storableAPIKey.CreatedAt {
|
||||||
|
lastUsed = 0
|
||||||
|
}
|
||||||
|
return &GettableAPIKey{
|
||||||
|
Identifiable: storableAPIKey.Identifiable,
|
||||||
|
TimeAuditable: storableAPIKey.TimeAuditable,
|
||||||
|
UserAuditable: storableAPIKey.UserAuditable,
|
||||||
|
Token: storableAPIKey.Token,
|
||||||
|
Role: storableAPIKey.Role,
|
||||||
|
Name: storableAPIKey.Name,
|
||||||
|
ExpiresAt: storableAPIKey.ExpiresAt.Unix(),
|
||||||
|
LastUsed: lastUsed,
|
||||||
|
Revoked: storableAPIKey.Revoked,
|
||||||
|
UserID: storableAPIKey.UserID.String(),
|
||||||
|
CreatedBy: storableAPIKey.CreatedByUser,
|
||||||
|
UpdatedBy: storableAPIKey.UpdatedByUser,
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,8 @@ var (
|
|||||||
ErrResetPasswordTokenAlreadyExists = errors.MustNewCode("reset_password_token_already_exists")
|
ErrResetPasswordTokenAlreadyExists = errors.MustNewCode("reset_password_token_already_exists")
|
||||||
ErrPasswordNotFound = errors.MustNewCode("password_not_found")
|
ErrPasswordNotFound = errors.MustNewCode("password_not_found")
|
||||||
ErrResetPasswordTokenNotFound = errors.MustNewCode("reset_password_token_not_found")
|
ErrResetPasswordTokenNotFound = errors.MustNewCode("reset_password_token_not_found")
|
||||||
|
ErrAPIKeyAlreadyExists = errors.MustNewCode("api_key_already_exists")
|
||||||
|
ErrAPIKeyNotFound = errors.MustNewCode("api_key_not_found")
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserStore interface {
|
type UserStore interface {
|
||||||
@ -58,6 +60,13 @@ type UserStore interface {
|
|||||||
|
|
||||||
// Temporary func for SSO
|
// Temporary func for SSO
|
||||||
GetDefaultOrgID(ctx context.Context) (string, error)
|
GetDefaultOrgID(ctx context.Context) (string, error)
|
||||||
|
|
||||||
|
// API KEY
|
||||||
|
CreateAPIKey(ctx context.Context, apiKey *StorableAPIKey) error
|
||||||
|
UpdateAPIKey(ctx context.Context, id valuer.UUID, apiKey *StorableAPIKey, updaterID valuer.UUID) error
|
||||||
|
ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*StorableAPIKeyUser, error)
|
||||||
|
RevokeAPIKey(ctx context.Context, id valuer.UUID, revokedByUserID valuer.UUID) error
|
||||||
|
GetAPIKey(ctx context.Context, orgID, id valuer.UUID) (*StorableAPIKeyUser, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type GettableUser struct {
|
type GettableUser struct {
|
||||||
@ -73,7 +82,7 @@ type User struct {
|
|||||||
DisplayName string `bun:"display_name,type:text,notnull" json:"displayName"`
|
DisplayName string `bun:"display_name,type:text,notnull" json:"displayName"`
|
||||||
Email string `bun:"email,type:text,notnull,unique:org_email" json:"email"`
|
Email string `bun:"email,type:text,notnull,unique:org_email" json:"email"`
|
||||||
Role string `bun:"role,type:text,notnull" json:"role"`
|
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"`
|
OrgID string `bun:"org_id,type:text,notnull,unique:org_email" json:"orgId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUser(displayName string, email string, role string, orgID string) (*User, error) {
|
func NewUser(displayName string, email string, role string, orgID string) (*User, error) {
|
||||||
@ -186,7 +195,7 @@ type ResetPasswordRequest struct {
|
|||||||
|
|
||||||
Identifiable
|
Identifiable
|
||||||
Token string `bun:"token,type:text,notnull" json:"token"`
|
Token string `bun:"token,type:text,notnull" json:"token"`
|
||||||
PasswordID string `bun:"password_id,type:text,notnull,unique,references:factor_password(id)" json:"passwordId"`
|
PasswordID string `bun:"password_id,type:text,notnull,unique" json:"passwordId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewResetPasswordRequest(passwordID string) (*ResetPasswordRequest, error) {
|
func NewResetPasswordRequest(passwordID string) (*ResetPasswordRequest, error) {
|
||||||
|
@ -18,7 +18,7 @@ def test_api_key(signoz: types.SigNoz, get_jwt_token) -> None:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == HTTPStatus.OK
|
assert response.status_code == HTTPStatus.CREATED
|
||||||
pat_response = response.json()
|
pat_response = response.json()
|
||||||
assert "data" in pat_response
|
assert "data" in pat_response
|
||||||
assert "token" in pat_response["data"]
|
assert "token" in pat_response["data"]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user