From e33a0fdd477045cb04a7f79f69576eca14f3be67 Mon Sep 17 00:00:00 2001 From: Raj Kamal Singh <1133322+raj-k-singh@users.noreply.github.com> Date: Sat, 1 Feb 2025 20:12:56 +0530 Subject: [PATCH] Feat/cloud integrations connection params api key (#6997) * feat: get started on PAT provisioning for AWS integration * chore: include cloud integration PAT in connection params * chore: some cleanup --------- Co-authored-by: Srikanth Chekuri --- ee/query-service/app/api/cloudIntegrations.go | 164 +++++++++++++++--- pkg/query-service/app/http_handler.go | 2 +- 2 files changed, 143 insertions(+), 23 deletions(-) diff --git a/ee/query-service/app/api/cloudIntegrations.go b/ee/query-service/app/api/cloudIntegrations.go index 187ef48925..5cfc5f06ba 100644 --- a/ee/query-service/app/api/cloudIntegrations.go +++ b/ee/query-service/app/api/cloudIntegrations.go @@ -10,8 +10,13 @@ import ( "strings" "time" + "github.com/google/uuid" "github.com/gorilla/mux" "go.signoz.io/signoz/ee/query-service/constants" + "go.signoz.io/signoz/ee/query-service/model" + "go.signoz.io/signoz/pkg/query-service/auth" + baseconstants "go.signoz.io/signoz/pkg/query-service/constants" + "go.signoz.io/signoz/pkg/query-service/dao" basemodel "go.signoz.io/signoz/pkg/query-service/model" "go.uber.org/zap" ) @@ -20,6 +25,7 @@ type CloudIntegrationConnectionParamsResponse struct { IngestionUrl string `json:"ingestion_url,omitempty"` IngestionKey string `json:"ingestion_key,omitempty"` SigNozAPIUrl string `json:"signoz_api_url,omitempty"` + SigNozAPIKey string `json:"signoz_api_key,omitempty"` } func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseWriter, r *http.Request) { @@ -31,44 +37,64 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW return } - license, err := ah.LM().GetRepo().GetActiveLicense(r.Context()) + currentUser, err := auth.GetUserFromRequest(r) if err != nil { - RespondError(w, basemodel.InternalError(fmt.Errorf( - "couldn't look for active license: %w", err, + RespondError(w, basemodel.UnauthorizedError(fmt.Errorf( + "couldn't deduce current user: %w", err, )), nil) return } - if license == nil { - RespondError(w, basemodel.ForbiddenError(fmt.Errorf( - "no active license found", - )), nil) - return - } - - ingestionUrl, signozApiUrl, err := getIngestionUrlAndSigNozAPIUrl(r.Context(), license.Key) - if err != nil { - RespondError(w, basemodel.InternalError(fmt.Errorf( - "couldn't deduce ingestion url and signoz api url: %w", err, - )), nil) + apiKey, apiErr := ah.getOrCreateCloudIntegrationPAT(r.Context(), currentUser.OrgId, cloudProvider) + if apiErr != nil { + RespondError(w, basemodel.WrapApiError( + apiErr, "couldn't provision PAT for cloud integration:", + ), nil) return } result := CloudIntegrationConnectionParamsResponse{ - IngestionUrl: ingestionUrl, - SigNozAPIUrl: signozApiUrl, + SigNozAPIKey: apiKey, } + license, apiErr := ah.LM().GetRepo().GetActiveLicense(r.Context()) + if apiErr != nil { + RespondError(w, basemodel.WrapApiError( + apiErr, "couldn't look for active license", + ), nil) + return + } + + if license == nil { + // Return the API Key (PAT) even if the rest of the params can not be deduced. + // Params not returned from here will be requested from the user via form inputs. + // This enables gracefully degraded but working experience even for non-cloud deployments. + zap.L().Info("ingestion params and signoz api url can not be deduced since no license was found") + ah.Respond(w, result) + return + } + + ingestionUrl, signozApiUrl, apiErr := getIngestionUrlAndSigNozAPIUrl(r.Context(), license.Key) + if apiErr != nil { + RespondError(w, basemodel.WrapApiError( + apiErr, "couldn't deduce ingestion url and signoz api url", + ), nil) + return + } + + result.IngestionUrl = ingestionUrl + result.SigNozAPIUrl = signozApiUrl + gatewayUrl := ah.opts.GatewayUrl if len(gatewayUrl) > 0 { - ingestionKey, err := getOrCreateCloudProviderIngestionKey( + ingestionKey, apiErr := getOrCreateCloudProviderIngestionKey( r.Context(), gatewayUrl, license.Key, cloudProvider, ) - if err != nil { - RespondError(w, basemodel.InternalError(fmt.Errorf( - "couldn't get or create ingestion key: %w", err, - )), nil) + if apiErr != nil { + RespondError(w, basemodel.WrapApiError( + apiErr, "couldn't get or create ingestion key", + ), nil) return } @@ -81,6 +107,100 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW ah.Respond(w, result) } +func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId string, cloudProvider string) ( + string, *basemodel.ApiError, +) { + integrationPATName := fmt.Sprintf("%s integration", cloudProvider) + + integrationUser, apiErr := ah.getOrCreateCloudIntegrationUser(ctx, orgId, cloudProvider) + if apiErr != nil { + return "", apiErr + } + + allPats, err := ah.AppDao().ListPATs(ctx) + if err != nil { + return "", basemodel.InternalError(fmt.Errorf( + "couldn't list PATs: %w", err.Error(), + )) + } + for _, p := range allPats { + if p.UserID == integrationUser.Id && p.Name == integrationPATName { + return p.Token, nil + } + } + + zap.L().Info( + "no PAT found for cloud integration, creating a new one", + zap.String("cloudProvider", cloudProvider), + ) + + newPAT := model.PAT{ + Token: generatePATToken(), + UserID: integrationUser.Id, + Name: integrationPATName, + Role: baseconstants.ViewerGroup, + ExpiresAt: 0, + CreatedAt: time.Now().Unix(), + UpdatedAt: time.Now().Unix(), + } + integrationPAT, err := ah.AppDao().CreatePAT(ctx, newPAT) + if err != nil { + return "", basemodel.InternalError(fmt.Errorf( + "couldn't create cloud integration PAT: %w", err.Error(), + )) + } + return integrationPAT.Token, nil +} + +func (ah *APIHandler) getOrCreateCloudIntegrationUser( + ctx context.Context, orgId string, cloudProvider string, +) (*basemodel.User, *basemodel.ApiError) { + cloudIntegrationUserId := fmt.Sprintf("%s-integration", cloudProvider) + + integrationUserResult, apiErr := ah.AppDao().GetUser(ctx, cloudIntegrationUserId) + if apiErr != nil { + return nil, basemodel.WrapApiError(apiErr, "couldn't look for integration user") + } + + if integrationUserResult != nil { + return &integrationUserResult.User, nil + } + + zap.L().Info( + "cloud integration user not found. Attempting to create the user", + zap.String("cloudProvider", cloudProvider), + ) + + newUser := &basemodel.User{ + Id: cloudIntegrationUserId, + Name: fmt.Sprintf("%s integration", cloudProvider), + Email: fmt.Sprintf("%s@signoz.io", cloudIntegrationUserId), + CreatedAt: time.Now().Unix(), + OrgId: orgId, + } + + viewerGroup, apiErr := dao.DB().GetGroupByName(ctx, baseconstants.ViewerGroup) + if apiErr != nil { + return nil, basemodel.WrapApiError(apiErr, "couldn't get viewer group for creating integration user") + } + newUser.GroupId = viewerGroup.Id + + passwordHash, err := auth.PasswordHash(uuid.NewString()) + if err != nil { + return nil, basemodel.InternalError(fmt.Errorf( + "couldn't hash random password for 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") + } + + return integrationUser, nil +} + func getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) ( string, string, *basemodel.ApiError, ) { diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 4b62ec65d2..185dfd749b 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -3939,7 +3939,7 @@ func (aH *APIHandler) RegisterCloudIntegrationsRoutes(router *mux.Router, am *Au ).Methods(http.MethodPost) subRouter.HandleFunc( - "/{cloudProvider}/agent-check-in", am.EditAccess(aH.CloudIntegrationsAgentCheckIn), + "/{cloudProvider}/agent-check-in", am.ViewAccess(aH.CloudIntegrationsAgentCheckIn), ).Methods(http.MethodPost) subRouter.HandleFunc(