From 388ef9453ca1ddb1c6bed03b88b4acd42cab0a95 Mon Sep 17 00:00:00 2001 From: Ahsan Barkati Date: Wed, 15 Feb 2023 01:34:22 +0530 Subject: [PATCH] Add APIs for PAT --- ee/query-service/app/api/api.go | 5 ++ ee/query-service/app/api/pat.go | 78 +++++++++++++++++++++++++ ee/query-service/dao/interface.go | 7 ++- ee/query-service/dao/sqlite/modelDao.go | 12 +++- ee/query-service/dao/sqlite/pat.go | 44 ++++++++++++++ ee/query-service/model/pat.go | 10 ++++ 6 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 ee/query-service/app/api/pat.go create mode 100644 ee/query-service/dao/sqlite/pat.go create mode 100644 ee/query-service/model/pat.go diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go index 601bed7714..93330c0572 100644 --- a/ee/query-service/app/api/api.go +++ b/ee/query-service/app/api/api.go @@ -122,6 +122,11 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router) { router.HandleFunc("/api/v1/traces/{traceId}", baseapp.ViewAccess(ah.searchTraces)).Methods(http.MethodGet) router.HandleFunc("/api/v2/metrics/query_range", baseapp.ViewAccess(ah.queryRangeMetricsV2)).Methods(http.MethodPost) + // PAT APIs + router.HandleFunc("/api/v1/pat", baseapp.SelfAccess(ah.createPAT)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/pat", baseapp.SelfAccess(ah.getPATs)).Methods(http.MethodGet) + router.HandleFunc("/api/v1/pat/{id}", baseapp.SelfAccess(ah.deletePAT)).Methods(http.MethodDelete) + ah.APIHandler.RegisterRoutes(router) } diff --git a/ee/query-service/app/api/pat.go b/ee/query-service/app/api/pat.go new file mode 100644 index 0000000000..1b32ddfde5 --- /dev/null +++ b/ee/query-service/app/api/pat.go @@ -0,0 +1,78 @@ +package api + +import ( + "context" + "crypto/rand" + "encoding/base64" + "encoding/json" + "net/http" + "time" + + "github.com/gorilla/mux" + "go.signoz.io/signoz/ee/query-service/model" + "go.signoz.io/signoz/pkg/query-service/auth" + "go.uber.org/zap" +) + +func generatePATToken() string { + // Generate a 32-byte random token. + token := make([]byte, 32) + rand.Read(token) + // Encode the token in base64. + encodedToken := base64.StdEncoding.EncodeToString(token) + return encodedToken +} + +func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) { + ctx := context.Background() + + req := model.PAT{} + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + RespondError(w, model.BadRequest(err), nil) + return + } + user, err := auth.GetUserFromRequest(r) + if err != nil { + RespondError(w, &model.ApiError{ + Typ: model.ErrorUnauthorized, + Err: err, + }, nil) + return + } + + req.UserID = user.Id + req.CreatedAt = time.Now().Unix() + req.Token = generatePATToken() + + zap.S().Infof("Got PAT request: %+v", req) + + if apierr := ah.AppDao().CreatePAT(ctx, &req); apierr != nil { + RespondError(w, apierr, nil) + return + } + + ah.Respond(w, &req) +} + +func (ah *APIHandler) getPATs(w http.ResponseWriter, r *http.Request) { + ctx := context.Background() + user, _ := auth.GetUserFromRequest(r) + zap.S().Infof("Get PATs for user: %+v", user.Id) + pats, apierr := ah.AppDao().ListPATs(ctx, user.Id) + if apierr != nil { + RespondError(w, apierr, nil) + return + } + ah.WriteJSON(w, r, pats) +} + +func (ah *APIHandler) deletePAT(w http.ResponseWriter, r *http.Request) { + ctx := context.Background() + id := mux.Vars(r)["id"] + zap.S().Infof("Delete PAT with id: %+v", id) + if apierr := ah.AppDao().DeletePAT(ctx, id); apierr != nil { + RespondError(w, apierr, nil) + return + } + ah.WriteJSON(w, r, map[string]string{"data": "pat deleted successfully"}) +} diff --git a/ee/query-service/dao/interface.go b/ee/query-service/dao/interface.go index a2c9d9d68d..a74fa5c6f2 100644 --- a/ee/query-service/dao/interface.go +++ b/ee/query-service/dao/interface.go @@ -3,6 +3,7 @@ package dao import ( "context" "net/url" + "github.com/google/uuid" "github.com/jmoiron/sqlx" "go.signoz.io/signoz/ee/query-service/model" @@ -24,7 +25,7 @@ type ModelDao interface { CanUsePassword(ctx context.Context, email string) (bool, basemodel.BaseApiError) PrepareSsoRedirect(ctx context.Context, redirectUri, email string) (redirectURL string, apierr basemodel.BaseApiError) GetDomainFromSsoResponse(ctx context.Context, relayState *url.URL) (*model.OrgDomain, error) - + // org domain (auth domains) CRUD ops ListDomains(ctx context.Context, orgId string) ([]model.OrgDomain, basemodel.BaseApiError) GetDomain(ctx context.Context, id uuid.UUID) (*model.OrgDomain, basemodel.BaseApiError) @@ -32,4 +33,8 @@ type ModelDao interface { UpdateDomain(ctx context.Context, domain *model.OrgDomain) basemodel.BaseApiError DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.BaseApiError GetDomainByEmail(ctx context.Context, email string) (*model.OrgDomain, basemodel.BaseApiError) + + CreatePAT(ctx context.Context, p *model.PAT) basemodel.BaseApiError + ListPATs(ctx context.Context, userID string) ([]model.PAT, basemodel.BaseApiError) + DeletePAT(ctx context.Context, id string) basemodel.BaseApiError } diff --git a/ee/query-service/dao/sqlite/modelDao.go b/ee/query-service/dao/sqlite/modelDao.go index 156f6b30e7..9b1d74c034 100644 --- a/ee/query-service/dao/sqlite/modelDao.go +++ b/ee/query-service/dao/sqlite/modelDao.go @@ -48,7 +48,17 @@ func InitDB(dataSourceName string) (*modelDao, error) { updated_at INTEGER, data TEXT NOT NULL, FOREIGN KEY(org_id) REFERENCES organizations(id) - );` + ); + CREATE TABLE IF NOT EXISTS personal_access_tokens ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL, + token TEXT NOT NULL, + name TEXT NOT NULL, + created_at INTEGER NOT NULL, + expires_at INTEGER NOT NULL, + FOREIGN KEY(user_id) REFERENCES users(id) + ); + ` _, err = m.DB().Exec(table_schema) if err != nil { diff --git a/ee/query-service/dao/sqlite/pat.go b/ee/query-service/dao/sqlite/pat.go new file mode 100644 index 0000000000..340af1f854 --- /dev/null +++ b/ee/query-service/dao/sqlite/pat.go @@ -0,0 +1,44 @@ +package sqlite + +import ( + "context" + "fmt" + + "go.signoz.io/signoz/ee/query-service/model" + basemodel "go.signoz.io/signoz/pkg/query-service/model" + "go.uber.org/zap" +) + +func (m *modelDao) CreatePAT(ctx context.Context, p *model.PAT) basemodel.BaseApiError { + _, err := m.DB().ExecContext(ctx, + "INSERT INTO personal_access_tokens (user_id, token, name, created_at, expires_at) VALUES ($1, $2, $3, $4, $5)", + p.UserID, + p.Token, + p.Name, + p.CreatedAt, + p.ExpiresAt) + if err != nil { + zap.S().Errorf("Failed to insert PAT in db, err: %v", zap.Error(err)) + return model.InternalError(fmt.Errorf("PAT insertion failed")) + } + return nil +} + +func (m *modelDao) ListPATs(ctx context.Context, userID string) ([]model.PAT, basemodel.BaseApiError) { + pats := []model.PAT{} + + if err := m.DB().Select(&pats, `SELECT * FROM personal_access_tokens WHERE user_id=?;`, userID); err != nil { + zap.S().Errorf("Failed to fetch PATs for user: %s, err: %v", userID, zap.Error(err)) + return nil, model.InternalError(fmt.Errorf("Failed to fetch PATs")) + } + return pats, nil +} + +func (m *modelDao) DeletePAT(ctx context.Context, id string) basemodel.BaseApiError { + _, err := m.DB().ExecContext(ctx, `DELETE from personal_access_tokens where id=?;`, id) + if err != nil { + zap.S().Errorf("Failed to delete PAT, err: %v", zap.Error(err)) + return model.InternalError(fmt.Errorf("Failed to delete PAT")) + } + return nil +} diff --git a/ee/query-service/model/pat.go b/ee/query-service/model/pat.go new file mode 100644 index 0000000000..f320d0be7c --- /dev/null +++ b/ee/query-service/model/pat.go @@ -0,0 +1,10 @@ +package model + +type PAT struct { + Id string `json:"id" db:"id"` + UserID string `json:"userId" db:"user_id"` + Token string `json:"token" db:"token"` + Name string `json:"name" db:"name"` + CreatedAt int64 `json:"createdAt" db:"created_at"` + ExpiresAt int64 `json:"expiresAt" db:"expires_at"` +}