diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b78c5fd9303..2ed38aabee7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -664,6 +664,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /pkg/services/caching/ @grafana/grafana-operator-experience-squad /pkg/services/cloudmigration/ @grafana/grafana-operator-experience-squad /pkg/services/gcom/ @grafana/grafana-operator-experience-squad +/pkg/services/authapi/ @grafana/grafana-operator-experience-squad # Feature toggles /pkg/services/featuremgmt/ @grafana/grafana-backend-services-squad diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 789beb9a939..6b907ebd1fa 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -188,6 +188,7 @@ Experimental features might be changed or removed without prior notice. | `kubernetesFeatureToggles` | Use the kubernetes API for feature toggle management in the frontend | | `newFolderPicker` | Enables the nested folder picker without having nested folders enabled | | `onPremToCloudMigrationsAlerts` | Enables the migration of alerts and its child resources to your Grafana Cloud stack. Requires `onPremToCloudMigrations` to be enabled in conjunction. | +| `onPremToCloudMigrationsAuthApiMig` | Enables the use of auth api instead of gcom for internal token services. Requires `onPremToCloudMigrations` to be enabled in conjunction. | | `sqlExpressions` | Enables using SQL and DuckDB functions as Expressions. | | `nodeGraphDotLayout` | Changed the layout algorithm for the node graph | | `kubernetesAggregator` | Enable grafana's embedded kube-aggregator | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index bc13567d445..cffab8c459a 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -158,6 +158,7 @@ export interface FeatureToggles { jitterAlertRulesWithinGroups?: boolean; onPremToCloudMigrations?: boolean; onPremToCloudMigrationsAlerts?: boolean; + onPremToCloudMigrationsAuthApiMig?: boolean; alertingSaveStatePeriodic?: boolean; promQLScope?: boolean; logQLScope?: boolean; diff --git a/pkg/services/authapi/authapi.go b/pkg/services/authapi/authapi.go new file mode 100644 index 00000000000..dd746b08fdd --- /dev/null +++ b/pkg/services/authapi/authapi.go @@ -0,0 +1,398 @@ +// Package authapi contains the connector for Grafana internal auth service. This can be used instead of the GCOM service +// to create access policies and access tokens +package authapi + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/grafana/grafana/pkg/infra/log" +) + +const LogPrefix = "auth-api.service" + +var ErrTokenNotFound = errors.New("auth-api: token not found") + +type Service interface { + CreateAccessPolicy(ctx context.Context, params CreateAccessPolicyParams, payload CreateAccessPolicyPayload) (AccessPolicy, error) + ListAccessPolicies(ctx context.Context, params ListAccessPoliciesParams) ([]AccessPolicy, error) + DeleteAccessPolicy(ctx context.Context, params DeleteAccessPolicyParams) (bool, error) + ListTokens(ctx context.Context, params ListTokenParams) ([]TokenView, error) + CreateToken(ctx context.Context, params CreateTokenParams, payload CreateTokenPayload) (Token, error) + DeleteToken(ctx context.Context, params DeleteTokenParams) error +} + +type CreateAccessPolicyParams struct { + RequestID string + // this is needed until we fully migrate from gcom to authapi + Region string +} + +type CreateAccessPolicyPayload struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + Realms []Realm `json:"realms"` + Scopes []string `json:"scopes"` +} + +type Realm struct { + Identifier string `json:"identifier"` + LabelPolicies []LabelPolicy `json:"labelPolicies"` + Type string `json:"type"` +} + +type LabelPolicy struct { + Selector string `json:"selector"` +} + +type createAccessPolicyResponse struct { + Data AccessPolicy `json:"data"` +} + +type AccessPolicy struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type ListAccessPoliciesParams struct { + RequestID string + Name string + // this is needed until we fully migrate from gcom to authapi + Region string +} + +type listAccessPoliciesResponse struct { + Data []AccessPolicy `json:"data"` +} + +type DeleteAccessPolicyParams struct { + RequestID string + AccessPolicyID string + // this is needed until we fully migrate from gcom to authapi + Region string +} + +type ListTokenParams struct { + RequestID string + AccessPolicyName string + TokenName string + // this is needed until we fully migrate from gcom to authapi + Region string +} + +type CreateTokenParams struct { + RequestID string + // this is needed until we fully migrate from gcom to authapi + Region string +} + +type CreateTokenPayload struct { + AccessPolicyID string `json:"accessPolicyId"` + DisplayName string `json:"displayName"` + Name string `json:"name"` + ExpiresAt time.Time `json:"expiresAt"` +} + +type createTokenResponse struct { + Data Token `json:"data"` +} + +// Token returned by authapi api when a token gets created. +type Token struct { + ID string `json:"id"` + AccessPolicyID string `json:"accessPolicyId"` + Name string `json:"name"` + Token string `json:"token"` +} + +type DeleteTokenParams struct { + RequestID string + TokenID string + // this is needed until we fully migrate from gcom to authapi + Region string +} + +// TokenView returned by authapi api for a GET token request. +type TokenView struct { + ID string `json:"id"` + AccessPolicyID string `json:"accessPolicyId"` + Name string `json:"name"` + DisplayName string `json:"displayName"` + ExpiresAt string `json:"expiresAt"` + FirstUsedAt string `json:"firstUsedAt"` + LastUsedAt string `json:"lastUsedAt"` + CreatedAt string `json:"createdAt"` +} + +type listTokensResponse struct { + Data []TokenView `json:"data"` +} + +var _ Service = (*AuthApiClient)(nil) + +type AuthApiClient struct { + log log.Logger + cfg Config + httpClient *http.Client +} + +type Config struct { + ApiURL string + Token string +} + +func New(cfg Config, httpClient *http.Client) Service { + return &AuthApiClient{ + log: log.New(LogPrefix), + cfg: cfg, + httpClient: httpClient, + } +} + +func (client *AuthApiClient) CreateAccessPolicy(ctx context.Context, params CreateAccessPolicyParams, payload CreateAccessPolicyPayload) (AccessPolicy, error) { + endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/accesspolicies") + if err != nil { + return AccessPolicy{}, fmt.Errorf("building authapi access policy url: %w", err) + } + + body, err := json.Marshal(&payload) + if err != nil { + return AccessPolicy{}, fmt.Errorf("marshaling request body: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) + if err != nil { + return AccessPolicy{}, fmt.Errorf("creating http request: %w", err) + } + + request.Header.Set("x-request-id", params.RequestID) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token)) + + response, err := client.httpClient.Do(request) + if err != nil { + return AccessPolicy{}, fmt.Errorf("sending http request to create access policy: %w", err) + } + defer func() { + if err := response.Body.Close(); err != nil { + client.log.Error("closing http response body", "err", err.Error()) + } + }() + + if response.StatusCode != http.StatusOK { + body, _ := io.ReadAll(response.Body) + return AccessPolicy{}, fmt.Errorf("unexpected response when creating access policy: code=%d body=%s", response.StatusCode, body) + } + + var capResp createAccessPolicyResponse + if err := json.NewDecoder(response.Body).Decode(&capResp); err != nil { + return AccessPolicy{}, fmt.Errorf("unmarshaling response body: %w", err) + } + + return capResp.Data, nil +} + +func (client *AuthApiClient) DeleteAccessPolicy(ctx context.Context, params DeleteAccessPolicyParams) (bool, error) { + endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/accesspolicies/", params.AccessPolicyID) + if err != nil { + return false, fmt.Errorf("building authapi access policy url: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return false, fmt.Errorf("creating http request: %w", err) + } + + request.Header.Set("x-request-id", params.RequestID) + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token)) + + response, err := client.httpClient.Do(request) + if err != nil { + return false, fmt.Errorf("sending http request to create access policy: %w", err) + } + defer func() { + if err := response.Body.Close(); err != nil { + client.log.Error("closing http response body", "err", err.Error()) + } + }() + + if response.StatusCode == http.StatusNotFound { + return false, nil + } + + if response.StatusCode == http.StatusOK || response.StatusCode == http.StatusNoContent { + return true, nil + } + + body, _ := io.ReadAll(response.Body) + return false, fmt.Errorf("unexpected response when deleting access policy: code=%d body=%s", response.StatusCode, body) +} + +func (client *AuthApiClient) ListAccessPolicies(ctx context.Context, params ListAccessPoliciesParams) ([]AccessPolicy, error) { + endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/accesspolicies") + if err != nil { + return nil, fmt.Errorf("building authapi access policy url: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, fmt.Errorf("creating http request: %w", err) + } + + query := url.Values{} + query.Set("name", params.Name) + request.URL.RawQuery = query.Encode() + request.Header.Set("x-request-id", params.RequestID) + request.Header.Set("Accept", "application/json") + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token)) + + response, err := client.httpClient.Do(request) + if err != nil { + return nil, fmt.Errorf("sending http request to create access policy: %w", err) + } + defer func() { + if err := response.Body.Close(); err != nil { + client.log.Error("closing http response body", "err", err.Error()) + } + }() + + if response.StatusCode != http.StatusOK { + body, _ := io.ReadAll(response.Body) + return nil, fmt.Errorf("unexpected response when listing access policies: code=%d body=%s", response.StatusCode, body) + } + + var lapResp listAccessPoliciesResponse + if err := json.NewDecoder(response.Body).Decode(&lapResp); err != nil { + return lapResp.Data, fmt.Errorf("unmarshaling response body: %w", err) + } + return lapResp.Data, nil +} + +func (client *AuthApiClient) ListTokens(ctx context.Context, params ListTokenParams) ([]TokenView, error) { + endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/tokens") + if err != nil { + return nil, fmt.Errorf("building authapi tokens url: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, fmt.Errorf("creating http request: %w", err) + } + + query := url.Values{} + query.Set("accessPolicyName", params.AccessPolicyName) + query.Set("name", params.TokenName) + + request.URL.RawQuery = query.Encode() + request.Header.Set("x-request-id", params.RequestID) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token)) + + response, err := client.httpClient.Do(request) + if err != nil { + return nil, fmt.Errorf("sending http request to list access tokens: %w", err) + } + defer func() { + if err := response.Body.Close(); err != nil { + client.log.Error("closing http response body", "err", err.Error()) + } + }() + + if response.StatusCode != http.StatusOK { + body, _ := io.ReadAll(response.Body) + return nil, fmt.Errorf("unexpected response when fetching access tokens: code=%d body=%s", response.StatusCode, body) + } + + var body listTokensResponse + if err := json.NewDecoder(response.Body).Decode(&body); err != nil { + return nil, fmt.Errorf("unmarshaling response body: %w", err) + } + return body.Data, nil +} + +func (client *AuthApiClient) CreateToken(ctx context.Context, params CreateTokenParams, payload CreateTokenPayload) (Token, error) { + endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/tokens") + if err != nil { + return Token{}, fmt.Errorf("building authapi tokens url: %w", err) + } + + body, err := json.Marshal(&payload) + if err != nil { + return Token{}, fmt.Errorf("marshaling request body: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) + if err != nil { + return Token{}, fmt.Errorf("creating http request: %w", err) + } + + request.Header.Set("x-request-id", params.RequestID) + request.Header.Set("Content-Type", "application/json") + + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token)) + + response, err := client.httpClient.Do(request) + if err != nil { + return Token{}, fmt.Errorf("sending http request to create access token: %w", err) + } + defer func() { + if err := response.Body.Close(); err != nil { + client.log.Error("closing http response body", "err", err.Error()) + } + }() + + if response.StatusCode != http.StatusOK { + body, _ := io.ReadAll(response.Body) + return Token{}, fmt.Errorf("unexpected response when creating access token: code=%d body=%s", response.StatusCode, body) + } + + var ctResp createTokenResponse + if err := json.NewDecoder(response.Body).Decode(&ctResp); err != nil { + return Token{}, fmt.Errorf("unmarshaling response body: %w", err) + } + + return ctResp.Data, nil +} + +func (client *AuthApiClient) DeleteToken(ctx context.Context, params DeleteTokenParams) error { + endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/tokens", params.TokenID) + if err != nil { + return fmt.Errorf("building authapi tokens url: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return fmt.Errorf("creating http request: %w", err) + } + + request.Header.Set("x-request-id", params.RequestID) + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token)) + + response, err := client.httpClient.Do(request) + if err != nil { + return fmt.Errorf("sending http request to delete access token: %w", err) + } + defer func() { + if err := response.Body.Close(); err != nil { + client.log.Error("closing http response body", "err", err.Error()) + } + }() + + if response.StatusCode == http.StatusNotFound { + return fmt.Errorf("token id: %s %w", params.TokenID, ErrTokenNotFound) + } + + if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusNoContent { + body, _ := io.ReadAll(response.Body) + return fmt.Errorf("unexpected response when deleting access token: code=%d body=%s", response.StatusCode, body) + } + + return nil +} diff --git a/pkg/services/authapi/fake/authapistub.go b/pkg/services/authapi/fake/authapistub.go new file mode 100644 index 00000000000..b87f393246b --- /dev/null +++ b/pkg/services/authapi/fake/authapistub.go @@ -0,0 +1,69 @@ +package fake + +import ( + "context" + "fmt" + + "github.com/grafana/grafana/pkg/services/authapi" + "github.com/grafana/grafana/pkg/util" +) + +var _ authapi.Service = (*AuthapiStub)(nil) + +type AuthapiStub struct { + // The cloud migration token created by this stub. + Token *authapi.TokenView + Policies map[string]authapi.AccessPolicy +} + +func (client *AuthapiStub) CreateAccessPolicy(_ context.Context, _ authapi.CreateAccessPolicyParams, _ authapi.CreateAccessPolicyPayload) (authapi.AccessPolicy, error) { + randStr := fmt.Sprintf("random-policy-%s", util.GenerateShortUID()) + policy := authapi.AccessPolicy{ + ID: randStr, + Name: randStr, + } + client.Policies[policy.ID] = policy + return policy, nil +} + +func (client *AuthapiStub) DeleteAccessPolicy(_ context.Context, params authapi.DeleteAccessPolicyParams) (bool, error) { + delete(client.Policies, params.AccessPolicyID) + return true, nil +} + +func (client *AuthapiStub) ListAccessPolicies(_ context.Context, _ authapi.ListAccessPoliciesParams) ([]authapi.AccessPolicy, error) { + items := make([]authapi.AccessPolicy, 0) + for _, v := range client.Policies { + items = append(items, v) + } + return items, nil +} + +func (client *AuthapiStub) ListTokens(_ context.Context, _ authapi.ListTokenParams) ([]authapi.TokenView, error) { + if client.Token == nil { + return []authapi.TokenView{}, nil + } + + return []authapi.TokenView{*client.Token}, nil +} + +func (client *AuthapiStub) CreateToken(_ context.Context, _ authapi.CreateTokenParams, payload authapi.CreateTokenPayload) (authapi.Token, error) { + token := authapi.Token{ + ID: fmt.Sprintf("random-token-%s", util.GenerateShortUID()), + Name: payload.Name, + AccessPolicyID: payload.AccessPolicyID, + Token: fmt.Sprintf("completely_fake_token_%s", util.GenerateShortUID()), + } + client.Token = &authapi.TokenView{ + ID: token.ID, + Name: token.Name, + AccessPolicyID: token.AccessPolicyID, + DisplayName: token.Name, + } + return token, nil +} + +func (client *AuthapiStub) DeleteToken(_ context.Context, _ authapi.DeleteTokenParams) error { + client.Token = nil + return nil +} diff --git a/pkg/services/cloudmigration/cloudmigration.go b/pkg/services/cloudmigration/cloudmigration.go index faebba11d50..15f450bcd64 100644 --- a/pkg/services/cloudmigration/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigration.go @@ -3,13 +3,13 @@ package cloudmigration import ( "context" - "github.com/grafana/grafana/pkg/services/gcom" + "github.com/grafana/grafana/pkg/services/authapi" "github.com/grafana/grafana/pkg/services/user" ) type Service interface { // GetToken Returns the cloud migration token if it exists. - GetToken(ctx context.Context) (gcom.TokenView, error) + GetToken(ctx context.Context) (authapi.TokenView, error) // CreateToken Creates a cloud migration token. CreateToken(ctx context.Context) (CreateAccessTokenResponse, error) // ValidateToken Sends a request to GMS to test the token. diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go index 2540df5442e..c38a57b2ce4 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration.go @@ -19,6 +19,8 @@ import ( "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/services/authapi" + "github.com/grafana/grafana/pkg/services/authapi/fake" "github.com/grafana/grafana/pkg/services/cloudmigration" "github.com/grafana/grafana/pkg/services/cloudmigration/api" "github.com/grafana/grafana/pkg/services/cloudmigration/gmsclient" @@ -62,6 +64,7 @@ type Service struct { dsService datasources.DataSourceService gcomService gcom.Service + authApiService authapi.Service dashboardService dashboards.DashboardService folderService folder.Service pluginStore pluginstore.Store @@ -151,9 +154,23 @@ func ProvideService( return nil, fmt.Errorf("creating http client for GCOM: %w", err) } s.gcomService = gcom.New(gcom.Config{ApiURL: cfg.GrafanaComAPIURL, Token: cfg.CloudMigration.GcomAPIToken}, httpClientGcom) + + if features.IsEnabledGlobally(featuremgmt.FlagOnPremToCloudMigrationsAuthApiMig) { + s.log.Info("using authapi client because feature flag is enabled") + httpClientAuthApi, err := httpClientProvider.New() + if err != nil { + return nil, fmt.Errorf("creating http client for AuthApi: %w", err) + } + // the api token is the same as for gcom + s.authApiService = authapi.New(authapi.Config{ApiURL: cfg.CloudMigration.AuthAPIUrl, Token: cfg.CloudMigration.GcomAPIToken}, httpClientAuthApi) + } else { + s.log.Info("using gcom client for auth") + s.authApiService = gcom.New(gcom.Config{ApiURL: cfg.GrafanaComAPIURL, Token: cfg.CloudMigration.GcomAPIToken}, httpClientGcom).(*gcom.GcomClient) + } } else { s.gmsClient = gmsclient.NewInMemoryClient() - s.gcomService = &gcomStub{policies: map[string]gcom.AccessPolicy{}, token: nil} + s.gcomService = &gcomStub{} + s.authApiService = &fake.AuthapiStub{Policies: map[string]authapi.AccessPolicy{}, Token: nil} s.cfg.StackID = "12345" } @@ -169,7 +186,7 @@ func ProvideService( return s, nil } -func (s *Service) GetToken(ctx context.Context) (gcom.TokenView, error) { +func (s *Service) GetToken(ctx context.Context) (authapi.TokenView, error) { ctx, span := s.tracer.Start(ctx, "CloudMigrationService.GetToken") defer span.End() logger := s.log.FromContext(ctx) @@ -179,7 +196,7 @@ func (s *Service) GetToken(ctx context.Context) (gcom.TokenView, error) { defer cancel() instance, err := s.gcomService.GetInstanceByID(timeoutCtx, requestID, s.cfg.StackID) if err != nil { - return gcom.TokenView{}, fmt.Errorf("fetching instance by id: id=%s %w", s.cfg.StackID, err) + return authapi.TokenView{}, fmt.Errorf("fetching instance by id: id=%s %w", s.cfg.StackID, err) } logger.Info("instance found", "slug", instance.Slug) @@ -189,14 +206,14 @@ func (s *Service) GetToken(ctx context.Context) (gcom.TokenView, error) { timeoutCtx, cancel = context.WithTimeout(ctx, s.cfg.CloudMigration.ListTokensTimeout) defer cancel() - tokens, err := s.gcomService.ListTokens(timeoutCtx, gcom.ListTokenParams{ + tokens, err := s.authApiService.ListTokens(timeoutCtx, authapi.ListTokenParams{ RequestID: requestID, - Region: instance.RegionSlug, AccessPolicyName: accessPolicyName, TokenName: accessTokenName, + Region: instance.RegionSlug, }) if err != nil { - return gcom.TokenView{}, fmt.Errorf("listing tokens: %w", err) + return authapi.TokenView{}, fmt.Errorf("listing tokens: %w", err) } logger.Info("found access tokens", "num_tokens", len(tokens)) @@ -208,7 +225,7 @@ func (s *Service) GetToken(ctx context.Context) (gcom.TokenView, error) { } logger.Info("cloud migration token not found") - return gcom.TokenView{}, fmt.Errorf("fetching cloud migration token: instance=%+v accessPolicyName=%s accessTokenName=%s %w", + return authapi.TokenView{}, fmt.Errorf("fetching cloud migration token: instance=%+v accessPolicyName=%s accessTokenName=%s %w", instance, accessPolicyName, accessTokenName, cloudmigration.ErrTokenNotFound) } @@ -231,7 +248,7 @@ func (s *Service) CreateToken(ctx context.Context) (cloudmigration.CreateAccessT timeoutCtx, cancel = context.WithTimeout(ctx, s.cfg.CloudMigration.FetchAccessPolicyTimeout) defer cancel() - existingAccessPolicy, err := s.findAccessPolicyByName(timeoutCtx, instance.RegionSlug, accessPolicyName) + existingAccessPolicy, err := s.findAccessPolicyByName(timeoutCtx, accessPolicyName, instance.RegionSlug) if err != nil { return cloudmigration.CreateAccessTokenResponse{}, fmt.Errorf("fetching access policy by name: name=%s %w", accessPolicyName, err) } @@ -239,7 +256,7 @@ func (s *Service) CreateToken(ctx context.Context) (cloudmigration.CreateAccessT if existingAccessPolicy != nil { timeoutCtx, cancel := context.WithTimeout(ctx, s.cfg.CloudMigration.DeleteAccessPolicyTimeout) defer cancel() - if _, err := s.gcomService.DeleteAccessPolicy(timeoutCtx, gcom.DeleteAccessPolicyParams{ + if _, err := s.authApiService.DeleteAccessPolicy(timeoutCtx, authapi.DeleteAccessPolicyParams{ RequestID: requestID, AccessPolicyID: existingAccessPolicy.ID, Region: instance.RegionSlug, @@ -251,15 +268,15 @@ func (s *Service) CreateToken(ctx context.Context) (cloudmigration.CreateAccessT timeoutCtx, cancel = context.WithTimeout(ctx, s.cfg.CloudMigration.CreateAccessPolicyTimeout) defer cancel() - accessPolicy, err := s.gcomService.CreateAccessPolicy(timeoutCtx, - gcom.CreateAccessPolicyParams{ + accessPolicy, err := s.authApiService.CreateAccessPolicy(timeoutCtx, + authapi.CreateAccessPolicyParams{ RequestID: requestID, Region: instance.RegionSlug, }, - gcom.CreateAccessPolicyPayload{ + authapi.CreateAccessPolicyPayload{ Name: accessPolicyName, DisplayName: accessPolicyDisplayName, - Realms: []gcom.Realm{{Type: "stack", Identifier: s.cfg.StackID, LabelPolicies: []gcom.LabelPolicy{}}}, + Realms: []authapi.Realm{{Type: "stack", Identifier: s.cfg.StackID, LabelPolicies: []authapi.LabelPolicy{}}}, Scopes: []string{"cloud-migrations:read", "cloud-migrations:write"}, }) if err != nil { @@ -273,9 +290,12 @@ func (s *Service) CreateToken(ctx context.Context) (cloudmigration.CreateAccessT timeoutCtx, cancel = context.WithTimeout(ctx, s.cfg.CloudMigration.CreateTokenTimeout) defer cancel() - token, err := s.gcomService.CreateToken(timeoutCtx, - gcom.CreateTokenParams{RequestID: requestID, Region: instance.RegionSlug}, - gcom.CreateTokenPayload{ + token, err := s.authApiService.CreateToken(timeoutCtx, + authapi.CreateTokenParams{ + RequestID: requestID, + Region: instance.RegionSlug, + }, + authapi.CreateTokenPayload{ AccessPolicyID: accessPolicy.ID, Name: accessTokenName, DisplayName: accessTokenDisplayName, @@ -303,14 +323,14 @@ func (s *Service) CreateToken(ctx context.Context) (cloudmigration.CreateAccessT return cloudmigration.CreateAccessTokenResponse{Token: base64.StdEncoding.EncodeToString(bytes)}, nil } -func (s *Service) findAccessPolicyByName(ctx context.Context, regionSlug, accessPolicyName string) (*gcom.AccessPolicy, error) { - accessPolicies, err := s.gcomService.ListAccessPolicies(ctx, gcom.ListAccessPoliciesParams{ +func (s *Service) findAccessPolicyByName(ctx context.Context, accessPolicyName string, region string) (*authapi.AccessPolicy, error) { + accessPolicies, err := s.authApiService.ListAccessPolicies(ctx, authapi.ListAccessPoliciesParams{ RequestID: tracing.TraceIDFromContext(ctx, false), - Region: regionSlug, Name: accessPolicyName, + Region: region, }) if err != nil { - return nil, fmt.Errorf("listing access policies: name=%s region=%s :%w", accessPolicyName, regionSlug, err) + return nil, fmt.Errorf("listing access policies: name=%s :%w", accessPolicyName, err) } for _, accessPolicy := range accessPolicies { @@ -349,10 +369,10 @@ func (s *Service) DeleteToken(ctx context.Context, tokenID string) error { timeoutCtx, cancel = context.WithTimeout(ctx, s.cfg.CloudMigration.DeleteTokenTimeout) defer cancel() - if err := s.gcomService.DeleteToken(timeoutCtx, gcom.DeleteTokenParams{ + if err := s.authApiService.DeleteToken(timeoutCtx, authapi.DeleteTokenParams{ RequestID: tracing.TraceIDFromContext(ctx, false), - Region: instance.RegionSlug, TokenID: tokenID, + Region: instance.RegionSlug, }); err != nil && !errors.Is(err, gcom.ErrTokenNotFound) { return fmt.Errorf("deleting cloud migration token: tokenID=%s %w", tokenID, err) } diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go index 28bd9ef5cc0..e0e559e23eb 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/cloudmigration_noop.go @@ -3,8 +3,8 @@ package cloudmigrationimpl import ( "context" + "github.com/grafana/grafana/pkg/services/authapi" "github.com/grafana/grafana/pkg/services/cloudmigration" - "github.com/grafana/grafana/pkg/services/gcom" "github.com/grafana/grafana/pkg/services/user" ) @@ -13,8 +13,8 @@ type NoopServiceImpl struct{} var _ cloudmigration.Service = (*NoopServiceImpl)(nil) -func (s *NoopServiceImpl) GetToken(ctx context.Context) (gcom.TokenView, error) { - return gcom.TokenView{}, cloudmigration.ErrFeatureDisabledError +func (s *NoopServiceImpl) GetToken(ctx context.Context) (authapi.TokenView, error) { + return authapi.TokenView{}, cloudmigration.ErrFeatureDisabledError } func (s *NoopServiceImpl) CreateToken(ctx context.Context) (cloudmigration.CreateAccessTokenResponse, error) { diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go b/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go index 020e5e96122..2bc0e4ea56a 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/fake/cloudmigration_fake.go @@ -6,8 +6,8 @@ import ( "sort" "time" + "github.com/grafana/grafana/pkg/services/authapi" "github.com/grafana/grafana/pkg/services/cloudmigration" - "github.com/grafana/grafana/pkg/services/gcom" "github.com/grafana/grafana/pkg/services/user" ) @@ -20,11 +20,11 @@ type FakeServiceImpl struct { var _ cloudmigration.Service = (*FakeServiceImpl)(nil) -func (m FakeServiceImpl) GetToken(_ context.Context) (gcom.TokenView, error) { +func (m FakeServiceImpl) GetToken(_ context.Context) (authapi.TokenView, error) { if m.ReturnError { - return gcom.TokenView{}, fmt.Errorf("mock error") + return authapi.TokenView{}, fmt.Errorf("mock error") } - return gcom.TokenView{ID: "mock_id", DisplayName: "mock_name"}, nil + return authapi.TokenView{ID: "mock_id", DisplayName: "mock_name"}, nil } func (m FakeServiceImpl) CreateToken(_ context.Context) (cloudmigration.CreateAccessTokenResponse, error) { diff --git a/pkg/services/cloudmigration/cloudmigrationimpl/gcomstub.go b/pkg/services/cloudmigration/cloudmigrationimpl/gcomstub.go index ca18a1d90ec..bce5d0a6134 100644 --- a/pkg/services/cloudmigration/cloudmigrationimpl/gcomstub.go +++ b/pkg/services/cloudmigration/cloudmigrationimpl/gcomstub.go @@ -6,16 +6,12 @@ import ( "strconv" "github.com/grafana/grafana/pkg/services/gcom" - "github.com/grafana/grafana/pkg/util" ) type gcomStub struct { - // The cloud migration token created by this stub. - token *gcom.TokenView - policies map[string]gcom.AccessPolicy } -func (client *gcomStub) GetInstanceByID(ctx context.Context, requestID string, instanceID string) (gcom.Instance, error) { +func (client *gcomStub) GetInstanceByID(_ context.Context, _ string, instanceID string) (gcom.Instance, error) { id, err := strconv.Atoi(instanceID) if err != nil { return gcom.Instance{}, fmt.Errorf("parsing instanceID: %w", err) @@ -27,55 +23,3 @@ func (client *gcomStub) GetInstanceByID(ctx context.Context, requestID string, i ClusterSlug: "fake-cluser", }, nil } - -func (client *gcomStub) CreateAccessPolicy(ctx context.Context, params gcom.CreateAccessPolicyParams, payload gcom.CreateAccessPolicyPayload) (gcom.AccessPolicy, error) { - randStr := fmt.Sprintf("random-policy-%s", util.GenerateShortUID()) - policy := gcom.AccessPolicy{ - ID: randStr, - Name: randStr, - } - client.policies[policy.ID] = policy - return policy, nil -} - -func (client *gcomStub) DeleteAccessPolicy(ctx context.Context, params gcom.DeleteAccessPolicyParams) (bool, error) { - delete(client.policies, params.AccessPolicyID) - return true, nil -} - -func (client *gcomStub) ListAccessPolicies(ctx context.Context, params gcom.ListAccessPoliciesParams) ([]gcom.AccessPolicy, error) { - items := make([]gcom.AccessPolicy, 0) - for _, v := range client.policies { - items = append(items, v) - } - return items, nil -} - -func (client *gcomStub) ListTokens(ctx context.Context, params gcom.ListTokenParams) ([]gcom.TokenView, error) { - if client.token == nil { - return []gcom.TokenView{}, nil - } - - return []gcom.TokenView{*client.token}, nil -} - -func (client *gcomStub) CreateToken(ctx context.Context, params gcom.CreateTokenParams, payload gcom.CreateTokenPayload) (gcom.Token, error) { - token := gcom.Token{ - ID: fmt.Sprintf("random-token-%s", util.GenerateShortUID()), - Name: payload.Name, - AccessPolicyID: payload.AccessPolicyID, - Token: fmt.Sprintf("completely_fake_token_%s", util.GenerateShortUID()), - } - client.token = &gcom.TokenView{ - ID: token.ID, - Name: token.Name, - AccessPolicyID: token.AccessPolicyID, - DisplayName: token.Name, - } - return token, nil -} - -func (client *gcomStub) DeleteToken(ctx context.Context, params gcom.DeleteTokenParams) error { - client.token = nil - return nil -} diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 3c3823981c8..d688806e1fc 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1058,6 +1058,12 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaOperatorExperienceSquad, }, + { + Name: "onPremToCloudMigrationsAuthApiMig", + Description: "Enables the use of auth api instead of gcom for internal token services. Requires `onPremToCloudMigrations` to be enabled in conjunction.", + Stage: FeatureStageExperimental, + Owner: grafanaOperatorExperienceSquad, + }, { Name: "alertingSaveStatePeriodic", Description: "Writes the state periodically to the database, asynchronous to rule evaluation", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index bd6982ee039..a9233bbeec4 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -139,6 +139,7 @@ newFolderPicker,experimental,@grafana/grafana-frontend-platform,false,false,true jitterAlertRulesWithinGroups,preview,@grafana/alerting-squad,false,true,false onPremToCloudMigrations,preview,@grafana/grafana-operator-experience-squad,false,false,false onPremToCloudMigrationsAlerts,experimental,@grafana/grafana-operator-experience-squad,false,false,false +onPremToCloudMigrationsAuthApiMig,experimental,@grafana/grafana-operator-experience-squad,false,false,false alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,false,false,false promQLScope,GA,@grafana/observability-metrics,false,false,false logQLScope,privatePreview,@grafana/observability-logs,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 8578b6d27b0..1043d2b3cc6 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -567,6 +567,10 @@ const ( // Enables the migration of alerts and its child resources to your Grafana Cloud stack. Requires `onPremToCloudMigrations` to be enabled in conjunction. FlagOnPremToCloudMigrationsAlerts = "onPremToCloudMigrationsAlerts" + // FlagOnPremToCloudMigrationsAuthApiMig + // Enables the use of auth api instead of gcom for internal token services. Requires `onPremToCloudMigrations` to be enabled in conjunction. + FlagOnPremToCloudMigrationsAuthApiMig = "onPremToCloudMigrationsAuthApiMig" + // FlagAlertingSaveStatePeriodic // Writes the state periodically to the database, asynchronous to rule evaluation FlagAlertingSaveStatePeriodic = "alertingSaveStatePeriodic" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 8d71e0d3bf5..88a55a353e5 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2404,6 +2404,18 @@ "codeowner": "@grafana/grafana-operator-experience-squad" } }, + { + "metadata": { + "name": "onPremToCloudMigrationsAuthApiMig", + "resourceVersion": "1732033809064", + "creationTimestamp": "2024-11-19T16:30:09Z" + }, + "spec": { + "description": "Enables the use of auth api instead of gcom for internal token services. Requires `onPremToCloudMigrations` to be enabled in conjunction.", + "stage": "experimental", + "codeowner": "@grafana/grafana-operator-experience-squad" + } + }, { "metadata": { "name": "openSearchBackendFlowEnabled", diff --git a/pkg/services/gcom/gcom.go b/pkg/services/gcom/gcom.go index 2db6dc7e1d2..b780be988d2 100644 --- a/pkg/services/gcom/gcom.go +++ b/pkg/services/gcom/gcom.go @@ -1,7 +1,6 @@ package gcom import ( - "bytes" "context" "encoding/json" "errors" @@ -9,7 +8,6 @@ import ( "io" "net/http" "net/url" - "time" "github.com/grafana/grafana/pkg/infra/log" ) @@ -20,12 +18,6 @@ var ErrTokenNotFound = errors.New("gcom: token not found") type Service interface { GetInstanceByID(ctx context.Context, requestID string, instanceID string) (Instance, error) - CreateAccessPolicy(ctx context.Context, params CreateAccessPolicyParams, payload CreateAccessPolicyPayload) (AccessPolicy, error) - ListAccessPolicies(ctx context.Context, params ListAccessPoliciesParams) ([]AccessPolicy, error) - DeleteAccessPolicy(ctx context.Context, params DeleteAccessPolicyParams) (bool, error) - ListTokens(ctx context.Context, params ListTokenParams) ([]TokenView, error) - CreateToken(ctx context.Context, params CreateTokenParams, payload CreateTokenPayload) (Token, error) - DeleteToken(ctx context.Context, params DeleteTokenParams) error } type Instance struct { @@ -33,98 +25,7 @@ type Instance struct { Slug string `json:"slug"` RegionSlug string `json:"regionSlug"` ClusterSlug string `json:"clusterSlug"` -} - -type CreateAccessPolicyParams struct { - RequestID string - Region string -} - -type CreateAccessPolicyPayload struct { - Name string `json:"name"` - DisplayName string `json:"displayName"` - Realms []Realm `json:"realms"` - Scopes []string `json:"scopes"` -} - -type Realm struct { - Identifier string `json:"identifier"` - LabelPolicies []LabelPolicy `json:"labelPolicies"` - Type string `json:"type"` -} - -type LabelPolicy struct { - Selector string `json:"selector"` -} - -type AccessPolicy struct { - ID string `json:"id"` - Name string `json:"name"` -} - -type ListAccessPoliciesParams struct { - RequestID string - Region string - Name string -} - -type ListAccessPoliciesResponse struct { - Items []AccessPolicy `json:"items"` -} - -type DeleteAccessPolicyParams struct { - RequestID string - AccessPolicyID string - Region string -} - -type ListTokenParams struct { - RequestID string - Region string - AccessPolicyName string - TokenName string -} - -type CreateTokenParams struct { - RequestID string - Region string -} - -type CreateTokenPayload struct { - AccessPolicyID string `json:"accessPolicyId"` - DisplayName string `json:"displayName"` - Name string `json:"name"` - ExpiresAt time.Time `json:"expiresAt"` -} - -// The token returned by gcom api when a token gets created. -type Token struct { - ID string `json:"id"` - AccessPolicyID string `json:"accessPolicyId"` - Name string `json:"name"` - Token string `json:"token"` -} - -type DeleteTokenParams struct { - RequestID string - Region string - TokenID string -} - -// The token returned by gcom api for a GET token request. -type TokenView struct { - ID string `json:"id"` - AccessPolicyID string `json:"accessPolicyId"` - Name string `json:"name"` - DisplayName string `json:"displayName"` - ExpiresAt string `json:"expiresAt"` - FirstUsedAt string `json:"firstUsedAt"` - LastUsedAt string `json:"lastUsedAt"` - CreatedAt string `json:"createdAt"` -} - -type listTokensResponse struct { - Items []TokenView `json:"items"` + OrgId int `json:"orgId"` } type GcomClient struct { @@ -184,267 +85,3 @@ func (client *GcomClient) GetInstanceByID(ctx context.Context, requestID string, return instance, nil } - -func (client *GcomClient) CreateAccessPolicy(ctx context.Context, params CreateAccessPolicyParams, payload CreateAccessPolicyPayload) (AccessPolicy, error) { - endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/accesspolicies") - if err != nil { - return AccessPolicy{}, fmt.Errorf("building gcom access policy url: %w", err) - } - - body, err := json.Marshal(&payload) - if err != nil { - return AccessPolicy{}, fmt.Errorf("marshaling request body: %w", err) - } - - request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) - if err != nil { - return AccessPolicy{}, fmt.Errorf("creating http request: %w", err) - } - - query := url.Values{} - query.Set("region", params.Region) - - request.URL.RawQuery = query.Encode() - request.Header.Set("x-request-id", params.RequestID) - request.Header.Set("Content-Type", "application/json") - - request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token)) - - response, err := client.httpClient.Do(request) - if err != nil { - return AccessPolicy{}, fmt.Errorf("sending http request to create access policy: %w", err) - } - defer func() { - if err := response.Body.Close(); err != nil { - client.log.Error("closing http response body", "err", err.Error()) - } - }() - - if response.StatusCode != http.StatusOK { - body, _ := io.ReadAll(response.Body) - return AccessPolicy{}, fmt.Errorf("unexpected response when creating access policy: code=%d body=%s", response.StatusCode, body) - } - - var accessPolicy AccessPolicy - if err := json.NewDecoder(response.Body).Decode(&accessPolicy); err != nil { - return accessPolicy, fmt.Errorf("unmarshaling response body: %w", err) - } - - return accessPolicy, nil -} - -func (client *GcomClient) DeleteAccessPolicy(ctx context.Context, params DeleteAccessPolicyParams) (bool, error) { - endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/accesspolicies/", params.AccessPolicyID) - if err != nil { - return false, fmt.Errorf("building gcom access policy url: %w", err) - } - - request, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return false, fmt.Errorf("creating http request: %w", err) - } - - query := url.Values{} - query.Set("region", params.Region) - - request.URL.RawQuery = query.Encode() - request.Header.Set("x-request-id", params.RequestID) - request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token)) - - response, err := client.httpClient.Do(request) - if err != nil { - return false, fmt.Errorf("sending http request to create access policy: %w", err) - } - defer func() { - if err := response.Body.Close(); err != nil { - client.log.Error("closing http response body", "err", err.Error()) - } - }() - - if response.StatusCode == http.StatusNotFound { - return false, nil - } - - if response.StatusCode == http.StatusOK || response.StatusCode == http.StatusNoContent { - return true, nil - } - - body, _ := io.ReadAll(response.Body) - return false, fmt.Errorf("unexpected response when deleting access policy: code=%d body=%s", response.StatusCode, body) -} - -func (client *GcomClient) ListAccessPolicies(ctx context.Context, params ListAccessPoliciesParams) ([]AccessPolicy, error) { - endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/accesspolicies") - if err != nil { - return nil, fmt.Errorf("building gcom access policy url: %w", err) - } - - request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, fmt.Errorf("creating http request: %w", err) - } - - query := url.Values{} - query.Set("region", params.Region) - query.Set("name", params.Name) - request.URL.RawQuery = query.Encode() - request.Header.Set("x-request-id", params.RequestID) - request.Header.Set("Accept", "application/json") - - request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token)) - - response, err := client.httpClient.Do(request) - if err != nil { - return nil, fmt.Errorf("sending http request to create access policy: %w", err) - } - defer func() { - if err := response.Body.Close(); err != nil { - client.log.Error("closing http response body", "err", err.Error()) - } - }() - - if response.StatusCode != http.StatusOK { - body, _ := io.ReadAll(response.Body) - return nil, fmt.Errorf("unexpected response when listing access policies: code=%d body=%s", response.StatusCode, body) - } - - var responseBody ListAccessPoliciesResponse - if err := json.NewDecoder(response.Body).Decode(&responseBody); err != nil { - return responseBody.Items, fmt.Errorf("unmarshaling response body: %w", err) - } - - return responseBody.Items, nil -} - -func (client *GcomClient) ListTokens(ctx context.Context, params ListTokenParams) ([]TokenView, error) { - endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/tokens") - if err != nil { - return nil, fmt.Errorf("building gcom tokens url: %w", err) - } - - request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, fmt.Errorf("creating http request: %w", err) - } - - query := url.Values{} - query.Set("region", params.Region) - query.Set("accessPolicyName", params.AccessPolicyName) - query.Set("name", params.TokenName) - - request.URL.RawQuery = query.Encode() - request.Header.Set("x-request-id", params.RequestID) - request.Header.Set("Content-Type", "application/json") - - request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token)) - - response, err := client.httpClient.Do(request) - if err != nil { - return nil, fmt.Errorf("sending http request to list access tokens: %w", err) - } - defer func() { - if err := response.Body.Close(); err != nil { - client.log.Error("closing http response body", "err", err.Error()) - } - }() - - if response.StatusCode != http.StatusOK { - body, _ := io.ReadAll(response.Body) - return nil, fmt.Errorf("unexpected response when fetching access tokens: code=%d body=%s", response.StatusCode, body) - } - - var body listTokensResponse - if err := json.NewDecoder(response.Body).Decode(&body); err != nil { - return nil, fmt.Errorf("unmarshaling response body: %w", err) - } - - return body.Items, nil -} - -func (client *GcomClient) CreateToken(ctx context.Context, params CreateTokenParams, payload CreateTokenPayload) (Token, error) { - endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/tokens") - if err != nil { - return Token{}, fmt.Errorf("building gcom tokens url: %w", err) - } - - body, err := json.Marshal(&payload) - if err != nil { - return Token{}, fmt.Errorf("marshaling request body: %w", err) - } - - request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) - if err != nil { - return Token{}, fmt.Errorf("creating http request: %w", err) - } - - query := url.Values{} - query.Set("region", params.Region) - - request.URL.RawQuery = query.Encode() - request.Header.Set("x-request-id", params.RequestID) - request.Header.Set("Content-Type", "application/json") - - request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token)) - - response, err := client.httpClient.Do(request) - if err != nil { - return Token{}, fmt.Errorf("sending http request to create access token: %w", err) - } - defer func() { - if err := response.Body.Close(); err != nil { - client.log.Error("closing http response body", "err", err.Error()) - } - }() - - if response.StatusCode != http.StatusOK { - body, _ := io.ReadAll(response.Body) - return Token{}, fmt.Errorf("unexpected response when creating access token: code=%d body=%s", response.StatusCode, body) - } - - var token Token - if err := json.NewDecoder(response.Body).Decode(&token); err != nil { - return token, fmt.Errorf("unmarshaling response body: %w", err) - } - - return token, nil -} - -func (client *GcomClient) DeleteToken(ctx context.Context, params DeleteTokenParams) error { - endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/tokens", params.TokenID) - if err != nil { - return fmt.Errorf("building gcom tokens url: %w", err) - } - - request, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil) - if err != nil { - return fmt.Errorf("creating http request: %w", err) - } - - query := url.Values{} - query.Set("region", params.Region) - - request.URL.RawQuery = query.Encode() - request.Header.Set("x-request-id", params.RequestID) - request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token)) - - response, err := client.httpClient.Do(request) - if err != nil { - return fmt.Errorf("sending http request to delete access token: %w", err) - } - defer func() { - if err := response.Body.Close(); err != nil { - client.log.Error("closing http response body", "err", err.Error()) - } - }() - - if response.StatusCode == http.StatusNotFound { - return fmt.Errorf("token id: %s %w", params.TokenID, ErrTokenNotFound) - } - - if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusNoContent { - body, _ := io.ReadAll(response.Body) - return fmt.Errorf("unexpected response when deleting access token: code=%d body=%s", response.StatusCode, body) - } - - return nil -} diff --git a/pkg/services/gcom/gcomauth.go b/pkg/services/gcom/gcomauth.go new file mode 100644 index 00000000000..a43faf13d73 --- /dev/null +++ b/pkg/services/gcom/gcomauth.go @@ -0,0 +1,289 @@ +package gcom + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + + "github.com/grafana/grafana/pkg/services/authapi" +) + +// this will be removed when service in authapi is fully enabled + +type listTokensResponse struct { + Items []authapi.TokenView `json:"items"` +} + +type listAccessPoliciesResponse struct { + Items []authapi.AccessPolicy `json:"items"` +} + +var _ authapi.Service = (*GcomClient)(nil) + +func (client *GcomClient) CreateAccessPolicy(ctx context.Context, params authapi.CreateAccessPolicyParams, payload authapi.CreateAccessPolicyPayload) (authapi.AccessPolicy, error) { + endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/accesspolicies") + if err != nil { + return authapi.AccessPolicy{}, fmt.Errorf("building gcom access policy url: %w", err) + } + + body, err := json.Marshal(&payload) + if err != nil { + return authapi.AccessPolicy{}, fmt.Errorf("marshaling request body: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) + if err != nil { + return authapi.AccessPolicy{}, fmt.Errorf("creating http request: %w", err) + } + + query := url.Values{} + query.Set("region", params.Region) + + request.URL.RawQuery = query.Encode() + request.Header.Set("x-request-id", params.RequestID) + request.Header.Set("Content-Type", "application/json") + + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token)) + + response, err := client.httpClient.Do(request) + if err != nil { + return authapi.AccessPolicy{}, fmt.Errorf("sending http request to create access policy: %w", err) + } + defer func() { + if err := response.Body.Close(); err != nil { + client.log.Error("closing http response body", "err", err.Error()) + } + }() + + if response.StatusCode != http.StatusOK { + body, _ := io.ReadAll(response.Body) + return authapi.AccessPolicy{}, fmt.Errorf("unexpected response when creating access policy: code=%d body=%s", response.StatusCode, body) + } + + var accessPolicy authapi.AccessPolicy + if err := json.NewDecoder(response.Body).Decode(&accessPolicy); err != nil { + return accessPolicy, fmt.Errorf("unmarshaling response body: %w", err) + } + + return accessPolicy, nil +} + +func (client *GcomClient) DeleteAccessPolicy(ctx context.Context, params authapi.DeleteAccessPolicyParams) (bool, error) { + endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/accesspolicies/", params.AccessPolicyID) + if err != nil { + return false, fmt.Errorf("building gcom access policy url: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return false, fmt.Errorf("creating http request: %w", err) + } + + query := url.Values{} + query.Set("region", params.Region) + + request.URL.RawQuery = query.Encode() + request.Header.Set("x-request-id", params.RequestID) + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token)) + + response, err := client.httpClient.Do(request) + if err != nil { + return false, fmt.Errorf("sending http request to create access policy: %w", err) + } + defer func() { + if err := response.Body.Close(); err != nil { + client.log.Error("closing http response body", "err", err.Error()) + } + }() + + if response.StatusCode == http.StatusNotFound { + return false, nil + } + + if response.StatusCode == http.StatusOK || response.StatusCode == http.StatusNoContent { + return true, nil + } + + body, _ := io.ReadAll(response.Body) + return false, fmt.Errorf("unexpected response when deleting access policy: code=%d body=%s", response.StatusCode, body) +} + +func (client *GcomClient) ListAccessPolicies(ctx context.Context, params authapi.ListAccessPoliciesParams) ([]authapi.AccessPolicy, error) { + endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/accesspolicies") + if err != nil { + return nil, fmt.Errorf("building gcom access policy url: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, fmt.Errorf("creating http request: %w", err) + } + + query := url.Values{} + query.Set("region", params.Region) + query.Set("name", params.Name) + request.URL.RawQuery = query.Encode() + request.Header.Set("x-request-id", params.RequestID) + request.Header.Set("Accept", "application/json") + + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token)) + + response, err := client.httpClient.Do(request) + if err != nil { + return nil, fmt.Errorf("sending http request to create access policy: %w", err) + } + defer func() { + if err := response.Body.Close(); err != nil { + client.log.Error("closing http response body", "err", err.Error()) + } + }() + + if response.StatusCode != http.StatusOK { + body, _ := io.ReadAll(response.Body) + return nil, fmt.Errorf("unexpected response when listing access policies: code=%d body=%s", response.StatusCode, body) + } + + var responseBody listAccessPoliciesResponse + if err := json.NewDecoder(response.Body).Decode(&responseBody); err != nil { + return responseBody.Items, fmt.Errorf("unmarshaling response body: %w", err) + } + + return responseBody.Items, nil +} + +func (client *GcomClient) ListTokens(ctx context.Context, params authapi.ListTokenParams) ([]authapi.TokenView, error) { + endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/tokens") + if err != nil { + return nil, fmt.Errorf("building gcom tokens url: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, fmt.Errorf("creating http request: %w", err) + } + + query := url.Values{} + query.Set("region", params.Region) + query.Set("accessPolicyName", params.AccessPolicyName) + query.Set("name", params.TokenName) + + request.URL.RawQuery = query.Encode() + request.Header.Set("x-request-id", params.RequestID) + request.Header.Set("Content-Type", "application/json") + + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token)) + + response, err := client.httpClient.Do(request) + if err != nil { + return nil, fmt.Errorf("sending http request to list access tokens: %w", err) + } + defer func() { + if err := response.Body.Close(); err != nil { + client.log.Error("closing http response body", "err", err.Error()) + } + }() + + if response.StatusCode != http.StatusOK { + body, _ := io.ReadAll(response.Body) + return nil, fmt.Errorf("unexpected response when fetching access tokens: code=%d body=%s", response.StatusCode, body) + } + + var body listTokensResponse + if err := json.NewDecoder(response.Body).Decode(&body); err != nil { + return nil, fmt.Errorf("unmarshaling response body: %w", err) + } + + return body.Items, nil +} + +func (client *GcomClient) CreateToken(ctx context.Context, params authapi.CreateTokenParams, payload authapi.CreateTokenPayload) (authapi.Token, error) { + endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/tokens") + if err != nil { + return authapi.Token{}, fmt.Errorf("building gcom tokens url: %w", err) + } + + body, err := json.Marshal(&payload) + if err != nil { + return authapi.Token{}, fmt.Errorf("marshaling request body: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) + if err != nil { + return authapi.Token{}, fmt.Errorf("creating http request: %w", err) + } + + query := url.Values{} + query.Set("region", params.Region) + + request.URL.RawQuery = query.Encode() + request.Header.Set("x-request-id", params.RequestID) + request.Header.Set("Content-Type", "application/json") + + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token)) + + response, err := client.httpClient.Do(request) + if err != nil { + return authapi.Token{}, fmt.Errorf("sending http request to create access token: %w", err) + } + defer func() { + if err := response.Body.Close(); err != nil { + client.log.Error("closing http response body", "err", err.Error()) + } + }() + + if response.StatusCode != http.StatusOK { + body, _ := io.ReadAll(response.Body) + return authapi.Token{}, fmt.Errorf("unexpected response when creating access token: code=%d body=%s", response.StatusCode, body) + } + + var token authapi.Token + if err := json.NewDecoder(response.Body).Decode(&token); err != nil { + return token, fmt.Errorf("unmarshaling response body: %w", err) + } + + return token, nil +} + +func (client *GcomClient) DeleteToken(ctx context.Context, params authapi.DeleteTokenParams) error { + endpoint, err := url.JoinPath(client.cfg.ApiURL, "/v1/tokens", params.TokenID) + if err != nil { + return fmt.Errorf("building gcom tokens url: %w", err) + } + + request, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return fmt.Errorf("creating http request: %w", err) + } + + query := url.Values{} + query.Set("region", params.Region) + + request.URL.RawQuery = query.Encode() + request.Header.Set("x-request-id", params.RequestID) + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.cfg.Token)) + + response, err := client.httpClient.Do(request) + if err != nil { + return fmt.Errorf("sending http request to delete access token: %w", err) + } + defer func() { + if err := response.Body.Close(); err != nil { + client.log.Error("closing http response body", "err", err.Error()) + } + }() + + if response.StatusCode == http.StatusNotFound { + return fmt.Errorf("token id: %s %w", params.TokenID, ErrTokenNotFound) + } + + if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusNoContent { + body, _ := io.ReadAll(response.Body) + return fmt.Errorf("unexpected response when deleting access token: code=%d body=%s", response.StatusCode, body) + } + + return nil +} diff --git a/pkg/setting/setting_cloud_migration.go b/pkg/setting/setting_cloud_migration.go index e05eb3b7f42..6f20e0d6bc1 100644 --- a/pkg/setting/setting_cloud_migration.go +++ b/pkg/setting/setting_cloud_migration.go @@ -8,6 +8,7 @@ import ( type CloudMigrationSettings struct { IsTarget bool GcomAPIToken string + AuthAPIUrl string SnapshotFolder string GMSDomain string GMSStartSnapshotTimeout time.Duration @@ -33,6 +34,7 @@ func (cfg *Cfg) readCloudMigrationSettings() { cloudMigration := cfg.Raw.Section("cloud_migration") cfg.CloudMigration.IsTarget = cloudMigration.Key("is_target").MustBool(false) cfg.CloudMigration.GcomAPIToken = cloudMigration.Key("gcom_api_token").MustString("") + cfg.CloudMigration.AuthAPIUrl = cloudMigration.Key("auth_api_url").MustString("") cfg.CloudMigration.SnapshotFolder = cloudMigration.Key("snapshot_folder").MustString("") cfg.CloudMigration.GMSDomain = cloudMigration.Key("domain").MustString("") cfg.CloudMigration.GMSValidateKeyTimeout = cloudMigration.Key("validate_key_timeout").MustDuration(5 * time.Second)