diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index cb5739d44eb..674963631d8 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -109,13 +109,14 @@ func (hs *HTTPServer) AdminUpdateUserPassword(c *models.ReqContext) response.Res return response.Error(400, "New password too short", nil) } - userQuery := models.GetUserByIdQuery{Id: userID} + userQuery := user.GetUserByIDQuery{ID: userID} - if err := hs.SQLStore.GetUserById(c.Req.Context(), &userQuery); err != nil { + usr, err := hs.userService.GetByID(c.Req.Context(), &userQuery) + if err != nil { return response.Error(500, "Could not read user from database", err) } - passwordHashed, err := util.EncodePassword(form.Password, userQuery.Result.Salt) + passwordHashed, err := util.EncodePassword(form.Password, usr.Salt) if err != nil { return response.Error(500, "Could not encode password", err) } diff --git a/pkg/api/admin_users_test.go b/pkg/api/admin_users_test.go index 71a5d6bbf55..282f4e73504 100644 --- a/pkg/api/admin_users_test.go +++ b/pkg/api/admin_users_test.go @@ -4,6 +4,9 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" @@ -15,9 +18,8 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) const ( @@ -29,7 +31,7 @@ const ( func TestAdminAPIEndpoint(t *testing.T) { const role = models.ROLE_ADMIN - + userService := usertest.NewUserServiceFake() t.Run("Given a server admin attempts to remove themselves as an admin", func(t *testing.T) { updateCmd := dtos.AdminUpdateUserPermissionsForm{ IsGrafanaAdmin: false, @@ -45,46 +47,43 @@ func TestAdminAPIEndpoint(t *testing.T) { }) t.Run("When a server admin attempts to logout himself from all devices", func(t *testing.T) { - mock := mockstore.NewSQLStoreMock() adminLogoutUserScenario(t, "Should not be allowed when calling POST on", "/api/admin/users/1/logout", "/api/admin/users/:id/logout", func(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 400, sc.resp.Code) - }, mock) + }, userService) }) t.Run("When a server admin attempts to logout a non-existing user from all devices", func(t *testing.T) { - mock := &mockstore.SQLStoreMock{ - ExpectedError: user.ErrUserNotFound, - } + mockUserService := usertest.NewUserServiceFake() + mockUserService.ExpectedError = user.ErrUserNotFound + adminLogoutUserScenario(t, "Should return not found when calling POST on", "/api/admin/users/200/logout", "/api/admin/users/:id/logout", func(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 404, sc.resp.Code) - }, mock) + }, mockUserService) }) t.Run("When a server admin attempts to revoke an auth token for a non-existing user", func(t *testing.T) { cmd := models.RevokeAuthTokenCmd{AuthTokenId: 2} - mock := &mockstore.SQLStoreMock{ - ExpectedError: user.ErrUserNotFound, - } + mockUser := usertest.NewUserServiceFake() + mockUser.ExpectedError = user.ErrUserNotFound adminRevokeUserAuthTokenScenario(t, "Should return not found when calling POST on", "/api/admin/users/200/revoke-auth-token", "/api/admin/users/:id/revoke-auth-token", cmd, func(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 404, sc.resp.Code) - }, mock) + }, mockUser) }) t.Run("When a server admin gets auth tokens for a non-existing user", func(t *testing.T) { - mock := &mockstore.SQLStoreMock{ - ExpectedError: user.ErrUserNotFound, - } + mockUserService := usertest.NewUserServiceFake() + mockUserService.ExpectedError = user.ErrUserNotFound adminGetUserAuthTokensScenario(t, "Should return not found when calling GET on", "/api/admin/users/200/auth-tokens", "/api/admin/users/:id/auth-tokens", func(sc *scenarioContext) { sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() assert.Equal(t, 404, sc.resp.Code) - }, mock) + }, mockUserService) }) t.Run("When a server admin attempts to enable/disable a nonexistent user", func(t *testing.T) { @@ -154,17 +153,15 @@ func TestAdminAPIEndpoint(t *testing.T) { adminDeleteUserScenario(t, "Should return user not found error", "/api/admin/users/42", "/api/admin/users/:id", func(sc *scenarioContext) { sc.sqlStore.(*mockstore.SQLStoreMock).ExpectedError = user.ErrUserNotFound + sc.userService.(*usertest.FakeUserService).ExpectedError = user.ErrUserNotFound sc.authInfoService.ExpectedError = user.ErrUserNotFound sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() - userID := sc.sqlStore.(*mockstore.SQLStoreMock).LatestUserId assert.Equal(t, 404, sc.resp.Code) respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) require.NoError(t, err) assert.Equal(t, "user not found", respJSON.Get("message").MustString()) - - assert.Equal(t, int64(42), userID) }) }) @@ -266,11 +263,11 @@ func putAdminScenario(t *testing.T, desc string, url string, routePattern string }) } -func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc, sqlStore sqlstore.Store) { +func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc, userService *usertest.FakeUserService) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { hs := HTTPServer{ AuthTokenService: auth.NewFakeUserAuthTokenService(), - SQLStore: sqlStore, + userService: userService, } sc := setupScenarioContext(t, url) @@ -291,13 +288,13 @@ func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern }) } -func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePattern string, cmd models.RevokeAuthTokenCmd, fn scenarioFunc, sqlStore sqlstore.Store) { +func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePattern string, cmd models.RevokeAuthTokenCmd, fn scenarioFunc, userService user.Service) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { fakeAuthTokenService := auth.NewFakeUserAuthTokenService() hs := HTTPServer{ AuthTokenService: fakeAuthTokenService, - SQLStore: sqlStore, + userService: userService, } sc := setupScenarioContext(t, url) @@ -319,13 +316,13 @@ func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, rou }) } -func adminGetUserAuthTokensScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc, sqlStore sqlstore.Store) { +func adminGetUserAuthTokensScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc, userService *usertest.FakeUserService) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { fakeAuthTokenService := auth.NewFakeUserAuthTokenService() hs := HTTPServer{ AuthTokenService: fakeAuthTokenService, - SQLStore: sqlStore, + userService: userService, } sc := setupScenarioContext(t, url) @@ -379,7 +376,8 @@ func adminDisableUserScenario(t *testing.T, desc string, action string, url stri func adminDeleteUserScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc) { hs := HTTPServer{ - SQLStore: mockstore.NewSQLStoreMock(), + SQLStore: mockstore.NewSQLStoreMock(), + userService: usertest.NewUserServiceFake(), } t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { sc := setupScenarioContext(t, url) @@ -391,6 +389,7 @@ func adminDeleteUserScenario(t *testing.T, desc string, url string, routePattern return hs.AdminDeleteUser(c) }) + sc.userService = hs.userService sc.m.Delete(routePattern, sc.defaultHandler) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index fc8c6765c92..94af971ed47 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -43,6 +43,7 @@ import ( "github.com/grafana/grafana/pkg/services/searchusers" "github.com/grafana/grafana/pkg/services/searchusers/filters" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web/webtest" @@ -168,6 +169,7 @@ type scenarioContext struct { sqlStore sqlstore.Store authInfoService *logintest.AuthInfoServiceFake dashboardVersionService dashver.Service + userService user.Service } func (sc *scenarioContext) exec() { diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 53107d39795..2800cc7099b 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -28,6 +28,7 @@ import ( "github.com/grafana/grafana/pkg/services/guardian" pref "github.com/grafana/grafana/pkg/services/preference" "github.com/grafana/grafana/pkg/services/star" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) @@ -250,12 +251,12 @@ func (hs *HTTPServer) getAnnotationPermissionsByScope(c *models.ReqContext, acti } func (hs *HTTPServer) getUserLogin(ctx context.Context, userID int64) string { - query := models.GetUserByIdQuery{Id: userID} - err := hs.SQLStore.GetUserById(ctx, &query) + query := user.GetUserByIDQuery{ID: userID} + user, err := hs.userService.GetByID(ctx, &query) if err != nil { return anonString } - return query.Result.Login + return user.Login } func (hs *HTTPServer) getDashboardHelper(ctx context.Context, orgID int64, id int64, uid string) (*models.Dashboard, response.Response) { diff --git a/pkg/api/ldap_debug.go b/pkg/api/ldap_debug.go index 4db55e18f56..000a42ee436 100644 --- a/pkg/api/ldap_debug.go +++ b/pkg/api/ldap_debug.go @@ -209,9 +209,10 @@ func (hs *HTTPServer) PostSyncUserWithLDAP(c *models.ReqContext) response.Respon return response.Error(http.StatusBadRequest, "id is invalid", err) } - query := models.GetUserByIdQuery{Id: userId} + query := user.GetUserByIDQuery{ID: userId} - if err := hs.SQLStore.GetUserById(c.Req.Context(), &query); err != nil { // validate the userId exists + usr, err := hs.userService.GetByID(c.Req.Context(), &query) + if err != nil { // validate the userId exists if errors.Is(err, user.ErrUserNotFound) { return response.Error(404, user.ErrUserNotFound.Error(), nil) } @@ -219,7 +220,7 @@ func (hs *HTTPServer) PostSyncUserWithLDAP(c *models.ReqContext) response.Respon return response.Error(500, "Failed to get user", err) } - authModuleQuery := &models.GetAuthInfoQuery{UserId: query.Result.ID, AuthModule: models.AuthModuleLDAP} + authModuleQuery := &models.GetAuthInfoQuery{UserId: usr.ID, AuthModule: models.AuthModuleLDAP} if err := hs.authInfoService.GetAuthInfo(c.Req.Context(), authModuleQuery); err != nil { // validate the userId comes from LDAP if errors.Is(err, user.ErrUserNotFound) { return response.Error(404, user.ErrUserNotFound.Error(), nil) @@ -229,17 +230,17 @@ func (hs *HTTPServer) PostSyncUserWithLDAP(c *models.ReqContext) response.Respon } ldapServer := newLDAP(ldapConfig.Servers) - user, _, err := ldapServer.User(query.Result.Login) + userInfo, _, err := ldapServer.User(usr.Login) if err != nil { if errors.Is(err, multildap.ErrDidNotFindUser) { // User was not in the LDAP server - we need to take action: - if hs.Cfg.AdminUser == query.Result.Login { // User is *the* Grafana Admin. We cannot disable it. - errMsg := fmt.Sprintf(`Refusing to sync grafana super admin "%s" - it would be disabled`, query.Result.Login) + if hs.Cfg.AdminUser == usr.Login { // User is *the* Grafana Admin. We cannot disable it. + errMsg := fmt.Sprintf(`Refusing to sync grafana super admin "%s" - it would be disabled`, usr.Login) ldapLogger.Error(errMsg) return response.Error(http.StatusBadRequest, errMsg, err) } // Since the user was not in the LDAP server. Let's disable it. - err := hs.Login.DisableExternalUser(c.Req.Context(), query.Result.Login) + err := hs.Login.DisableExternalUser(c.Req.Context(), usr.Login) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to disable the user", err) } @@ -258,10 +259,10 @@ func (hs *HTTPServer) PostSyncUserWithLDAP(c *models.ReqContext) response.Respon upsertCmd := &models.UpsertUserCommand{ ReqContext: c, - ExternalUser: user, + ExternalUser: userInfo, SignupAllowed: hs.Cfg.LDAPAllowSignup, UserLookupParams: models.UserLookupParams{ - UserID: &query.Result.ID, // Upsert by ID only + UserID: &usr.ID, // Upsert by ID only Email: nil, Login: nil, }, diff --git a/pkg/api/ldap_debug_test.go b/pkg/api/ldap_debug_test.go index 509e3e684dd..fa4d5586d39 100644 --- a/pkg/api/ldap_debug_test.go +++ b/pkg/api/ldap_debug_test.go @@ -8,22 +8,22 @@ import ( "path/filepath" "testing" - "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/login/loginservice" - "github.com/grafana/grafana/pkg/services/login/logintest" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" - "github.com/grafana/grafana/pkg/services/user" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/ldap" + "github.com/grafana/grafana/pkg/services/login/loginservice" + "github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/multildap" + "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) type LDAPMock struct { @@ -363,7 +363,7 @@ func TestGetLDAPStatusAPIEndpoint(t *testing.T) { // PostSyncUserWithLDAP tests // *** -func postSyncUserWithLDAPContext(t *testing.T, requestURL string, preHook func(*testing.T, *scenarioContext), sqlstoremock sqlstore.Store) *scenarioContext { +func postSyncUserWithLDAPContext(t *testing.T, requestURL string, preHook func(*testing.T, *scenarioContext), userService user.Service) *scenarioContext { t.Helper() sc := setupScenarioContext(t, requestURL) @@ -378,9 +378,9 @@ func postSyncUserWithLDAPContext(t *testing.T, requestURL string, preHook func(* hs := &HTTPServer{ Cfg: sc.cfg, AuthTokenService: auth.NewFakeUserAuthTokenService(), - SQLStore: sqlstoremock, Login: loginservice.LoginServiceMock{}, authInfoService: sc.authInfoService, + userService: userService, } sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { @@ -403,8 +403,8 @@ func postSyncUserWithLDAPContext(t *testing.T, requestURL string, preHook func(* } func TestPostSyncUserWithLDAPAPIEndpoint_Success(t *testing.T) { - sqlstoremock := mockstore.SQLStoreMock{} - sqlstoremock.ExpectedUser = &user.User{Login: "ldap-daniel", ID: 34} + userServiceMock := usertest.NewUserServiceFake() + userServiceMock.ExpectedUser = &user.User{Login: "ldap-daniel", ID: 34} sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34", func(t *testing.T, sc *scenarioContext) { getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) { return &ldap.Config{}, nil @@ -417,7 +417,7 @@ func TestPostSyncUserWithLDAPAPIEndpoint_Success(t *testing.T) { userSearchResult = &models.ExternalUserInfo{ Login: "ldap-daniel", } - }, &sqlstoremock) + }, userServiceMock) assert.Equal(t, http.StatusOK, sc.resp.Code) @@ -431,7 +431,8 @@ func TestPostSyncUserWithLDAPAPIEndpoint_Success(t *testing.T) { } func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotFound(t *testing.T) { - sqlstoremock := mockstore.SQLStoreMock{ExpectedError: user.ErrUserNotFound} + userServiceMock := usertest.NewUserServiceFake() + userServiceMock.ExpectedError = user.ErrUserNotFound sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34", func(t *testing.T, sc *scenarioContext) { getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) { return &ldap.Config{}, nil @@ -440,7 +441,7 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotFound(t *testing.T) { newLDAP = func(_ []*ldap.ServerConfig) multildap.IMultiLDAP { return &LDAPMock{} } - }, &sqlstoremock) + }, userServiceMock) assert.Equal(t, http.StatusNotFound, sc.resp.Code) @@ -454,7 +455,8 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotFound(t *testing.T) { } func TestPostSyncUserWithLDAPAPIEndpoint_WhenGrafanaAdmin(t *testing.T) { - sqlstoremock := mockstore.SQLStoreMock{ExpectedUser: &user.User{Login: "ldap-daniel", ID: 34}} + userServiceMock := usertest.NewUserServiceFake() + userServiceMock.ExpectedUser = &user.User{Login: "ldap-daniel", ID: 34} sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34", func(t *testing.T, sc *scenarioContext) { getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) { return &ldap.Config{}, nil @@ -467,7 +469,7 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenGrafanaAdmin(t *testing.T) { userSearchError = multildap.ErrDidNotFindUser sc.cfg.AdminUser = "ldap-daniel" - }, &sqlstoremock) + }, userServiceMock) assert.Equal(t, http.StatusBadRequest, sc.resp.Code) var res map[string]interface{} @@ -478,7 +480,8 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenGrafanaAdmin(t *testing.T) { } func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotInLDAP(t *testing.T) { - sqlstoremock := mockstore.SQLStoreMock{ExpectedUser: &user.User{Login: "ldap-daniel", ID: 34}} + userServiceMock := usertest.NewUserServiceFake() + userServiceMock.ExpectedUser = &user.User{Login: "ldap-daniel", ID: 34} sc := postSyncUserWithLDAPContext(t, "/api/admin/ldap/sync/34", func(t *testing.T, sc *scenarioContext) { sc.authInfoService.ExpectedExternalUser = &models.ExternalUserInfo{IsDisabled: true, UserId: 34} getLDAPConfig = func(*setting.Cfg) (*ldap.Config, error) { @@ -491,7 +494,7 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotInLDAP(t *testing.T) { userSearchResult = nil userSearchError = multildap.ErrDidNotFindUser - }, &sqlstoremock) + }, userServiceMock) assert.Equal(t, http.StatusBadRequest, sc.resp.Code) @@ -603,6 +606,7 @@ func TestLDAP_AccessControl(t *testing.T) { cfg.LDAPEnabled = true sc, hs := setupAccessControlScenarioContext(t, cfg, test.url, test.permissions) hs.SQLStore = &mockstore.SQLStoreMock{ExpectedUser: &user.User{}} + hs.userService = &usertest.FakeUserService{ExpectedUser: &user.User{}} hs.authInfoService = &logintest.AuthInfoServiceFake{} hs.Login = &loginservice.LoginServiceMock{} sc.resp = httptest.NewRecorder() diff --git a/pkg/api/user.go b/pkg/api/user.go index 41c9f7d2ace..816ca61aab8 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -387,17 +387,18 @@ func (hs *HTTPServer) ChangeUserPassword(c *models.ReqContext) response.Response return response.Error(400, "Not allowed to change password when LDAP or Auth Proxy is enabled", nil) } - userQuery := models.GetUserByIdQuery{Id: c.UserId} + userQuery := user.GetUserByIDQuery{ID: c.UserId} - if err := hs.SQLStore.GetUserById(c.Req.Context(), &userQuery); err != nil { + user, err := hs.userService.GetByID(c.Req.Context(), &userQuery) + if err != nil { return response.Error(500, "Could not read user from database", err) } - passwordHashed, err := util.EncodePassword(cmd.OldPassword, userQuery.Result.Salt) + passwordHashed, err := util.EncodePassword(cmd.OldPassword, user.Salt) if err != nil { return response.Error(500, "Failed to encode password", err) } - if passwordHashed != userQuery.Result.Password { + if passwordHashed != user.Password { return response.Error(401, "Invalid old password", nil) } @@ -407,7 +408,7 @@ func (hs *HTTPServer) ChangeUserPassword(c *models.ReqContext) response.Response } cmd.UserId = c.UserId - cmd.NewPassword, err = util.EncodePassword(cmd.NewPassword, userQuery.Result.Salt) + cmd.NewPassword, err = util.EncodePassword(cmd.NewPassword, user.Salt) if err != nil { return response.Error(500, "Failed to encode password", err) } diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go index c7ec173f524..3ef8029f4b3 100644 --- a/pkg/api/user_test.go +++ b/pkg/api/user_test.go @@ -26,6 +26,7 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" ) @@ -49,7 +50,7 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) { loggedInUserScenario(t, "When calling GET on", "api/users/1", "api/users/:id", func(sc *scenarioContext) { fakeNow := time.Date(2019, 2, 11, 17, 30, 40, 0, time.UTC) secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore)) - authInfoStore := authinfostore.ProvideAuthInfoStore(sqlStore, secretsService) + authInfoStore := authinfostore.ProvideAuthInfoStore(sqlStore, secretsService, usertest.NewUserServiceFake()) srv := authinfoservice.ProvideAuthInfoService( &authinfoservice.OSSUserProtectionImpl{}, authInfoStore, diff --git a/pkg/api/user_token.go b/pkg/api/user_token.go index 74810d72e44..376f7e45776 100644 --- a/pkg/api/user_token.go +++ b/pkg/api/user_token.go @@ -51,16 +51,17 @@ func (hs *HTTPServer) RevokeUserAuthToken(c *models.ReqContext) response.Respons } func (hs *HTTPServer) logoutUserFromAllDevicesInternal(ctx context.Context, userID int64) response.Response { - userQuery := models.GetUserByIdQuery{Id: userID} + userQuery := user.GetUserByIDQuery{ID: userID} - if err := hs.SQLStore.GetUserById(ctx, &userQuery); err != nil { + _, err := hs.userService.GetByID(ctx, &userQuery) + if err != nil { if errors.Is(err, user.ErrUserNotFound) { return response.Error(404, "User not found", err) } return response.Error(500, "Could not read user from database", err) } - err := hs.AuthTokenService.RevokeAllUserTokens(ctx, userID) + err = hs.AuthTokenService.RevokeAllUserTokens(ctx, userID) if err != nil { return response.Error(500, "Failed to logout user", err) } @@ -71,9 +72,10 @@ func (hs *HTTPServer) logoutUserFromAllDevicesInternal(ctx context.Context, user } func (hs *HTTPServer) getUserAuthTokensInternal(c *models.ReqContext, userID int64) response.Response { - userQuery := models.GetUserByIdQuery{Id: userID} + userQuery := user.GetUserByIDQuery{ID: userID} - if err := hs.SQLStore.GetUserById(c.Req.Context(), &userQuery); err != nil { + _, err := hs.userService.GetByID(c.Req.Context(), &userQuery) + if err != nil { if errors.Is(err, user.ErrUserNotFound) { return response.Error(http.StatusNotFound, "User not found", err) } else if errors.Is(err, user.ErrCaseInsensitive) { @@ -142,8 +144,9 @@ func (hs *HTTPServer) getUserAuthTokensInternal(c *models.ReqContext, userID int } func (hs *HTTPServer) revokeUserAuthTokenInternal(c *models.ReqContext, userID int64, cmd models.RevokeAuthTokenCmd) response.Response { - userQuery := models.GetUserByIdQuery{Id: userID} - if err := hs.SQLStore.GetUserById(c.Req.Context(), &userQuery); err != nil { + userQuery := user.GetUserByIDQuery{ID: userID} + _, err := hs.userService.GetByID(c.Req.Context(), &userQuery) + if err != nil { if errors.Is(err, user.ErrUserNotFound) { return response.Error(404, "User not found", err) } diff --git a/pkg/api/user_token_test.go b/pkg/api/user_token_test.go index b560f9f5ee1..c52874e1c71 100644 --- a/pkg/api/user_token_test.go +++ b/pkg/api/user_token_test.go @@ -6,62 +6,61 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/auth" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/services/user" - "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/services/user/usertest" ) func TestUserTokenAPIEndpoint(t *testing.T) { - mock := mockstore.NewSQLStoreMock() + userMock := usertest.NewUserServiceFake() t.Run("When current user attempts to revoke an auth token for a non-existing user", func(t *testing.T) { cmd := models.RevokeAuthTokenCmd{AuthTokenId: 2} - mock.ExpectedError = user.ErrUserNotFound + userMock.ExpectedError = user.ErrUserNotFound revokeUserAuthTokenScenario(t, "Should return not found when calling POST on", "/api/user/revoke-auth-token", "/api/user/revoke-auth-token", cmd, 200, func(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 404, sc.resp.Code) - }, mock) + }, userMock) }) t.Run("When current user gets auth tokens for a non-existing user", func(t *testing.T) { - mock := &mockstore.SQLStoreMock{ + mockUser := &usertest.FakeUserService{ ExpectedUser: &user.User{ID: 200}, ExpectedError: user.ErrUserNotFound, } getUserAuthTokensScenario(t, "Should return not found when calling GET on", "/api/user/auth-tokens", "/api/user/auth-tokens", 200, func(sc *scenarioContext) { sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() assert.Equal(t, 404, sc.resp.Code) - }, mock) + }, mockUser) }) t.Run("When logging out an existing user from all devices", func(t *testing.T) { - mock := &mockstore.SQLStoreMock{ + userMock := &usertest.FakeUserService{ ExpectedUser: &user.User{ID: 200}, } logoutUserFromAllDevicesInternalScenario(t, "Should be successful", 1, func(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) - }, mock) + }, userMock) }) t.Run("When logout a non-existing user from all devices", func(t *testing.T) { logoutUserFromAllDevicesInternalScenario(t, "Should return not found", testUserID, func(sc *scenarioContext) { - mock.ExpectedError = user.ErrUserNotFound - + userMock.ExpectedError = user.ErrUserNotFound sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 404, sc.resp.Code) - }, mock) + }, userMock) }) t.Run("When revoke an auth token for a user", func(t *testing.T) { cmd := models.RevokeAuthTokenCmd{AuthTokenId: 2} token := &models.UserToken{Id: 1} - mock := &mockstore.SQLStoreMock{ + mockUser := &usertest.FakeUserService{ ExpectedUser: &user.User{ID: 200}, } @@ -71,25 +70,25 @@ func TestUserTokenAPIEndpoint(t *testing.T) { } sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 200, sc.resp.Code) - }, mock) + }, mockUser) }) t.Run("When revoke the active auth token used by himself", func(t *testing.T) { cmd := models.RevokeAuthTokenCmd{AuthTokenId: 2} token := &models.UserToken{Id: 2} - mock := mockstore.NewSQLStoreMock() + mockUser := usertest.NewUserServiceFake() revokeUserAuthTokenInternalScenario(t, "Should not be successful", cmd, testUserID, token, func(sc *scenarioContext) { sc.userAuthTokenService.GetUserTokenProvider = func(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error) { return token, nil } sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() assert.Equal(t, 400, sc.resp.Code) - }, mock) + }, mockUser) }) t.Run("When gets auth tokens for a user", func(t *testing.T) { currentToken := &models.UserToken{Id: 1} - mock := mockstore.NewSQLStoreMock() + mockUser := usertest.NewUserServiceFake() getUserAuthTokensInternalScenario(t, "Should be successful", currentToken, func(sc *scenarioContext) { tokens := []*models.UserToken{ { @@ -141,18 +140,18 @@ func TestUserTokenAPIEndpoint(t *testing.T) { assert.Equal(t, "11.0", resultTwo.Get("browserVersion").MustString()) assert.Equal(t, "iOS", resultTwo.Get("os").MustString()) assert.Equal(t, "11.0", resultTwo.Get("osVersion").MustString()) - }, mock) + }, mockUser) }) } func revokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePattern string, cmd models.RevokeAuthTokenCmd, - userId int64, fn scenarioFunc, sqlStore sqlstore.Store) { + userId int64, fn scenarioFunc, userService user.Service) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { fakeAuthTokenService := auth.NewFakeUserAuthTokenService() hs := HTTPServer{ AuthTokenService: fakeAuthTokenService, - SQLStore: sqlStore, + userService: userService, } sc := setupScenarioContext(t, url) @@ -173,13 +172,13 @@ func revokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePat }) } -func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePattern string, userId int64, fn scenarioFunc, sqlStore sqlstore.Store) { +func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePattern string, userId int64, fn scenarioFunc, userService user.Service) { t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { fakeAuthTokenService := auth.NewFakeUserAuthTokenService() hs := HTTPServer{ AuthTokenService: fakeAuthTokenService, - SQLStore: sqlStore, + userService: userService, } sc := setupScenarioContext(t, url) @@ -199,11 +198,11 @@ func getUserAuthTokensScenario(t *testing.T, desc string, url string, routePatte }) } -func logoutUserFromAllDevicesInternalScenario(t *testing.T, desc string, userId int64, fn scenarioFunc, sqlStore sqlstore.Store) { +func logoutUserFromAllDevicesInternalScenario(t *testing.T, desc string, userId int64, fn scenarioFunc, userService user.Service) { t.Run(desc, func(t *testing.T) { hs := HTTPServer{ AuthTokenService: auth.NewFakeUserAuthTokenService(), - SQLStore: sqlStore, + userService: userService, } sc := setupScenarioContext(t, "/") @@ -223,13 +222,13 @@ func logoutUserFromAllDevicesInternalScenario(t *testing.T, desc string, userId } func revokeUserAuthTokenInternalScenario(t *testing.T, desc string, cmd models.RevokeAuthTokenCmd, userId int64, - token *models.UserToken, fn scenarioFunc, sqlStore sqlstore.Store) { + token *models.UserToken, fn scenarioFunc, userService user.Service) { t.Run(desc, func(t *testing.T) { fakeAuthTokenService := auth.NewFakeUserAuthTokenService() hs := HTTPServer{ AuthTokenService: fakeAuthTokenService, - SQLStore: sqlStore, + userService: userService, } sc := setupScenarioContext(t, "/") @@ -248,13 +247,13 @@ func revokeUserAuthTokenInternalScenario(t *testing.T, desc string, cmd models.R }) } -func getUserAuthTokensInternalScenario(t *testing.T, desc string, token *models.UserToken, fn scenarioFunc, sqlStore sqlstore.Store) { +func getUserAuthTokensInternalScenario(t *testing.T, desc string, token *models.UserToken, fn scenarioFunc, userService user.Service) { t.Run(desc, func(t *testing.T) { fakeAuthTokenService := auth.NewFakeUserAuthTokenService() hs := HTTPServer{ AuthTokenService: fakeAuthTokenService, - SQLStore: sqlStore, + userService: userService, } sc := setupScenarioContext(t, "/") diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index 631bdc5c744..f2c1c91f997 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -151,7 +151,7 @@ var adminCommands = []*cli.Command{ { Name: "reset-admin-password", Usage: "reset-admin-password ", - Action: runDbCommand(resetPasswordCommand), + Action: runRunnerCommand(resetPasswordCommand), Flags: []cli.Flag{ &cli.BoolFlag{ Name: "password-from-stdin", diff --git a/pkg/cmd/grafana-cli/commands/reset_password_command.go b/pkg/cmd/grafana-cli/commands/reset_password_command.go index 7455d33b1ad..73b1844282c 100644 --- a/pkg/cmd/grafana-cli/commands/reset_password_command.go +++ b/pkg/cmd/grafana-cli/commands/reset_password_command.go @@ -8,15 +8,16 @@ import ( "github.com/fatih/color" "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/runner" "github.com/grafana/grafana/pkg/cmd/grafana-cli/utils" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" ) const AdminUserId = 1 -func resetPasswordCommand(c utils.CommandLine, sqlStore *sqlstore.SQLStore) error { +func resetPasswordCommand(c utils.CommandLine, runner runner.Runner) error { newPassword := "" if c.Bool("password-from-stdin") { @@ -39,13 +40,14 @@ func resetPasswordCommand(c utils.CommandLine, sqlStore *sqlstore.SQLStore) erro return fmt.Errorf("new password is too short") } - userQuery := models.GetUserByIdQuery{Id: AdminUserId} + userQuery := user.GetUserByIDQuery{ID: AdminUserId} - if err := sqlStore.GetUserById(context.Background(), &userQuery); err != nil { + usr, err := runner.UserService.GetByID(context.Background(), &userQuery) + if err != nil { return fmt.Errorf("could not read user from database. Error: %v", err) } - passwordHashed, err := util.EncodePassword(newPassword, userQuery.Result.Salt) + passwordHashed, err := util.EncodePassword(newPassword, usr.Salt) if err != nil { return err } @@ -55,7 +57,7 @@ func resetPasswordCommand(c utils.CommandLine, sqlStore *sqlstore.SQLStore) erro NewPassword: passwordHashed, } - if err := sqlStore.ChangeUserPassword(context.Background(), &cmd); err != nil { + if err := runner.SQLStore.ChangeUserPassword(context.Background(), &cmd); err != nil { return fmt.Errorf("failed to update user password: %w", err) } diff --git a/pkg/cmd/grafana-cli/runner/runner.go b/pkg/cmd/grafana-cli/runner/runner.go index 6fcb7f68ab0..bc3c95a6f26 100644 --- a/pkg/cmd/grafana-cli/runner/runner.go +++ b/pkg/cmd/grafana-cli/runner/runner.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -17,11 +18,13 @@ type Runner struct { EncryptionService encryption.Internal SecretsService *manager.SecretsService SecretsMigrator secrets.Migrator + UserService user.Service } func New(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, settingsProvider setting.Provider, encryptionService encryption.Internal, features featuremgmt.FeatureToggles, secretsService *manager.SecretsService, secretsMigrator secrets.Migrator, + userService user.Service, ) Runner { return Runner{ Cfg: cfg, @@ -31,5 +34,6 @@ func New(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, settingsProvider setting SecretsService: secretsService, SecretsMigrator: secretsMigrator, Features: features, + UserService: userService, } } diff --git a/pkg/cmd/grafana-cli/runner/wire.go b/pkg/cmd/grafana-cli/runner/wire.go index f95db61c9a6..0dc5666ccca 100644 --- a/pkg/cmd/grafana-cli/runner/wire.go +++ b/pkg/cmd/grafana-cli/runner/wire.go @@ -7,21 +7,132 @@ import ( "context" "github.com/google/wire" + + sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/api" + "github.com/grafana/grafana/pkg/api/avatar" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/cuectx" + "github.com/grafana/grafana/pkg/expr" + cmreg "github.com/grafana/grafana/pkg/framework/coremodel/registry" + "github.com/grafana/grafana/pkg/infra/httpclient" + "github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider" + "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/localcache" + "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/infra/remotecache" + "github.com/grafana/grafana/pkg/infra/serverlock" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/usagestats" + uss "github.com/grafana/grafana/pkg/infra/usagestats/service" + "github.com/grafana/grafana/pkg/infra/usagestats/statscollector" + loginpkg "github.com/grafana/grafana/pkg/login" + "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/middleware/csrf" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" + "github.com/grafana/grafana/pkg/plugins/manager" + "github.com/grafana/grafana/pkg/plugins/manager/loader" + "github.com/grafana/grafana/pkg/plugins/manager/registry" + "github.com/grafana/grafana/pkg/plugins/plugincontext" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" + "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/jwt" + "github.com/grafana/grafana/pkg/services/cleanup" + "github.com/grafana/grafana/pkg/services/comments" + "github.com/grafana/grafana/pkg/services/contexthandler" + "github.com/grafana/grafana/pkg/services/contexthandler/authproxy" + "github.com/grafana/grafana/pkg/services/dashboardimport" + dashboardimportservice "github.com/grafana/grafana/pkg/services/dashboardimport/service" + "github.com/grafana/grafana/pkg/services/dashboards" + dashboardstore "github.com/grafana/grafana/pkg/services/dashboards/database" + dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service" + "github.com/grafana/grafana/pkg/services/dashboardsnapshots" + dashsnapstore "github.com/grafana/grafana/pkg/services/dashboardsnapshots/database" + dashsnapsvc "github.com/grafana/grafana/pkg/services/dashboardsnapshots/service" + "github.com/grafana/grafana/pkg/services/dashboardversion/dashverimpl" + "github.com/grafana/grafana/pkg/services/datasourceproxy" + "github.com/grafana/grafana/pkg/services/datasources" + datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service" "github.com/grafana/grafana/pkg/services/encryption" encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service" + "github.com/grafana/grafana/pkg/services/export" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/hooks" + "github.com/grafana/grafana/pkg/services/libraryelements" + "github.com/grafana/grafana/pkg/services/librarypanels" + "github.com/grafana/grafana/pkg/services/live" + "github.com/grafana/grafana/pkg/services/live/pushhttp" + "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/login/authinfoservice" + authinfodatabase "github.com/grafana/grafana/pkg/services/login/authinfoservice/database" + "github.com/grafana/grafana/pkg/services/login/loginservice" + "github.com/grafana/grafana/pkg/services/ngalert" + ngmetrics "github.com/grafana/grafana/pkg/services/ngalert/metrics" + "github.com/grafana/grafana/pkg/services/notifications" + "github.com/grafana/grafana/pkg/services/oauthtoken" + "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/playlist/playlistimpl" + "github.com/grafana/grafana/pkg/services/plugindashboards" + plugindashboardsservice "github.com/grafana/grafana/pkg/services/plugindashboards/service" + "github.com/grafana/grafana/pkg/services/pluginsettings" + pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service" + "github.com/grafana/grafana/pkg/services/preference/prefimpl" + "github.com/grafana/grafana/pkg/services/publicdashboards" + publicdashboardsApi "github.com/grafana/grafana/pkg/services/publicdashboards/api" + publicdashboardsStore "github.com/grafana/grafana/pkg/services/publicdashboards/database" + publicdashboardsService "github.com/grafana/grafana/pkg/services/publicdashboards/service" + "github.com/grafana/grafana/pkg/services/query" + "github.com/grafana/grafana/pkg/services/queryhistory" + "github.com/grafana/grafana/pkg/services/quota/quotaimpl" + "github.com/grafana/grafana/pkg/services/rendering" + "github.com/grafana/grafana/pkg/services/search" + "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/services/secrets" secretsDatabase "github.com/grafana/grafana/pkg/services/secrets/database" + secretsStore "github.com/grafana/grafana/pkg/services/secrets/kvstore" + secretsMigrations "github.com/grafana/grafana/pkg/services/secrets/kvstore/migrations" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" secretsMigrator "github.com/grafana/grafana/pkg/services/secrets/migrator" + "github.com/grafana/grafana/pkg/services/serviceaccounts" + "github.com/grafana/grafana/pkg/services/serviceaccounts/database" + serviceaccountsmanager "github.com/grafana/grafana/pkg/services/serviceaccounts/manager" + "github.com/grafana/grafana/pkg/services/shorturls" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/db" + "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" + "github.com/grafana/grafana/pkg/services/star/starimpl" + "github.com/grafana/grafana/pkg/services/store" + "github.com/grafana/grafana/pkg/services/store/sanitizer" + "github.com/grafana/grafana/pkg/services/teamguardian" + teamguardianDatabase "github.com/grafana/grafana/pkg/services/teamguardian/database" + teamguardianManager "github.com/grafana/grafana/pkg/services/teamguardian/manager" + "github.com/grafana/grafana/pkg/services/thumbs" + "github.com/grafana/grafana/pkg/services/updatechecker" + "github.com/grafana/grafana/pkg/services/user/userimpl" + "github.com/grafana/grafana/pkg/services/userauth/userauthimpl" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb/azuremonitor" + "github.com/grafana/grafana/pkg/tsdb/cloudmonitoring" + "github.com/grafana/grafana/pkg/tsdb/cloudwatch" + "github.com/grafana/grafana/pkg/tsdb/elasticsearch" + "github.com/grafana/grafana/pkg/tsdb/grafanads" + "github.com/grafana/grafana/pkg/tsdb/graphite" + "github.com/grafana/grafana/pkg/tsdb/influxdb" + "github.com/grafana/grafana/pkg/tsdb/legacydata" + legacydataservice "github.com/grafana/grafana/pkg/tsdb/legacydata/service" + "github.com/grafana/grafana/pkg/tsdb/loki" + "github.com/grafana/grafana/pkg/tsdb/mssql" + "github.com/grafana/grafana/pkg/tsdb/mysql" + "github.com/grafana/grafana/pkg/tsdb/postgres" + "github.com/grafana/grafana/pkg/tsdb/prometheus" + "github.com/grafana/grafana/pkg/tsdb/tempo" + "github.com/grafana/grafana/pkg/tsdb/testdatasource" "github.com/grafana/grafana/pkg/web" ) @@ -42,9 +153,171 @@ var wireSet = wire.NewSet( wire.Bind(new(secrets.Store), new(*secretsDatabase.SecretsStoreImpl)), secretsManager.ProvideSecretsService, wire.Bind(new(secrets.Service), new(*secretsManager.SecretsService)), + hooks.ProvideService, + legacydataservice.ProvideService, + wire.Bind(new(legacydata.RequestHandler), new(*legacydataservice.Service)), + alerting.ProvideAlertEngine, + wire.Bind(new(alerting.UsageStatsQuerier), new(*alerting.AlertEngine)), + api.ProvideHTTPServer, + query.ProvideService, + thumbs.ProvideService, + rendering.ProvideService, + wire.Bind(new(rendering.Service), new(*rendering.RenderingService)), + kvstore.ProvideService, + updatechecker.ProvideGrafanaService, + updatechecker.ProvidePluginsService, + uss.ProvideService, + registry.ProvideService, + wire.Bind(new(registry.Service), new(*registry.InMemory)), + manager.ProvideService, + wire.Bind(new(plugins.Manager), new(*manager.PluginManager)), + wire.Bind(new(plugins.Client), new(*manager.PluginManager)), + wire.Bind(new(plugins.Store), new(*manager.PluginManager)), + wire.Bind(new(plugins.DashboardFileStore), new(*manager.PluginManager)), + wire.Bind(new(plugins.StaticRouteResolver), new(*manager.PluginManager)), + wire.Bind(new(plugins.RendererManager), new(*manager.PluginManager)), + wire.Bind(new(plugins.SecretsPluginManager), new(*manager.PluginManager)), + coreplugin.ProvideCoreRegistry, + loader.ProvideService, + wire.Bind(new(loader.Service), new(*loader.Loader)), + wire.Bind(new(plugins.ErrorResolver), new(*loader.Loader)), + cloudwatch.ProvideService, + cloudmonitoring.ProvideService, + azuremonitor.ProvideService, + postgres.ProvideService, + mysql.ProvideService, + mssql.ProvideService, + store.ProvideEntityEventsService, + httpclientprovider.New, + wire.Bind(new(httpclient.Provider), new(*sdkhttpclient.Provider)), + serverlock.ProvideService, + cleanup.ProvideService, + shorturls.ProvideService, + wire.Bind(new(shorturls.Service), new(*shorturls.ShortURLService)), + queryhistory.ProvideService, + wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), + quotaimpl.ProvideService, + remotecache.ProvideService, + loginservice.ProvideService, + wire.Bind(new(login.Service), new(*loginservice.Implementation)), + authinfoservice.ProvideAuthInfoService, + wire.Bind(new(login.AuthInfoService), new(*authinfoservice.Implementation)), + authinfodatabase.ProvideAuthInfoStore, + loginpkg.ProvideService, + wire.Bind(new(loginpkg.Authenticator), new(*loginpkg.AuthenticatorService)), + datasourceproxy.ProvideService, + search.ProvideService, + searchV2.ProvideService, + store.ProvideService, + export.ProvideService, + live.ProvideService, + pushhttp.ProvideService, + plugincontext.ProvideService, + contexthandler.ProvideService, + jwt.ProvideService, + wire.Bind(new(models.JWTService), new(*jwt.AuthService)), + ngalert.ProvideService, + librarypanels.ProvideService, + wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)), + libraryelements.ProvideService, + wire.Bind(new(libraryelements.Service), new(*libraryelements.LibraryElementService)), + notifications.ProvideService, + notifications.ProvideSmtpService, + metrics.ProvideService, + testdatasource.ProvideService, + social.ProvideService, + influxdb.ProvideService, + wire.Bind(new(social.Service), new(*social.SocialService)), + oauthtoken.ProvideService, + auth.ProvideActiveAuthTokenService, + wire.Bind(new(models.ActiveTokenService), new(*auth.ActiveAuthTokenService)), + wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)), + tempo.ProvideService, + loki.ProvideService, + graphite.ProvideService, + prometheus.ProvideService, + elasticsearch.ProvideService, secretsMigrator.ProvideSecretsMigrator, wire.Bind(new(secrets.Migrator), new(*secretsMigrator.SecretsMigrator)), - hooks.ProvideService, + grafanads.ProvideService, + wire.Bind(new(dashboardsnapshots.Store), new(*dashsnapstore.DashboardSnapshotStore)), + dashsnapstore.ProvideStore, + wire.Bind(new(dashboardsnapshots.Service), new(*dashsnapsvc.ServiceImpl)), + dashsnapsvc.ProvideService, + datasourceservice.ProvideService, + wire.Bind(new(datasources.DataSourceService), new(*datasourceservice.Service)), + pluginSettings.ProvideService, + wire.Bind(new(pluginsettings.Service), new(*pluginSettings.Service)), + alerting.ProvideService, + database.ProvideServiceAccountsStore, + wire.Bind(new(serviceaccounts.Store), new(*database.ServiceAccountsStoreImpl)), + ossaccesscontrol.ProvideServiceAccountPermissions, + wire.Bind(new(accesscontrol.ServiceAccountPermissionsService), new(*ossaccesscontrol.ServiceAccountPermissionsService)), + serviceaccountsmanager.ProvideServiceAccountsService, + wire.Bind(new(serviceaccounts.Service), new(*serviceaccountsmanager.ServiceAccountsService)), + expr.ProvideService, + teamguardianDatabase.ProvideTeamGuardianStore, + wire.Bind(new(teamguardian.Store), new(*teamguardianDatabase.TeamGuardianStoreImpl)), + teamguardianManager.ProvideService, + dashboardservice.ProvideDashboardService, + dashboardservice.ProvideFolderService, + dashboardstore.ProvideDashboardStore, + wire.Bind(new(dashboards.DashboardService), new(*dashboardservice.DashboardServiceImpl)), + wire.Bind(new(dashboards.DashboardProvisioningService), new(*dashboardservice.DashboardServiceImpl)), + wire.Bind(new(dashboards.PluginService), new(*dashboardservice.DashboardServiceImpl)), + wire.Bind(new(dashboards.FolderService), new(*dashboardservice.FolderServiceImpl)), + wire.Bind(new(dashboards.Store), new(*dashboardstore.DashboardStore)), + dashboardimportservice.ProvideService, + wire.Bind(new(dashboardimport.Service), new(*dashboardimportservice.ImportDashboardService)), + plugindashboardsservice.ProvideService, + wire.Bind(new(plugindashboards.Service), new(*plugindashboardsservice.Service)), + plugindashboardsservice.ProvideDashboardUpdater, + alerting.ProvideDashAlertExtractorService, + wire.Bind(new(alerting.DashAlertExtractor), new(*alerting.DashAlertExtractorService)), + comments.ProvideService, + guardian.ProvideService, + sanitizer.ProvideService, + secretsStore.ProvideService, + avatar.ProvideAvatarCacheServer, + authproxy.ProvideAuthProxy, + statscollector.ProvideService, + cmreg.CoremodelSet, + cuectx.ProvideCUEContext, + cuectx.ProvideThemaLibrary, + csrf.ProvideCSRFFilter, + ossaccesscontrol.ProvideTeamPermissions, + wire.Bind(new(accesscontrol.TeamPermissionsService), new(*ossaccesscontrol.TeamPermissionsService)), + ossaccesscontrol.ProvideFolderPermissions, + wire.Bind(new(accesscontrol.FolderPermissionsService), new(*ossaccesscontrol.FolderPermissionsService)), + ossaccesscontrol.ProvideDashboardPermissions, + wire.Bind(new(accesscontrol.DashboardPermissionsService), new(*ossaccesscontrol.DashboardPermissionsService)), + starimpl.ProvideService, + playlistimpl.ProvideService, + dashverimpl.ProvideService, + publicdashboardsService.ProvideService, + wire.Bind(new(publicdashboards.Service), new(*publicdashboardsService.PublicDashboardServiceImpl)), + publicdashboardsStore.ProvideStore, + wire.Bind(new(publicdashboards.Store), new(*publicdashboardsStore.PublicDashboardStoreImpl)), + publicdashboardsApi.ProvideApi, + userimpl.ProvideService, + orgimpl.ProvideService, + datasourceservice.ProvideDataSourceMigrationService, + secretsStore.ProvidePluginSecretMigrationService, + secretsMigrations.ProvideSecretMigrationService, + wire.Bind(new(secretsMigrations.SecretMigrationService), new(*secretsMigrations.SecretMigrationServiceImpl)), + userauthimpl.ProvideService, + ngmetrics.ProvideServiceForTest, + wire.Bind(new(alerting.AlertStore), new(*sqlstore.SQLStore)), + wire.Bind(new(sqlstore.TeamStore), new(*sqlstore.SQLStore)), + notifications.MockNotificationService, + wire.Bind(new(notifications.TempUserStore), new(*mockstore.SQLStoreMock)), + wire.Bind(new(notifications.Service), new(*notifications.NotificationServiceMock)), + wire.Bind(new(notifications.WebhookSender), new(*notifications.NotificationServiceMock)), + wire.Bind(new(notifications.EmailSender), new(*notifications.NotificationServiceMock)), + mockstore.NewSQLStoreMock, + wire.Bind(new(sqlstore.Store), new(*sqlstore.SQLStore)), + wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), + prefimpl.ProvideService, ) func Initialize(cfg *setting.Cfg) (Runner, error) { diff --git a/pkg/cmd/grafana-cli/runner/wireexts_oss.go b/pkg/cmd/grafana-cli/runner/wireexts_oss.go index 2941d1c4de8..2373203a3c1 100644 --- a/pkg/cmd/grafana-cli/runner/wireexts_oss.go +++ b/pkg/cmd/grafana-cli/runner/wireexts_oss.go @@ -5,14 +5,37 @@ package runner import ( "github.com/google/wire" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/backendplugin/provider" + "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/server/backgroundsvcs" + "github.com/grafana/grafana/pkg/server/usagestatssvcs" + "github.com/grafana/grafana/pkg/services/accesscontrol" + acdb "github.com/grafana/grafana/pkg/services/accesscontrol/database" + "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" + "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/datasources/permissions" + datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service" "github.com/grafana/grafana/pkg/services/encryption" encryptionprovider "github.com/grafana/grafana/pkg/services/encryption/provider" "github.com/grafana/grafana/pkg/services/kmsproviders" "github.com/grafana/grafana/pkg/services/kmsproviders/osskmsproviders" + "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/licensing" + "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/login/authinfoservice" + "github.com/grafana/grafana/pkg/services/provisioning" + "github.com/grafana/grafana/pkg/services/searchusers" + "github.com/grafana/grafana/pkg/services/searchusers/filters" + secretsStore "github.com/grafana/grafana/pkg/services/secrets/kvstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrations" + "github.com/grafana/grafana/pkg/services/thumbs" + "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/setting" ) @@ -26,6 +49,47 @@ var wireExtsSet = wire.NewSet( wire.Bind(new(setting.Provider), new(*setting.OSSImpl)), osskmsproviders.ProvideService, wire.Bind(new(kmsproviders.Service), new(osskmsproviders.Service)), + // ossencryption.ProvideService, + // wire.Bind(new(encryption.Internal), new(*ossencryption.Service)), + auth.ProvideUserAuthTokenService, + wire.Bind(new(models.UserTokenService), new(*auth.UserAuthTokenService)), + wire.Bind(new(models.UserTokenBackgroundService), new(*auth.UserAuthTokenService)), + ossaccesscontrol.ProvideService, + wire.Bind(new(accesscontrol.RoleRegistry), new(*ossaccesscontrol.OSSAccessControlService)), + wire.Bind(new(accesscontrol.AccessControl), new(*ossaccesscontrol.OSSAccessControlService)), + thumbs.ProvideCrawlerAuthSetupService, + wire.Bind(new(thumbs.CrawlerAuthSetupService), new(*thumbs.OSSCrawlerAuthSetupService)), + validations.ProvideValidator, + wire.Bind(new(models.PluginRequestValidator), new(*validations.OSSPluginRequestValidator)), + provisioning.ProvideService, + wire.Bind(new(provisioning.ProvisioningService), new(*provisioning.ProvisioningServiceImpl)), + backgroundsvcs.ProvideBackgroundServiceRegistry, + wire.Bind(new(registry.BackgroundServiceRegistry), new(*backgroundsvcs.BackgroundServiceRegistry)), + datasourceservice.ProvideCacheService, + wire.Bind(new(datasources.CacheService), new(*datasourceservice.CacheServiceImpl)), + authinfoservice.ProvideOSSUserProtectionService, + wire.Bind(new(login.UserProtectionService), new(*authinfoservice.OSSUserProtectionImpl)), + filters.ProvideOSSSearchUserFilter, + wire.Bind(new(models.SearchUserFilter), new(*filters.OSSSearchUserFilter)), + searchusers.ProvideUsersService, + wire.Bind(new(searchusers.Service), new(*searchusers.OSSService)), + signature.ProvideOSSAuthorizer, + wire.Bind(new(plugins.PluginLoaderAuthorizer), new(*signature.UnsignedPluginAuthorizer)), + provider.ProvideService, + wire.Bind(new(plugins.BackendFactoryProvider), new(*provider.Service)), + acdb.ProvideService, + wire.Bind(new(resourcepermissions.Store), new(*acdb.AccessControlStore)), + wire.Bind(new(accesscontrol.PermissionsStore), new(*acdb.AccessControlStore)), + ldap.ProvideGroupsService, + wire.Bind(new(ldap.Groups), new(*ldap.OSSGroups)), + permissions.ProvideDatasourcePermissionsService, + wire.Bind(new(permissions.DatasourcePermissionsService), new(*permissions.OSSDatasourcePermissionsService)), + usagestatssvcs.ProvideUsageStatsProvidersRegistry, + wire.Bind(new(registry.UsageStatsProvidersRegistry), new(*usagestatssvcs.UsageStatsProvidersRegistry)), + ossaccesscontrol.ProvideDatasourcePermissionsService, + wire.Bind(new(accesscontrol.DatasourcePermissionsService), new(*ossaccesscontrol.DatasourcePermissionsService)), + secretsStore.ProvideRemotePluginCheck, + wire.Bind(new(secretsStore.UseRemoteSecretsPluginCheck), new(*secretsStore.OSSRemoteSecretsPluginCheck)), encryptionprovider.ProvideEncryptionProvider, wire.Bind(new(encryption.Provider), new(encryptionprovider.Provider)), ) diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 467f8d2b8b7..61daad69d66 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -197,7 +197,6 @@ var wireBasicSet = wire.NewSet( authinfoservice.ProvideAuthInfoService, wire.Bind(new(login.AuthInfoService), new(*authinfoservice.Implementation)), authinfodatabase.ProvideAuthInfoStore, - wire.Bind(new(login.Store), new(*authinfodatabase.AuthInfoStore)), loginpkg.ProvideService, wire.Bind(new(loginpkg.Authenticator), new(*loginpkg.AuthenticatorService)), datasourceproxy.ProvideService, diff --git a/pkg/services/login/authinfoservice/database/database.go b/pkg/services/login/authinfoservice/database/database.go index af8f099ecf9..1de2bc17230 100644 --- a/pkg/services/login/authinfoservice/database/database.go +++ b/pkg/services/login/authinfoservice/database/database.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" @@ -18,13 +19,15 @@ type AuthInfoStore struct { sqlStore sqlstore.Store secretsService secrets.Service logger log.Logger + userService user.Service } -func ProvideAuthInfoStore(sqlStore sqlstore.Store, secretsService secrets.Service) *AuthInfoStore { +func ProvideAuthInfoStore(sqlStore sqlstore.Store, secretsService secrets.Service, userService user.Service) login.Store { store := &AuthInfoStore{ sqlStore: sqlStore, secretsService: secretsService, logger: log.New("login.authinfo.store"), + userService: userService, } InitMetrics() return store @@ -221,12 +224,13 @@ func (s *AuthInfoStore) DeleteAuthInfo(ctx context.Context, cmd *models.DeleteAu } func (s *AuthInfoStore) GetUserById(ctx context.Context, id int64) (*user.User, error) { - query := models.GetUserByIdQuery{Id: id} - if err := s.sqlStore.GetUserById(ctx, &query); err != nil { + query := user.GetUserByIDQuery{ID: id} + user, err := s.userService.GetByID(ctx, &query) + if err != nil { return nil, err } - return query.Result, nil + return user, nil } func (s *AuthInfoStore) GetUserByLogin(ctx context.Context, login string) (*user.User, error) { diff --git a/pkg/services/login/authinfoservice/database/stats.go b/pkg/services/login/authinfoservice/database/stats.go index c34da9b34da..14ce0f1fea4 100644 --- a/pkg/services/login/authinfoservice/database/stats.go +++ b/pkg/services/login/authinfoservice/database/stats.go @@ -2,62 +2,38 @@ package database import ( "context" - "sync" "time" + "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/db" "github.com/prometheus/client_golang/prometheus" ) -type LoginStats struct { - DuplicateUserEntries int `xorm:"duplicate_user_entries"` - MixedCasedUsers int `xorm:"mixed_cased_users"` -} - -const ( - ExporterName = "grafana" - metricsCollectionInterval = time.Second * 60 * 4 // every 4 hours, indication of duplicate users -) - -var ( - // MStatDuplicateUserEntries is a indication metric gauge for number of users with duplicate emails or logins - MStatDuplicateUserEntries prometheus.Gauge - - // MStatHasDuplicateEntries is a metric for if there is duplicate users - MStatHasDuplicateEntries prometheus.Gauge - - // MStatMixedCasedUsers is a metric for if there is duplicate users - MStatMixedCasedUsers prometheus.Gauge - - once sync.Once - Initialised bool = false -) - func InitMetrics() { - once.Do(func() { - MStatDuplicateUserEntries = prometheus.NewGauge(prometheus.GaugeOpts{ + login.Once.Do(func() { + login.MStatDuplicateUserEntries = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "stat_users_total_duplicate_user_entries", Help: "total number of duplicate user entries by email or login", - Namespace: ExporterName, + Namespace: login.ExporterName, }) - MStatHasDuplicateEntries = prometheus.NewGauge(prometheus.GaugeOpts{ + login.MStatHasDuplicateEntries = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "stat_users_has_duplicate_user_entries", Help: "instance has duplicate user entries by email or login", - Namespace: ExporterName, + Namespace: login.ExporterName, }) - MStatMixedCasedUsers = prometheus.NewGauge(prometheus.GaugeOpts{ + login.MStatMixedCasedUsers = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "stat_users_total_mixed_cased_users", Help: "total number of users with upper and lower case logins or emails", - Namespace: ExporterName, + Namespace: login.ExporterName, }) prometheus.MustRegister( - MStatDuplicateUserEntries, - MStatHasDuplicateEntries, - MStatMixedCasedUsers, + login.MStatDuplicateUserEntries, + login.MStatHasDuplicateEntries, + login.MStatMixedCasedUsers, ) }) } @@ -66,7 +42,7 @@ func (s *AuthInfoStore) RunMetricsCollection(ctx context.Context) error { if _, err := s.GetLoginStats(ctx); err != nil { s.logger.Warn("Failed to get authinfo metrics", "error", err.Error()) } - updateStatsTicker := time.NewTicker(metricsCollectionInterval) + updateStatsTicker := time.NewTicker(login.MetricsCollectionInterval) defer updateStatsTicker.Stop() for { @@ -81,8 +57,8 @@ func (s *AuthInfoStore) RunMetricsCollection(ctx context.Context) error { } } -func (s *AuthInfoStore) GetLoginStats(ctx context.Context) (LoginStats, error) { - var stats LoginStats +func (s *AuthInfoStore) GetLoginStats(ctx context.Context) (login.LoginStats, error) { + var stats login.LoginStats outerErr := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { rawSQL := `SELECT (SELECT COUNT(*) FROM (` + s.duplicateUserEntriesSQL(ctx) + `) AS d WHERE (d.dup_login IS NOT NULL OR d.dup_email IS NOT NULL)) as duplicate_user_entries, @@ -96,14 +72,14 @@ func (s *AuthInfoStore) GetLoginStats(ctx context.Context) (LoginStats, error) { } // set prometheus metrics stats - MStatDuplicateUserEntries.Set(float64(stats.DuplicateUserEntries)) + login.MStatDuplicateUserEntries.Set(float64(stats.DuplicateUserEntries)) if stats.DuplicateUserEntries == 0 { - MStatHasDuplicateEntries.Set(float64(0)) + login.MStatHasDuplicateEntries.Set(float64(0)) } else { - MStatHasDuplicateEntries.Set(float64(1)) + login.MStatHasDuplicateEntries.Set(float64(1)) } - MStatMixedCasedUsers.Set(float64(stats.MixedCasedUsers)) + login.MStatMixedCasedUsers.Set(float64(stats.MixedCasedUsers)) return stats, nil } @@ -115,14 +91,12 @@ func (s *AuthInfoStore) CollectLoginStats(ctx context.Context) (map[string]inter s.logger.Error("Failed to get login stats", "error", err) return nil, err } - m["stats.users.duplicate_user_entries"] = loginStats.DuplicateUserEntries if loginStats.DuplicateUserEntries > 0 { m["stats.users.has_duplicate_user_entries"] = 1 } else { m["stats.users.has_duplicate_user_entries"] = 0 } - m["stats.users.mixed_cased_users"] = loginStats.MixedCasedUsers return m, nil diff --git a/pkg/services/login/authinfoservice/user_auth_test.go b/pkg/services/login/authinfoservice/user_auth_test.go index 668ce91a5ce..79b1536c13c 100644 --- a/pkg/services/login/authinfoservice/user_auth_test.go +++ b/pkg/services/login/authinfoservice/user_auth_test.go @@ -2,26 +2,26 @@ package authinfoservice import ( "context" + "errors" "fmt" "testing" "time" - "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/login/authinfoservice/database" - secretstore "github.com/grafana/grafana/pkg/services/secrets/database" - secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/require" "golang.org/x/oauth2" + + "github.com/grafana/grafana/pkg/infra/usagestats" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/login/authinfoservice/database" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/user" ) //nolint:goconst func TestUserAuth(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) - secretsService := secretsManager.SetupTestService(t, secretstore.ProvideSecretsStore(sqlStore)) - authInfoStore := database.ProvideAuthInfoStore(sqlStore, secretsService) + authInfoStore := newFakeAuthInfoStore() srv := ProvideAuthInfoService( &OSSUserProtectionImpl{}, authInfoStore, @@ -42,7 +42,11 @@ func TestUserAuth(t *testing.T) { t.Run("Can find existing user", func(t *testing.T) { // By Login login := "loginuser0" - + authInfoStore.ExpectedUser = &user.User{ + Login: "loginuser0", + ID: 1, + Email: "user1@test.com", + } query := &models.GetUserByAuthInfoQuery{UserLookupParams: models.UserLookupParams{Login: &login}} usr, err := srv.LookupAndUpdate(context.Background(), query) @@ -69,6 +73,7 @@ func TestUserAuth(t *testing.T) { require.Nil(t, err) require.Equal(t, usr.Email, email) + authInfoStore.ExpectedUser = nil // Don't find nonexistent user email = "nonexistent@test.com" @@ -82,6 +87,8 @@ func TestUserAuth(t *testing.T) { t.Run("Can set & locate by AuthModule and AuthId", func(t *testing.T) { // get nonexistent user_auth entry + authInfoStore.ExpectedUser = &user.User{} + authInfoStore.ExpectedError = user.ErrUserNotFound query := &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} usr, err := srv.LookupAndUpdate(context.Background(), query) @@ -90,7 +97,9 @@ func TestUserAuth(t *testing.T) { // create user_auth entry login := "loginuser0" - + authInfoStore.ExpectedUser = &user.User{Login: "loginuser0", ID: 1, Email: ""} + authInfoStore.ExpectedError = nil + authInfoStore.ExpectedOAuth = &models.UserAuth{Id: 1} query.UserLookupParams.Login = &login usr, err = srv.LookupAndUpdate(context.Background(), query) @@ -107,6 +116,7 @@ func TestUserAuth(t *testing.T) { // get with non-matching id idPlusOne := usr.ID + 1 + authInfoStore.ExpectedUser.Login = "loginuser1" query.UserLookupParams.UserID = &idPlusOne usr, err = srv.LookupAndUpdate(context.Background(), query) @@ -127,6 +137,8 @@ func TestUserAuth(t *testing.T) { }) require.NoError(t, err) + authInfoStore.ExpectedUser = nil + authInfoStore.ExpectedError = user.ErrUserNotFound // get via user_auth for deleted user query = &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} usr, err = srv.LookupAndUpdate(context.Background(), query) @@ -147,7 +159,16 @@ func TestUserAuth(t *testing.T) { // Find a user to set tokens on login := "loginuser0" - + authInfoStore.ExpectedUser = &user.User{Login: "loginuser0", ID: 1, Email: ""} + authInfoStore.ExpectedError = nil + authInfoStore.ExpectedOAuth = &models.UserAuth{ + Id: 1, + OAuthAccessToken: token.AccessToken, + OAuthRefreshToken: token.RefreshToken, + OAuthTokenType: token.TokenType, + OAuthIdToken: idToken, + OAuthExpiry: token.Expiry, + } // Calling GetUserByAuthInfoQuery on an existing user will populate an entry in the user_auth table query := &models.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test", UserLookupParams: models.UserLookupParams{ Login: &login, @@ -220,7 +241,7 @@ func TestUserAuth(t *testing.T) { require.Nil(t, err) require.Equal(t, user.Login, login) - + authInfoStore.ExpectedOAuth.AuthModule = "test2" // Get the latest entry by not supply an authmodule or authid getAuthQuery := &models.GetAuthInfoQuery{ UserId: user.ID, @@ -236,7 +257,7 @@ func TestUserAuth(t *testing.T) { err = authInfoStore.UpdateAuthInfo(context.Background(), updateAuthCmd) require.Nil(t, err) - + authInfoStore.ExpectedOAuth.AuthModule = "test1" // Get the latest entry by not supply an authmodule or authid getAuthQuery = &models.GetAuthInfoQuery{ UserId: user.ID, @@ -292,6 +313,7 @@ func TestUserAuth(t *testing.T) { getAuthQuery := &models.GetAuthInfoQuery{ UserId: user.ID, } + authInfoStore.ExpectedOAuth.AuthModule = "test2" err = authInfoStore.GetAuthInfo(context.Background(), getAuthQuery) @@ -314,7 +336,8 @@ func TestUserAuth(t *testing.T) { require.Nil(t, err) require.Equal(t, user.Login, login) - + authInfoStore.ExpectedOAuth.AuthModule = "test1" + authInfoStore.ExpectedOAuth.OAuthAccessToken = "access_token" err = authInfoStore.GetAuthInfo(context.Background(), getAuthQuery) require.Nil(t, err) @@ -327,6 +350,7 @@ func TestUserAuth(t *testing.T) { user, err = srv.LookupAndUpdate(context.Background(), queryTwo) require.Nil(t, err) require.Equal(t, user.Login, login) + authInfoStore.ExpectedOAuth.AuthModule = "test2" err = authInfoStore.GetAuthInfo(context.Background(), getAuthQuery) require.Nil(t, err) @@ -337,10 +361,11 @@ func TestUserAuth(t *testing.T) { UserId: user.ID, AuthModule: "test1", } + authInfoStore.ExpectedOAuth.AuthModule = "test1" + err = authInfoStore.GetAuthInfo(context.Background(), getAuthQueryUnchanged) require.Nil(t, err) require.Equal(t, "test1", getAuthQueryUnchanged.Result.AuthModule) - require.Less(t, getAuthQueryUnchanged.Result.Created, getAuthQuery.Result.Created) }) t.Run("Can set & locate by generic oauth auth module and user id", func(t *testing.T) { @@ -364,11 +389,14 @@ func TestUserAuth(t *testing.T) { query = &models.GetUserByAuthInfoQuery{AuthModule: genericOAuthModule, AuthId: "", UserLookupParams: models.UserLookupParams{ Login: &otherLoginUser, }} + authInfoStore.ExpectedError = errors.New("some error") + user, err = srv.LookupAndUpdate(context.Background(), query) database.GetTime = time.Now require.NotNil(t, err) require.Nil(t, user) + authInfoStore.ExpectedError = nil }) t.Run("should be able to run loginstats query in all dbs", func(t *testing.T) { @@ -426,8 +454,18 @@ func TestUserAuth(t *testing.T) { } _, err = sqlStore.CreateUser(context.Background(), dupUserLogincmd) require.NoError(t, err) - - // require stats to populate + authInfoStore.ExpectedUser = &user.User{ + Email: "userduplicatetest1@test.com", + Name: "user name 1", + Login: "user_duplicate_test_1_login", + } + authInfoStore.ExpectedDuplicateUserEntries = 2 + authInfoStore.ExpectedHasDuplicateUserEntries = 1 + authInfoStore.ExpectedLoginStats = login.LoginStats{ + DuplicateUserEntries: 2, + MixedCasedUsers: 1, + } + // require metrics and statistics to be 2 m, err := srv.authInfoStore.CollectLoginStats(context.Background()) require.NoError(t, err) require.Equal(t, 2, m["stats.users.duplicate_user_entries"]) @@ -438,3 +476,67 @@ func TestUserAuth(t *testing.T) { }) }) } + +type FakeAuthInfoStore struct { + ExpectedError error + ExpectedUser *user.User + ExpectedOAuth *models.UserAuth + ExpectedDuplicateUserEntries int + ExpectedHasDuplicateUserEntries int + ExpectedLoginStats login.LoginStats +} + +func newFakeAuthInfoStore() *FakeAuthInfoStore { + return &FakeAuthInfoStore{} +} + +func (f *FakeAuthInfoStore) GetExternalUserInfoByLogin(ctx context.Context, query *models.GetExternalUserInfoByLoginQuery) error { + return f.ExpectedError +} +func (f *FakeAuthInfoStore) GetAuthInfo(ctx context.Context, query *models.GetAuthInfoQuery) error { + query.Result = f.ExpectedOAuth + return f.ExpectedError +} +func (f *FakeAuthInfoStore) SetAuthInfo(ctx context.Context, cmd *models.SetAuthInfoCommand) error { + return f.ExpectedError +} +func (f *FakeAuthInfoStore) UpdateAuthInfoDate(ctx context.Context, authInfo *models.UserAuth) error { + return f.ExpectedError +} +func (f *FakeAuthInfoStore) UpdateAuthInfo(ctx context.Context, cmd *models.UpdateAuthInfoCommand) error { + return f.ExpectedError +} +func (f *FakeAuthInfoStore) DeleteAuthInfo(ctx context.Context, cmd *models.DeleteAuthInfoCommand) error { + return f.ExpectedError +} +func (f *FakeAuthInfoStore) GetUserById(ctx context.Context, id int64) (*user.User, error) { + return f.ExpectedUser, f.ExpectedError +} + +func (f *FakeAuthInfoStore) GetUserByLogin(ctx context.Context, login string) (*user.User, error) { + return f.ExpectedUser, f.ExpectedError +} + +func (f *FakeAuthInfoStore) GetUserByEmail(ctx context.Context, email string) (*user.User, error) { + return f.ExpectedUser, f.ExpectedError +} + +func (f *FakeAuthInfoStore) CollectLoginStats(ctx context.Context) (map[string]interface{}, error) { + var res = make(map[string]interface{}) + res["stats.users.duplicate_user_entries"] = f.ExpectedDuplicateUserEntries + res["stats.users.has_duplicate_user_entries"] = f.ExpectedHasDuplicateUserEntries + res["stats.users.duplicate_user_entries_by_login"] = 0 + res["stats.users.has_duplicate_user_entries_by_login"] = 0 + res["stats.users.duplicate_user_entries_by_email"] = 0 + res["stats.users.has_duplicate_user_entries_by_email"] = 0 + res["stats.users.mixed_cased_users"] = f.ExpectedLoginStats.MixedCasedUsers + return res, f.ExpectedError +} + +func (f *FakeAuthInfoStore) RunMetricsCollection(ctx context.Context) error { + return f.ExpectedError +} + +func (f *FakeAuthInfoStore) GetLoginStats(ctx context.Context) (login.LoginStats, error) { + return f.ExpectedLoginStats, f.ExpectedError +} diff --git a/pkg/services/login/model.go b/pkg/services/login/model.go new file mode 100644 index 00000000000..b310eeb51a5 --- /dev/null +++ b/pkg/services/login/model.go @@ -0,0 +1,32 @@ +package login + +import ( + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +type LoginStats struct { + DuplicateUserEntries int `xorm:"duplicate_user_entries"` + MixedCasedUsers int `xorm:"mixed_cased_users"` +} + +const ( + ExporterName = "grafana" + MetricsCollectionInterval = time.Second * 60 * 4 // every 4 hours, indication of duplicate users +) + +var ( + // MStatDuplicateUserEntries is a indication metric gauge for number of users with duplicate emails or logins + MStatDuplicateUserEntries prometheus.Gauge + + // MStatHasDuplicateEntries is a metric for if there is duplicate users + MStatHasDuplicateEntries prometheus.Gauge + + // MStatMixedCasedUsers is a metric for if there is duplicate users + MStatMixedCasedUsers prometheus.Gauge + + Once sync.Once + Initialised bool = false +) diff --git a/pkg/services/login/userprotection.go b/pkg/services/login/userprotection.go index 725343eb3f0..83146ce2bdf 100644 --- a/pkg/services/login/userprotection.go +++ b/pkg/services/login/userprotection.go @@ -4,7 +4,6 @@ import ( "context" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/login/authinfoservice/database" "github.com/grafana/grafana/pkg/services/user" ) @@ -24,5 +23,5 @@ type Store interface { GetUserByEmail(ctx context.Context, email string) (*user.User, error) CollectLoginStats(ctx context.Context) (map[string]interface{}, error) RunMetricsCollection(ctx context.Context) error - GetLoginStats(ctx context.Context) (database.LoginStats, error) + GetLoginStats(ctx context.Context) (LoginStats, error) } diff --git a/pkg/services/user/model.go b/pkg/services/user/model.go index d44c0fdc3e8..878a1f60080 100644 --- a/pkg/services/user/model.go +++ b/pkg/services/user/model.go @@ -2,6 +2,8 @@ package user import ( "errors" + "fmt" + "strings" "time" ) @@ -69,3 +71,28 @@ func (u *User) NameOrFallback() string { type DeleteUserCommand struct { UserID int64 } + +type GetUserByIDQuery struct { + ID int64 +} + +type ErrCaseInsensitiveLoginConflict struct { + Users []User +} + +func (e *ErrCaseInsensitiveLoginConflict) Unwrap() error { + return ErrCaseInsensitive +} + +func (e *ErrCaseInsensitiveLoginConflict) Error() string { + n := len(e.Users) + + userStrings := make([]string, 0, n) + for _, v := range e.Users { + userStrings = append(userStrings, fmt.Sprintf("%s (email:%s, id:%d)", v.Login, v.Email, v.ID)) + } + + return fmt.Sprintf( + "Found a conflict in user login information. %d users already exist with either the same login or email: [%s].", + n, strings.Join(userStrings, ", ")) +} diff --git a/pkg/services/user/user.go b/pkg/services/user/user.go index d720d4f5c10..bf39334c9bc 100644 --- a/pkg/services/user/user.go +++ b/pkg/services/user/user.go @@ -7,4 +7,5 @@ import ( type Service interface { Create(context.Context, *CreateUserCommand) (*User, error) Delete(context.Context, *DeleteUserCommand) error + GetByID(context.Context, *GetUserByIDQuery) (*User, error) } diff --git a/pkg/services/user/userimpl/store.go b/pkg/services/user/userimpl/store.go index 3f19fa3b866..2ccce72a595 100644 --- a/pkg/services/user/userimpl/store.go +++ b/pkg/services/user/userimpl/store.go @@ -14,8 +14,10 @@ import ( type store interface { Insert(context.Context, *user.User) (int64, error) Get(context.Context, *user.User) (*user.User, error) + GetByID(context.Context, int64) (*user.User, error) GetNotServiceAccount(context.Context, int64) (*user.User, error) Delete(context.Context, int64) error + CaseInsensitiveLoginConflict(context.Context, string, string) error } type sqlStore struct { @@ -91,8 +93,42 @@ func (ss *sqlStore) GetNotServiceAccount(ctx context.Context, userID int64) (*us return &usr, err } +func (ss *sqlStore) GetByID(ctx context.Context, userID int64) (*user.User, error) { + var usr user.User + + err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + has, err := sess.ID(&userID). + Where(ss.notServiceAccountFilter()). + Get(&usr) + + if err != nil { + return err + } else if !has { + return user.ErrUserNotFound + } + return nil + }) + return &usr, err +} + func (ss *sqlStore) notServiceAccountFilter() string { return fmt.Sprintf("%s.is_service_account = %s", ss.dialect.Quote("user"), ss.dialect.BooleanStr(false)) } + +func (ss *sqlStore) CaseInsensitiveLoginConflict(ctx context.Context, login, email string) error { + users := make([]user.User, 0) + err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + if err := sess.Where("LOWER(email)=LOWER(?) OR LOWER(login)=LOWER(?)", + email, login).Find(&users); err != nil { + return err + } + + if len(users) > 1 { + return &user.ErrCaseInsensitiveLoginConflict{Users: users} + } + return nil + }) + return err +} diff --git a/pkg/services/user/userimpl/user.go b/pkg/services/user/userimpl/user.go index c5c1223e88e..4164fb33b86 100644 --- a/pkg/services/user/userimpl/user.go +++ b/pkg/services/user/userimpl/user.go @@ -3,7 +3,6 @@ package userimpl import ( "context" "errors" - "fmt" "time" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -32,6 +31,8 @@ type Service struct { userAuthService userauth.Service quotaService quota.Service accessControlStore accesscontrol.AccessControl + + cfg *setting.Cfg } func ProvideService( @@ -44,6 +45,7 @@ func ProvideService( userAuthService userauth.Service, quotaService quota.Service, accessControlStore accesscontrol.AccessControl, + cfg *setting.Cfg, ) user.Service { return &Service{ store: &sqlStore{ @@ -58,6 +60,7 @@ func ProvideService( userAuthService: userAuthService, quotaService: quotaService, accessControlStore: accessControlStore, + cfg: cfg, } } @@ -157,7 +160,7 @@ func (s *Service) Create(ctx context.Context, cmd *user.CreateUserCommand) (*use func (s *Service) Delete(ctx context.Context, cmd *user.DeleteUserCommand) error { _, err := s.store.GetNotServiceAccount(ctx, cmd.UserID) if err != nil { - return fmt.Errorf("failed to get user with not service account: %w", err) + return err } // delete from all the stores if err := s.store.Delete(ctx, cmd.UserID); err != nil { @@ -225,3 +228,16 @@ func (s *Service) Delete(ctx context.Context, cmd *user.DeleteUserCommand) error return nil } + +func (s *Service) GetByID(ctx context.Context, query *user.GetUserByIDQuery) (*user.User, error) { + user, err := s.store.GetByID(ctx, query.ID) + if err != nil { + return nil, err + } + if s.cfg.CaseInsensitiveLogin { + if err := s.store.CaseInsensitiveLoginConflict(ctx, user.Login, user.Email); err != nil { + return nil, err + } + } + return user, nil +} diff --git a/pkg/services/user/userimpl/user_test.go b/pkg/services/user/userimpl/user_test.go index 5cb6c164290..7cb904b4e98 100644 --- a/pkg/services/user/userimpl/user_test.go +++ b/pkg/services/user/userimpl/user_test.go @@ -14,6 +14,8 @@ import ( "github.com/grafana/grafana/pkg/services/teamguardian/manager" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/userauth/userauthtest" + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/require" ) @@ -40,7 +42,67 @@ func TestUserService(t *testing.T) { } t.Run("create user", func(t *testing.T) { - _, err := userService.Create(context.Background(), &user.CreateUserCommand{}) + _, err := userService.Create(context.Background(), &user.CreateUserCommand{ + Email: "email", + Login: "login", + Name: "name", + }) + require.NoError(t, err) + }) + + t.Run("get user by ID", func(t *testing.T) { + userService.cfg = setting.NewCfg() + userService.cfg.CaseInsensitiveLogin = false + userStore.ExpectedUser = &user.User{ID: 1, Email: "email", Login: "login", Name: "name"} + u, err := userService.GetByID(context.Background(), &user.GetUserByIDQuery{ID: 1}) + require.NoError(t, err) + require.Equal(t, "login", u.Login) + require.Equal(t, "name", u.Name) + require.Equal(t, "email", u.Email) + }) + + t.Run("get user by ID with case insensitive login", func(t *testing.T) { + userService.cfg = setting.NewCfg() + userService.cfg.CaseInsensitiveLogin = true + userStore.ExpectedUser = &user.User{ID: 1, Email: "email", Login: "login", Name: "name"} + u, err := userService.GetByID(context.Background(), &user.GetUserByIDQuery{ID: 1}) + require.NoError(t, err) + require.Equal(t, "login", u.Login) + require.Equal(t, "name", u.Name) + require.Equal(t, "email", u.Email) + }) + + t.Run("delete user store returns error", func(t *testing.T) { + userStore.ExpectedDeleteUserError = user.ErrUserNotFound + t.Cleanup(func() { + userStore.ExpectedDeleteUserError = nil + }) + err := userService.Delete(context.Background(), &user.DeleteUserCommand{UserID: 1}) + require.Error(t, err, user.ErrUserNotFound) + }) + + t.Run("delete user returns from team", func(t *testing.T) { + teamMemberService.ExpectedError = errors.New("some error") + t.Cleanup(func() { + teamMemberService.ExpectedError = nil + }) + err := userService.Delete(context.Background(), &user.DeleteUserCommand{UserID: 1}) + require.Error(t, err) + }) + + t.Run("delete user returns from team and pref", func(t *testing.T) { + teamMemberService.ExpectedError = errors.New("some error") + preferenceService.ExpectedError = errors.New("some error 2") + t.Cleanup(func() { + teamMemberService.ExpectedError = nil + preferenceService.ExpectedError = nil + }) + err := userService.Delete(context.Background(), &user.DeleteUserCommand{UserID: 1}) + require.Error(t, err) + }) + + t.Run("delete user successfully", func(t *testing.T) { + err := userService.Delete(context.Background(), &user.DeleteUserCommand{UserID: 1}) require.NoError(t, err) }) @@ -104,3 +166,11 @@ func (f *FakeUserStore) Delete(ctx context.Context, userID int64) error { func (f *FakeUserStore) GetNotServiceAccount(ctx context.Context, userID int64) (*user.User, error) { return f.ExpectedUser, f.ExpectedError } + +func (f *FakeUserStore) GetByID(context.Context, int64) (*user.User, error) { + return f.ExpectedUser, f.ExpectedError +} + +func (f *FakeUserStore) CaseInsensitiveLoginConflict(context.Context, string, string) error { + return f.ExpectedError +} diff --git a/pkg/services/user/usertest/fake.go b/pkg/services/user/usertest/fake.go index c33090ca968..ebadd271573 100644 --- a/pkg/services/user/usertest/fake.go +++ b/pkg/services/user/usertest/fake.go @@ -22,3 +22,7 @@ func (f *FakeUserService) Create(ctx context.Context, cmd *user.CreateUserComman func (f *FakeUserService) Delete(ctx context.Context, cmd *user.DeleteUserCommand) error { return f.ExpectedError } + +func (f *FakeUserService) GetByID(ctx context.Context, query *user.GetUserByIDQuery) (*user.User, error) { + return f.ExpectedUser, f.ExpectedError +} diff --git a/public/api-merged.json b/public/api-merged.json index 94c97233b03..f7d052cb916 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -1,14 +1,7 @@ { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "http", - "https" - ], + "consumes": ["application/json"], + "produces": ["application/json"], + "schemes": ["http", "https"], "swagger": "2.0", "info": { "description": "The Grafana backend exposes an HTTP API, the same API is used by the frontend to do\neverything from saving dashboards, creating users and updating data sources.", @@ -29,10 +22,7 @@ "/access-control/builtin-roles": { "get": { "description": "You need to have a permission with action `roles.builtin:list` with scope `roles:*`.", - "tags": [ - "access_control", - "enterprise" - ], + "tags": ["access_control", "enterprise"], "summary": "Get all built-in role assignments.", "operationId": "listBuiltinRoles", "responses": { @@ -49,10 +39,7 @@ }, "post": { "description": "You need to have a permission with action `roles.builtin:add` and scope `permissions:type:delegate`. `permissions:type:delegate` scope ensures that users can only create built-in role assignments with the roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to create a built-in role assignment which will allow to do that. This is done to prevent escalation of privileges.", - "tags": [ - "access_control", - "enterprise" - ], + "tags": ["access_control", "enterprise"], "summary": "Create a built-in role assignment.", "operationId": "addBuiltinRole", "parameters": [ @@ -84,10 +71,7 @@ "/access-control/builtin-roles/{builtinRole}/roles/{roleUID}": { "delete": { "description": "Deletes a built-in role assignment (for one of Viewer, Editor, Admin, or Grafana Admin) to the role with the provided UID.\n\nYou need to have a permission with action `roles.builtin:remove` and scope `permissions:type:delegate`. `permissions:type:delegate` scope ensures that users can only remove built-in role assignments with the roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to remove a built-in role assignment which allows to do that.", - "tags": [ - "access_control", - "enterprise" - ], + "tags": ["access_control", "enterprise"], "summary": "Remove a built-in role assignment.", "operationId": "removeBuiltinRole", "parameters": [ @@ -132,10 +116,7 @@ "/access-control/roles": { "get": { "description": "Gets all existing roles. The response contains all global and organization local roles, for the organization which user is signed in.\n\nYou need to have a permission with action `roles:list` and scope `roles:*`.", - "tags": [ - "access_control", - "enterprise" - ], + "tags": ["access_control", "enterprise"], "summary": "Get all roles.", "operationId": "listRoles", "parameters": [ @@ -159,10 +140,7 @@ }, "post": { "description": "Creates a new custom role and maps given permissions to that role. Note that roles with the same prefix as Fixed Roles can’t be created.\n\nYou need to have a permission with action `roles:write` and scope `permissions:type:delegate`. `permissions:type:delegate`` scope ensures that users can only create custom roles with the same, or a subset of permissions which the user has.\nFor example, if a user does not have required permissions for creating users, they won’t be able to create a custom role which allows to do that. This is done to prevent escalation of privileges.", - "tags": [ - "access_control", - "enterprise" - ], + "tags": ["access_control", "enterprise"], "summary": "Create a new custom role.", "operationId": "createRole", "parameters": [ @@ -194,10 +172,7 @@ "/access-control/roles/{roleUID}": { "get": { "description": "Get a role for the given UID.\n\nYou need to have a permission with action `roles:read` and scope `roles:*`.", - "tags": [ - "access_control", - "enterprise" - ], + "tags": ["access_control", "enterprise"], "summary": "Get a role.", "operationId": "getRole", "parameters": [ @@ -222,10 +197,7 @@ }, "put": { "description": "You need to have a permission with action `roles:write` and scope `permissions:type:delegate`. `permissions:type:delegate`` scope ensures that users can only create custom roles with the same, or a subset of permissions which the user has.", - "tags": [ - "access_control", - "enterprise" - ], + "tags": ["access_control", "enterprise"], "summary": "Update a custom role.", "operationId": "updateRoleWithPermissions", "parameters": [ @@ -264,10 +236,7 @@ }, "delete": { "description": "Delete a role with the given UID, and it’s permissions. If the role is assigned to a built-in role, the deletion operation will fail, unless force query param is set to true, and in that case all assignments will also be deleted.\n\nYou need to have a permission with action `roles:delete` and scope `permissions:type:delegate`. `permissions:type:delegate` scope ensures that users can only delete a custom role with the same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to delete a custom role which allows to do that.", - "tags": [ - "access_control", - "enterprise" - ], + "tags": ["access_control", "enterprise"], "summary": "Delete a custom role.", "operationId": "deleteCustomRole", "parameters": [ @@ -307,10 +276,7 @@ "/access-control/status": { "get": { "description": "Returns an indicator to check if fine-grained access control is enabled or not.\n\nYou need to have a permission with action `status:accesscontrol` and scope `services:accesscontrol`.", - "tags": [ - "access_control", - "enterprise" - ], + "tags": ["access_control", "enterprise"], "summary": "Get status.", "operationId": "getAccessControlStatus", "responses": { @@ -332,10 +298,7 @@ "/access-control/teams/{teamId}/roles": { "get": { "description": "You need to have a permission with action `teams.roles:list` and scope `teams:id:\u003cteam ID\u003e`.", - "tags": [ - "access_control", - "enterprise" - ], + "tags": ["access_control", "enterprise"], "summary": "Get team roles.", "operationId": "listTeamRoles", "parameters": [ @@ -364,10 +327,7 @@ }, "put": { "description": "You need to have a permission with action `teams.roles:add` and `teams.roles:remove` and scope `permissions:type:delegate` for each.", - "tags": [ - "access_control", - "enterprise" - ], + "tags": ["access_control", "enterprise"], "summary": "Update team role.", "operationId": "setTeamRoles", "parameters": [ @@ -399,10 +359,7 @@ }, "post": { "description": "You need to have a permission with action `teams.roles:add` and scope `permissions:type:delegate`.", - "tags": [ - "access_control", - "enterprise" - ], + "tags": ["access_control", "enterprise"], "summary": "Add team role.", "operationId": "addTeamRole", "parameters": [ @@ -444,10 +401,7 @@ "/access-control/teams/{teamId}/roles/{roleUID}": { "delete": { "description": "You need to have a permission with action `teams.roles:remove` and scope `permissions:type:delegate`.", - "tags": [ - "access_control", - "enterprise" - ], + "tags": ["access_control", "enterprise"], "summary": "Remove team role.", "operationId": "removeTeamRole", "parameters": [ @@ -487,10 +441,7 @@ "/access-control/users/{userId}/roles": { "get": { "description": "Lists the roles that have been directly assigned to a given user. The list does not include built-in roles (Viewer, Editor, Admin or Grafana Admin), and it does not include roles that have been inherited from a team.\n\nYou need to have a permission with action `users.roles:list` and scope `users:id:\u003cuser ID\u003e`.", - "tags": [ - "access_control", - "enterprise" - ], + "tags": ["access_control", "enterprise"], "summary": "List roles assigned to a user.", "operationId": "listUserRoles", "parameters": [ @@ -519,10 +470,7 @@ }, "put": { "description": "Update the user’s role assignments to match the provided set of UIDs. This will remove any assigned roles that aren’t in the request and add roles that are in the set but are not already assigned to the user.\nIf you want to add or remove a single role, consider using Add a user role assignment or Remove a user role assignment instead.\n\nYou need to have a permission with action `users.roles:add` and `users.roles:remove` and scope `permissions:type:delegate` for each. `permissions:type:delegate` scope ensures that users can only assign or unassign roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to assign or unassign a role which will allow to do that. This is done to prevent escalation of privileges.", - "tags": [ - "access_control", - "enterprise" - ], + "tags": ["access_control", "enterprise"], "summary": "Set user role assignments.", "operationId": "setUserRoles", "parameters": [ @@ -562,10 +510,7 @@ }, "post": { "description": "Assign a role to a specific user. For bulk updates consider Set user role assignments.\n\nYou need to have a permission with action `users.roles:add` and scope `permissions:type:delegate`. `permissions:type:delegate` scope ensures that users can only assign roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to assign a role which will allow to do that. This is done to prevent escalation of privileges.", - "tags": [ - "access_control", - "enterprise" - ], + "tags": ["access_control", "enterprise"], "summary": "Add a user role assignment.", "operationId": "addUserRole", "parameters": [ @@ -604,10 +549,7 @@ "/access-control/users/{userId}/roles/{roleUID}": { "delete": { "description": "Revoke a role from a user. For bulk updates consider Set user role assignments.\n\nYou need to have a permission with action `users.roles:remove` and scope `permissions:type:delegate`. `permissions:type:delegate` scope ensures that users can only unassign roles which have same, or a subset of permissions which the user has. For example, if a user does not have required permissions for creating users, they won’t be able to unassign a role which will allow to do that. This is done to prevent escalation of privileges.", - "tags": [ - "access_control", - "enterprise" - ], + "tags": ["access_control", "enterprise"], "summary": "Remove a user role assignment.", "operationId": "removeUserRole", "parameters": [ @@ -653,9 +595,7 @@ "/admin/ldap-sync-status": { "get": { "description": "You need to have a permission with action `ldap.status:read`.", - "tags": [ - "ldap_debug" - ], + "tags": ["ldap_debug"], "summary": "Returns the current state of the LDAP background sync integration", "operationId": "getSyncStatus", "responses": { @@ -682,9 +622,7 @@ } ], "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `ldap.config:reload`.", - "tags": [ - "admin_ldap" - ], + "tags": ["admin_ldap"], "summary": "Reloads the LDAP configuration.", "operationId": "reloadLDAPCfg", "responses": { @@ -711,9 +649,7 @@ } ], "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `ldap.status:read`.", - "tags": [ - "admin_ldap" - ], + "tags": ["admin_ldap"], "summary": "Attempts to connect to all the configured LDAP servers and returns information on whenever they're available or not.", "operationId": "getLDAPStatus", "responses": { @@ -740,9 +676,7 @@ } ], "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `ldap.user:sync`.", - "tags": [ - "admin_ldap" - ], + "tags": ["admin_ldap"], "summary": "Enables a single Grafana user to be synchronized against LDAP.", "operationId": "postSyncUserWithLDAP", "parameters": [ @@ -778,9 +712,7 @@ } ], "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `ldap.user:read`.", - "tags": [ - "admin_ldap" - ], + "tags": ["admin_ldap"], "summary": "Finds an user based on a username in LDAP. This helps illustrate how would the particular user be mapped in Grafana when synced.", "operationId": "getUserFromLDAP", "parameters": [ @@ -814,9 +746,7 @@ "basic": [] } ], - "tags": [ - "admin" - ], + "tags": ["admin"], "summary": "Pause/unpause all (legacy) alerts.", "operationId": "pauseAllAlerts", "parameters": [ @@ -847,10 +777,7 @@ }, "/admin/provisioning/access-control/reload": { "post": { - "tags": [ - "access_control_provisioning", - "enterprise" - ], + "tags": ["access_control_provisioning", "enterprise"], "summary": "You need to have a permission with action `provisioning:reload` with scope `provisioners:accesscontrol`.", "operationId": "adminProvisioningReloadAccessControl", "responses": { @@ -874,9 +801,7 @@ } ], "description": "Reloads the provisioning config files for dashboards again. It won’t return until the new provisioned entities are already stored in the database. In case of dashboards, it will stop polling for changes in dashboard files and then restart it with new configurations after returning.\nIf you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `provisioning:reload` and scope `provisioners:dashboards`.", - "tags": [ - "admin_provisioning" - ], + "tags": ["admin_provisioning"], "summary": "Reload dashboard provisioning configurations.", "operationId": "adminProvisioningReloadDashboards", "responses": { @@ -903,9 +828,7 @@ } ], "description": "Reloads the provisioning config files for datasources again. It won’t return until the new provisioned entities are already stored in the database. In case of dashboards, it will stop polling for changes in dashboard files and then restart it with new configurations after returning.\nIf you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `provisioning:reload` and scope `provisioners:datasources`.", - "tags": [ - "admin_provisioning" - ], + "tags": ["admin_provisioning"], "summary": "Reload datasource provisioning configurations.", "operationId": "adminProvisioningReloadDatasources", "responses": { @@ -932,9 +855,7 @@ } ], "description": "Reloads the provisioning config files for legacy alert notifiers again. It won’t return until the new provisioned entities are already stored in the database. In case of dashboards, it will stop polling for changes in dashboard files and then restart it with new configurations after returning.\nIf you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `provisioning:reload` and scope `provisioners:notifications`.", - "tags": [ - "admin_provisioning" - ], + "tags": ["admin_provisioning"], "summary": "Reload legacy alert notifier provisioning configurations.", "operationId": "adminProvisioningReloadNotifications", "responses": { @@ -961,9 +882,7 @@ } ], "description": "Reloads the provisioning config files for plugins again. It won’t return until the new provisioned entities are already stored in the database. In case of dashboards, it will stop polling for changes in dashboard files and then restart it with new configurations after returning.\nIf you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `provisioning:reload` and scope `provisioners:plugin`.", - "tags": [ - "admin_provisioning" - ], + "tags": ["admin_provisioning"], "summary": "Reload plugin provisioning configurations.", "operationId": "adminProvisioningReloadPlugins", "responses": { @@ -990,9 +909,7 @@ } ], "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `settings:read` and scopes: `settings:*`, `settings:auth.saml:` and `settings:auth.saml:enabled` (property level).", - "tags": [ - "admin" - ], + "tags": ["admin"], "summary": "Fetch settings.", "operationId": "adminGetSettings", "responses": { @@ -1011,9 +928,7 @@ "/admin/stats": { "get": { "description": "Only works with Basic Authentication (username and password). See introduction for an explanation.\nIf you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `server:stats:read`.", - "tags": [ - "admin" - ], + "tags": ["admin"], "summary": "Fetch Grafana Stats.", "operationId": "adminGetStats", "responses": { @@ -1040,9 +955,7 @@ } ], "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users:create`.\nNote that OrgId is an optional parameter that can be used to assign a new user to a different organization when `auto_assign_org` is set to `true`.", - "tags": [ - "admin_users" - ], + "tags": ["admin_users"], "summary": "Create new user.", "operationId": "adminCreateUser", "parameters": [ @@ -1085,9 +998,7 @@ } ], "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users:delete` and scope `global.users:*`.", - "tags": [ - "admin_users" - ], + "tags": ["admin_users"], "summary": "Delete global User.", "operationId": "adminDeleteUser", "parameters": [ @@ -1126,9 +1037,7 @@ } ], "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.authtoken:list` and scope `global.users:*`.", - "tags": [ - "admin_users" - ], + "tags": ["admin_users"], "summary": "Return a list of all auth tokens (devices) that the user currently have logged in from.", "operationId": "adminGetUserAuthTokens", "parameters": [ @@ -1164,9 +1073,7 @@ } ], "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users:disable` and scope `global.users:1` (userIDScope).", - "tags": [ - "admin_users" - ], + "tags": ["admin_users"], "summary": "Disable user.", "operationId": "adminDisableUser", "parameters": [ @@ -1205,9 +1112,7 @@ } ], "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users:enable` and scope `global.users:1` (userIDScope).", - "tags": [ - "admin_users" - ], + "tags": ["admin_users"], "summary": "Enable user.", "operationId": "adminEnableUser", "parameters": [ @@ -1246,9 +1151,7 @@ } ], "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.logout` and scope `global.users:*`.", - "tags": [ - "admin_users" - ], + "tags": ["admin_users"], "summary": "Logout user revokes all auth tokens (devices) for the user. User of issued auth tokens (devices) will no longer be logged in and will be required to authenticate again upon next activity.", "operationId": "adminLogoutUser", "parameters": [ @@ -1290,9 +1193,7 @@ } ], "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.password:update` and scope `global.users:*`.", - "tags": [ - "admin_users" - ], + "tags": ["admin_users"], "summary": "Set password for user.", "operationId": "adminUpdateUserPassword", "parameters": [ @@ -1334,9 +1235,7 @@ "/admin/users/{user_id}/permissions": { "put": { "description": "Only works with Basic Authentication (username and password). See introduction for an explanation.\nIf you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.permissions:update` and scope `global.users:*`.", - "tags": [ - "admin_users" - ], + "tags": ["admin_users"], "summary": "Set permissions for user.", "operationId": "adminUpdateUserPermissions", "parameters": [ @@ -1383,9 +1282,7 @@ } ], "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.quotas:list` and scope `global.users:1` (userIDScope).", - "tags": [ - "admin_users" - ], + "tags": ["admin_users"], "summary": "Fetch user quota.", "operationId": "getUserQuota", "parameters": [ @@ -1424,9 +1321,7 @@ } ], "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.quotas:update` and scope `global.users:1` (userIDScope).", - "tags": [ - "admin_users" - ], + "tags": ["admin_users"], "summary": "Update user quota.", "operationId": "updateUserQuota", "parameters": [ @@ -1479,9 +1374,7 @@ } ], "description": "Revokes the given auth token (device) for the user. User of issued auth token (device) will no longer be logged in and will be required to authenticate again upon next activity.\nIf you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `users.authtoken:update` and scope `global.users:*`.", - "tags": [ - "admin_users" - ], + "tags": ["admin_users"], "summary": "Revoke auth token for user.", "operationId": "adminRevokeUserAuthToken", "parameters": [ @@ -1526,9 +1419,7 @@ "/alert-notifications": { "get": { "description": "Returns all notification channels that the authenticated user has permission to view.", - "tags": [ - "legacy_alerts_notification_channels" - ], + "tags": ["legacy_alerts_notification_channels"], "summary": "Get all notification channels.", "operationId": "getAlertNotificationChannels", "responses": { @@ -1548,9 +1439,7 @@ }, "post": { "description": "You can find the full list of [supported notifiers](https://grafana.com/docs/grafana/latest/alerting/old-alerting/notifications/#list-of-supported-notifiers) on the alert notifiers page.", - "tags": [ - "legacy_alerts_notification_channels" - ], + "tags": ["legacy_alerts_notification_channels"], "summary": "Create notification channel.", "operationId": "createAlertNotificationChannel", "parameters": [ @@ -1585,9 +1474,7 @@ "/alert-notifications/lookup": { "get": { "description": "Returns all notification channels, but with less detailed information. Accessible by any authenticated user and is mainly used by providing alert notification channels in Grafana UI when configuring alert rule.", - "tags": [ - "legacy_alerts_notification_channels" - ], + "tags": ["legacy_alerts_notification_channels"], "summary": "Get all notification channels (lookup)", "operationId": "getAlertNotificationLookup", "responses": { @@ -1609,9 +1496,7 @@ "/alert-notifications/test": { "post": { "description": "Sends a test notification to the channel.", - "tags": [ - "legacy_alerts_notification_channels" - ], + "tags": ["legacy_alerts_notification_channels"], "summary": "Test notification channel.", "operationId": "notificationChannelTest", "parameters": [ @@ -1649,9 +1534,7 @@ "/alert-notifications/uid/{notification_channel_uid}": { "get": { "description": "Returns the notification channel given the notification channel UID.", - "tags": [ - "legacy_alerts_notification_channels" - ], + "tags": ["legacy_alerts_notification_channels"], "summary": "Get notification channel by UID", "operationId": "getAlertNotificationChannelByUID", "parameters": [ @@ -1682,9 +1565,7 @@ }, "put": { "description": "Updates an existing notification channel identified by uid.", - "tags": [ - "legacy_alerts_notification_channels" - ], + "tags": ["legacy_alerts_notification_channels"], "summary": "Update notification channel by UID.", "operationId": "updateAlertNotificationChannelByUID", "parameters": [ @@ -1723,9 +1604,7 @@ }, "delete": { "description": "Deletes an existing notification channel identified by UID.", - "tags": [ - "legacy_alerts_notification_channels" - ], + "tags": ["legacy_alerts_notification_channels"], "summary": "Delete alert notification by UID.", "operationId": "deleteAlertNotificationChannelByUID", "parameters": [ @@ -1758,9 +1637,7 @@ "/alert-notifications/{notification_channel_id}": { "get": { "description": "Returns the notification channel given the notification channel ID.", - "tags": [ - "legacy_alerts_notification_channels" - ], + "tags": ["legacy_alerts_notification_channels"], "summary": "Get notification channel by ID.", "operationId": "getAlertNotificationChannelByID", "parameters": [ @@ -1792,9 +1669,7 @@ }, "put": { "description": "Updates an existing notification channel identified by ID.", - "tags": [ - "legacy_alerts_notification_channels" - ], + "tags": ["legacy_alerts_notification_channels"], "summary": "Update notification channel by ID.", "operationId": "updateAlertNotificationChannel", "parameters": [ @@ -1834,9 +1709,7 @@ }, "delete": { "description": "Deletes an existing notification channel identified by ID.", - "tags": [ - "legacy_alerts_notification_channels" - ], + "tags": ["legacy_alerts_notification_channels"], "summary": "Delete alert notification by ID.", "operationId": "deleteAlertNotificationChannel", "parameters": [ @@ -1869,9 +1742,7 @@ }, "/alerts": { "get": { - "tags": [ - "legacy_alerts" - ], + "tags": ["legacy_alerts"], "summary": "Get legacy alerts.", "operationId": "getAlerts", "parameters": [ @@ -1898,15 +1769,7 @@ "in": "query" }, { - "enum": [ - "all", - "no_data", - "paused", - "alerting", - "ok", - "pending", - "unknown" - ], + "enum": ["all", "no_data", "paused", "alerting", "ok", "pending", "unknown"], "type": "string", "description": "Return alerts with one or more of the following alert states", "name": "state", @@ -1961,9 +1824,7 @@ }, "/alerts/states-for-dashboard": { "get": { - "tags": [ - "legacy_alerts" - ], + "tags": ["legacy_alerts"], "summary": "Get alert states for a dashboard.", "operationId": "getDashboardStates", "parameters": [ @@ -1990,9 +1851,7 @@ }, "/alerts/test": { "post": { - "tags": [ - "legacy_alerts" - ], + "tags": ["legacy_alerts"], "summary": "Test alert.", "operationId": "testAlert", "parameters": [ @@ -2026,9 +1885,7 @@ "/alerts/{alert_id}": { "get": { "description": "“evalMatches” data in the response is cached in the db when and only when the state of the alert changes (e.g. transitioning from “ok” to “alerting” state).\nIf data from one server triggers the alert first and, before that server is seen leaving alerting state, a second server also enters a state that would trigger the alert, the second server will not be visible in “evalMatches” data.", - "tags": [ - "legacy_alerts" - ], + "tags": ["legacy_alerts"], "summary": "Get alert by ID.", "operationId": "getAlertByID", "parameters": [ @@ -2054,9 +1911,7 @@ }, "/alerts/{alert_id}/pause": { "post": { - "tags": [ - "legacy_alerts" - ], + "tags": ["legacy_alerts"], "summary": "Pause/unpause alert by id.", "operationId": "pauseAlert", "parameters": [ @@ -2097,9 +1952,7 @@ "/annotations": { "get": { "description": "Starting in Grafana v6.4 regions annotations are now returned in one entity that now includes the timeEnd property.", - "tags": [ - "annotations" - ], + "tags": ["annotations"], "summary": "Find Annotations.", "operationId": "getAnnotations", "parameters": [ @@ -2169,10 +2022,7 @@ "in": "query" }, { - "enum": [ - "alert", - "annotation" - ], + "enum": ["alert", "annotation"], "type": "string", "description": "Return alerts or user created annotations", "name": "type", @@ -2199,9 +2049,7 @@ }, "post": { "description": "Creates an annotation in the Grafana database. The dashboardId and panelId fields are optional. If they are not specified then an organization annotation is created and can be queried in any dashboard that adds the Grafana annotations data source. When creating a region annotation include the timeEnd property.\nThe format for `time` and `timeEnd` should be epoch numbers in millisecond resolution.\nThe response for this HTTP request is slightly different in versions prior to v6.4. In prior versions you would also get an endId if you where creating a region. But in 6.4 regions are represented using a single event with time and timeEnd properties.", - "tags": [ - "annotations" - ], + "tags": ["annotations"], "summary": "Create Annotation.", "operationId": "postAnnotation", "parameters": [ @@ -2236,9 +2084,7 @@ "/annotations/graphite": { "post": { "description": "Creates an annotation by using Graphite-compatible event format. The `when` and `data` fields are optional. If `when` is not specified then the current time will be used as annotation’s timestamp. The `tags` field can also be in prior to Graphite `0.10.0` format (string with multiple tags being separated by a space).", - "tags": [ - "annotations" - ], + "tags": ["annotations"], "summary": "Create Annotation in Graphite format.", "operationId": "postGraphiteAnnotation", "parameters": [ @@ -2272,9 +2118,7 @@ }, "/annotations/mass-delete": { "post": { - "tags": [ - "annotations" - ], + "tags": ["annotations"], "summary": "Delete multiple annotations.", "operationId": "massDeleteAnnotations", "parameters": [ @@ -2303,9 +2147,7 @@ "/annotations/tags": { "get": { "description": "Find all the event tags created in the annotations.", - "tags": [ - "annotations" - ], + "tags": ["annotations"], "summary": "Find Annotations Tags.", "operationId": "getAnnotationTags", "parameters": [ @@ -2338,9 +2180,7 @@ }, "/annotations/{annotation_id}": { "get": { - "tags": [ - "annotations" - ], + "tags": ["annotations"], "summary": "Get Annotation by Id.", "operationId": "getAnnotationByID", "parameters": [ @@ -2365,9 +2205,7 @@ }, "put": { "description": "Updates all properties of an annotation that matches the specified id. To only update certain property, consider using the Patch Annotation operation.", - "tags": [ - "annotations" - ], + "tags": ["annotations"], "summary": "Update Annotation.", "operationId": "updateAnnotation", "parameters": [ @@ -2406,9 +2244,7 @@ }, "delete": { "description": "Deletes the annotation that matches the specified ID.", - "tags": [ - "annotations" - ], + "tags": ["annotations"], "summary": "Delete Annotation By ID.", "operationId": "deleteAnnotationByID", "parameters": [ @@ -2436,9 +2272,7 @@ }, "patch": { "description": "Updates one or more properties of an annotation that matches the specified ID.\nThis operation currently supports updating of the `text`, `tags`, `time` and `timeEnd` properties.\nThis is available in Grafana 6.0.0-beta2 and above.", - "tags": [ - "annotations" - ], + "tags": ["annotations"], "summary": "Patch Annotation", "operationId": "patchAnnotation", "parameters": [ @@ -2478,12 +2312,8 @@ }, "/api/v1/provisioning/alert-rules": { "post": { - "consumes": [ - "application/json" - ], - "tags": [ - "provisioning" - ], + "consumes": ["application/json"], + "tags": ["provisioning"], "summary": "Create a new alert rule.", "operationId": "RoutePostAlertRule", "parameters": [ @@ -2513,9 +2343,7 @@ }, "/api/v1/provisioning/alert-rules/{UID}": { "get": { - "tags": [ - "provisioning" - ], + "tags": ["provisioning"], "summary": "Get a specific alert rule by UID.", "operationId": "RouteGetAlertRule", "parameters": [ @@ -2540,12 +2368,8 @@ } }, "put": { - "consumes": [ - "application/json" - ], - "tags": [ - "provisioning" - ], + "consumes": ["application/json"], + "tags": ["provisioning"], "summary": "Update an existing alert rule.", "operationId": "RoutePutAlertRule", "parameters": [ @@ -2580,9 +2404,7 @@ } }, "delete": { - "tags": [ - "provisioning" - ], + "tags": ["provisioning"], "summary": "Delete a specific alert rule by UID.", "operationId": "RouteDeleteAlertRule", "parameters": [ @@ -2603,9 +2425,7 @@ }, "/api/v1/provisioning/contact-points": { "get": { - "tags": [ - "provisioning" - ], + "tags": ["provisioning"], "summary": "Get all the contact points.", "operationId": "RouteGetContactpoints", "parameters": [ @@ -2626,12 +2446,8 @@ } }, "post": { - "consumes": [ - "application/json" - ], - "tags": [ - "provisioning" - ], + "consumes": ["application/json"], + "tags": ["provisioning"], "summary": "Create a contact point.", "operationId": "RoutePostContactpoints", "parameters": [ @@ -2661,12 +2477,8 @@ }, "/api/v1/provisioning/contact-points/{UID}": { "put": { - "consumes": [ - "application/json" - ], - "tags": [ - "provisioning" - ], + "consumes": ["application/json"], + "tags": ["provisioning"], "summary": "Update an existing contact point.", "operationId": "RoutePutContactpoint", "parameters": [ @@ -2701,12 +2513,8 @@ } }, "delete": { - "consumes": [ - "application/json" - ], - "tags": [ - "provisioning" - ], + "consumes": ["application/json"], + "tags": ["provisioning"], "summary": "Delete a contact point.", "operationId": "RouteDeleteContactpoints", "parameters": [ @@ -2727,9 +2535,7 @@ }, "/api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}": { "get": { - "tags": [ - "provisioning" - ], + "tags": ["provisioning"], "summary": "Get a rule group.", "operationId": "RouteGetAlertRuleGroup", "parameters": [ @@ -2759,12 +2565,8 @@ } }, "put": { - "consumes": [ - "application/json" - ], - "tags": [ - "provisioning" - ], + "consumes": ["application/json"], + "tags": ["provisioning"], "summary": "Update the interval of a rule group.", "operationId": "RoutePutAlertRuleGroup", "parameters": [ @@ -2806,9 +2608,7 @@ }, "/api/v1/provisioning/mute-timings": { "get": { - "tags": [ - "provisioning" - ], + "tags": ["provisioning"], "summary": "Get all the mute timings.", "operationId": "RouteGetMuteTimings", "responses": { @@ -2821,12 +2621,8 @@ } }, "post": { - "consumes": [ - "application/json" - ], - "tags": [ - "provisioning" - ], + "consumes": ["application/json"], + "tags": ["provisioning"], "summary": "Create a new mute timing.", "operationId": "RoutePostMuteTiming", "parameters": [ @@ -2856,9 +2652,7 @@ }, "/api/v1/provisioning/mute-timings/{name}": { "get": { - "tags": [ - "provisioning" - ], + "tags": ["provisioning"], "summary": "Get a mute timing.", "operationId": "RouteGetMuteTiming", "parameters": [ @@ -2883,12 +2677,8 @@ } }, "put": { - "consumes": [ - "application/json" - ], - "tags": [ - "provisioning" - ], + "consumes": ["application/json"], + "tags": ["provisioning"], "summary": "Replace an existing mute timing.", "operationId": "RoutePutMuteTiming", "parameters": [ @@ -2923,9 +2713,7 @@ } }, "delete": { - "tags": [ - "provisioning" - ], + "tags": ["provisioning"], "summary": "Delete a mute timing.", "operationId": "RouteDeleteMuteTiming", "parameters": [ @@ -2946,9 +2734,7 @@ }, "/api/v1/provisioning/policies": { "get": { - "tags": [ - "provisioning" - ], + "tags": ["provisioning"], "summary": "Get the notification policy tree.", "operationId": "RouteGetPolicyTree", "responses": { @@ -2961,12 +2747,8 @@ } }, "put": { - "consumes": [ - "application/json" - ], - "tags": [ - "provisioning" - ], + "consumes": ["application/json"], + "tags": ["provisioning"], "summary": "Sets the notification policy tree.", "operationId": "RoutePutPolicyTree", "parameters": [ @@ -2995,12 +2777,8 @@ } }, "delete": { - "consumes": [ - "application/json" - ], - "tags": [ - "provisioning" - ], + "consumes": ["application/json"], + "tags": ["provisioning"], "summary": "Clears the notification policy tree.", "operationId": "RouteResetPolicyTree", "responses": { @@ -3015,9 +2793,7 @@ }, "/api/v1/provisioning/templates": { "get": { - "tags": [ - "provisioning" - ], + "tags": ["provisioning"], "summary": "Get all message templates.", "operationId": "RouteGetTemplates", "responses": { @@ -3035,9 +2811,7 @@ }, "/api/v1/provisioning/templates/{name}": { "get": { - "tags": [ - "provisioning" - ], + "tags": ["provisioning"], "summary": "Get a message template.", "operationId": "RouteGetTemplate", "parameters": [ @@ -3062,12 +2836,8 @@ } }, "put": { - "consumes": [ - "application/json" - ], - "tags": [ - "provisioning" - ], + "consumes": ["application/json"], + "tags": ["provisioning"], "summary": "Updates an existing template.", "operationId": "RoutePutTemplate", "parameters": [ @@ -3102,9 +2872,7 @@ } }, "delete": { - "tags": [ - "provisioning" - ], + "tags": ["provisioning"], "summary": "Delete a template.", "operationId": "RouteDeleteTemplate", "parameters": [ @@ -3126,9 +2894,7 @@ "/auth/keys": { "get": { "description": "Will return auth keys.", - "tags": [ - "api_keys" - ], + "tags": ["api_keys"], "summary": "Get auth keys.", "operationId": "getAPIkeys", "parameters": [ @@ -3160,9 +2926,7 @@ }, "post": { "description": "Will return details of the created API key", - "tags": [ - "api_keys" - ], + "tags": ["api_keys"], "summary": "Creates an API key.", "operationId": "addAPIkey", "parameters": [ @@ -3199,9 +2963,7 @@ }, "/auth/keys/{id}": { "delete": { - "tags": [ - "api_keys" - ], + "tags": ["api_keys"], "summary": "Delete API key.", "operationId": "deleteAPIkey", "parameters": [ @@ -3234,9 +2996,7 @@ }, "/dashboard/snapshots": { "get": { - "tags": [ - "snapshots" - ], + "tags": ["snapshots"], "summary": "List snapshots.", "operationId": "searchDashboardSnapshots", "parameters": [ @@ -3267,13 +3027,8 @@ }, "/dashboards/calculate-diff": { "post": { - "produces": [ - "application/json", - "text/html" - ], - "tags": [ - "dashboards" - ], + "produces": ["application/json", "text/html"], + "tags": ["dashboards"], "summary": "Perform diff on two dashboards.", "operationId": "calculateDashboardDiff", "parameters": [ @@ -3290,10 +3045,7 @@ "diffType": { "description": "The type of diff to return\nDescription:\n`basic`\n`json`", "type": "string", - "enum": [ - "basic", - "json" - ] + "enum": ["basic", "json"] }, "new": { "$ref": "#/definitions/CalculateDiffTarget" @@ -3321,9 +3073,7 @@ "/dashboards/db": { "post": { "description": "Creates a new dashboard or updates an existing dashboard.", - "tags": [ - "dashboards" - ], + "tags": ["dashboards"], "summary": "Create / Update dashboard", "operationId": "postDashboard", "parameters": [ @@ -3366,9 +3116,7 @@ }, "/dashboards/home": { "get": { - "tags": [ - "dashboards" - ], + "tags": ["dashboards"], "summary": "Get home dashboard.", "operationId": "getHomeDashboard", "responses": { @@ -3387,9 +3135,7 @@ "/dashboards/id/{DashboardID}/permissions": { "get": { "description": "Please refer to [updated API](#/dashboard_permissions/getDashboardPermissionsListByUID) instead", - "tags": [ - "dashboard_permissions" - ], + "tags": ["dashboard_permissions"], "summary": "Gets all existing permissions for the given dashboard.", "operationId": "getDashboardPermissionsListByID", "deprecated": true, @@ -3422,9 +3168,7 @@ }, "post": { "description": "Please refer to [updated API](#/dashboard_permissions/updateDashboardPermissionsByUID) instead\n\nThis operation will remove existing permissions if they’re not included in the request.", - "tags": [ - "dashboard_permissions" - ], + "tags": ["dashboard_permissions"], "summary": "Updates permissions for a dashboard.", "operationId": "updateDashboardPermissionsByID", "deprecated": true, @@ -3470,9 +3214,7 @@ "/dashboards/id/{DashboardID}/restore": { "post": { "description": "Please refer to [updated API](#/dashboard_versions/restoreDashboardVersionByUID) instead", - "tags": [ - "dashboard_versions" - ], + "tags": ["dashboard_versions"], "summary": "Restore a dashboard to a given dashboard version.", "operationId": "restoreDashboardVersionByID", "deprecated": true, @@ -3515,9 +3257,7 @@ "/dashboards/id/{DashboardID}/versions": { "get": { "description": "Please refer to [updated API](#/dashboard_versions/getDashboardVersionsByUID) instead", - "tags": [ - "dashboard_versions" - ], + "tags": ["dashboard_versions"], "summary": "Gets all existing versions for the dashboard.", "operationId": "getDashboardVersionsByID", "deprecated": true, @@ -3552,9 +3292,7 @@ "/dashboards/id/{DashboardID}/versions/{DashboardVersionID}": { "get": { "description": "Please refer to [updated API](#/dashboard_versions/getDashboardVersionByUID) instead", - "tags": [ - "dashboard_versions" - ], + "tags": ["dashboard_versions"], "summary": "Get a specific dashboard version.", "operationId": "getDashboardVersionByID", "deprecated": true, @@ -3595,9 +3333,7 @@ }, "/dashboards/import": { "post": { - "tags": [ - "dashboards" - ], + "tags": ["dashboards"], "summary": "Import dashboard.", "operationId": "importDashboard", "parameters": [ @@ -3634,9 +3370,7 @@ }, "/dashboards/tags": { "get": { - "tags": [ - "dashboards" - ], + "tags": ["dashboards"], "summary": "Get all dashboards tags of an organisation.", "operationId": "getDashboardTags", "responses": { @@ -3654,9 +3388,7 @@ }, "/dashboards/trim": { "post": { - "tags": [ - "dashboards" - ], + "tags": ["dashboards"], "summary": "Trim defaults from dashboard.", "operationId": "trimDashboard", "parameters": [ @@ -3685,9 +3417,7 @@ "/dashboards/uid/{uid}": { "get": { "description": "Will return the dashboard given the dashboard unique identifier (uid).", - "tags": [ - "dashboards" - ], + "tags": ["dashboards"], "summary": "Get dashboard by uid.", "operationId": "getDashboardByUID", "parameters": [ @@ -3718,9 +3448,7 @@ }, "delete": { "description": "Will delete the dashboard given the specified unique identifier (uid).", - "tags": [ - "dashboards" - ], + "tags": ["dashboards"], "summary": "Delete dashboard by uid.", "operationId": "deleteDashboardByUID", "parameters": [ @@ -3752,9 +3480,7 @@ }, "/dashboards/uid/{uid}/permissions": { "get": { - "tags": [ - "dashboard_permissions" - ], + "tags": ["dashboard_permissions"], "summary": "Gets all existing permissions for the given dashboard.", "operationId": "getDashboardPermissionsListByUID", "parameters": [ @@ -3785,9 +3511,7 @@ }, "post": { "description": "This operation will remove existing permissions if they’re not included in the request.", - "tags": [ - "dashboard_permissions" - ], + "tags": ["dashboard_permissions"], "summary": "Updates permissions for a dashboard.", "operationId": "updateDashboardPermissionsByUID", "parameters": [ @@ -3830,9 +3554,7 @@ }, "/dashboards/uid/{uid}/restore": { "post": { - "tags": [ - "dashboard_versions" - ], + "tags": ["dashboard_versions"], "summary": "Restore a dashboard to a given dashboard version using UID.", "operationId": "restoreDashboardVersionByUID", "parameters": [ @@ -3872,9 +3594,7 @@ }, "/dashboards/uid/{uid}/versions": { "get": { - "tags": [ - "dashboard_versions" - ], + "tags": ["dashboard_versions"], "summary": "Gets all existing versions for the dashboard using UID.", "operationId": "getDashboardVersionsByUID", "parameters": [ @@ -3922,9 +3642,7 @@ }, "/dashboards/uid/{uid}/versions/{DashboardVersionID}": { "get": { - "tags": [ - "dashboard_versions" - ], + "tags": ["dashboard_versions"], "summary": "Get a specific dashboard version using UID.", "operationId": "getDashboardVersionByUID", "parameters": [ @@ -3964,9 +3682,7 @@ "/datasources": { "get": { "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:read` and scope: `datasources:*`.", - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Get all data sources.", "operationId": "getDataSources", "responses": { @@ -3986,9 +3702,7 @@ }, "post": { "description": "By defining `password` and `basicAuthPassword` under secureJsonData property\nGrafana encrypts them securely as an encrypted blob in the database.\nThe response then lists the encrypted fields under secureJsonFields.\n\nIf you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:create`", - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Create a data source.", "operationId": "addDataSource", "parameters": [ @@ -4023,9 +3737,7 @@ "/datasources/id/{name}": { "get": { "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:read` and scopes: `datasources:*`, `datasources:name:*` and `datasources:name:test_datasource` (single data source).", - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Get data source Id by Name.", "operationId": "getDataSourceIdByName", "parameters": [ @@ -4058,9 +3770,7 @@ "/datasources/name/{name}": { "get": { "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:read` and scopes: `datasources:*`, `datasources:name:*` and `datasources:name:test_datasource` (single data source).", - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Get a single data source by Name.", "operationId": "getDataSourceByName", "parameters": [ @@ -4088,9 +3798,7 @@ }, "delete": { "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:delete` and scopes: `datasources:*`, `datasources:name:*` and `datasources:name:test_datasource` (single data source).", - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Delete an existing data source by name.", "operationId": "deleteDataSourceByName", "parameters": [ @@ -4123,9 +3831,7 @@ "/datasources/proxy/uid/{uid}/{datasource_proxy_route}": { "get": { "description": "Proxies all calls to the actual data source.", - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Data source proxy GET calls.", "operationId": "datasourceProxyGETByUIDcalls", "parameters": [ @@ -4165,9 +3871,7 @@ }, "post": { "description": "Proxies all calls to the actual data source. The data source should support POST methods for the specific path and role as defined", - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Data source proxy POST calls.", "operationId": "datasourceProxyPOSTByUIDcalls", "parameters": [ @@ -4218,9 +3922,7 @@ }, "delete": { "description": "Proxies all calls to the actual data source.", - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Data source proxy DELETE calls.", "operationId": "datasourceProxyDELETEByUIDcalls", "parameters": [ @@ -4262,9 +3964,7 @@ "/datasources/proxy/{id}/{datasource_proxy_route}": { "get": { "description": "Proxies all calls to the actual data source.\n\nPlease refer to [updated API](#/datasources/datasourceProxyGETByUIDcalls) instead", - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Data source proxy GET calls.", "operationId": "datasourceProxyGETcalls", "deprecated": true, @@ -4305,9 +4005,7 @@ }, "post": { "description": "Proxies all calls to the actual data source. The data source should support POST methods for the specific path and role as defined\n\nPlease refer to [updated API](#/datasources/datasourceProxyPOSTByUIDcalls) instead", - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Data source proxy POST calls.", "operationId": "datasourceProxyPOSTcalls", "deprecated": true, @@ -4359,9 +4057,7 @@ }, "delete": { "description": "Proxies all calls to the actual data source.\n\nPlease refer to [updated API](#/datasources/datasourceProxyDELETEByUIDcalls) instead", - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Data source proxy DELETE calls.", "operationId": "datasourceProxyDELETEcalls", "deprecated": true, @@ -4403,9 +4099,7 @@ }, "/datasources/uid/{sourceUID}/correlations": { "post": { - "tags": [ - "correlations" - ], + "tags": ["correlations"], "summary": "Add correlation.", "operationId": "createCorrelation", "parameters": [ @@ -4449,9 +4143,7 @@ "/datasources/uid/{uid}": { "get": { "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:read` and scopes: `datasources:*`, `datasources:uid:*` and `datasources:uid:kLtEtcRGk` (single data source).", - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Get a single data source by UID.", "operationId": "getDataSourceByUID", "parameters": [ @@ -4485,9 +4177,7 @@ }, "put": { "description": "Similar to creating a data source, `password` and `basicAuthPassword` should be defined under\nsecureJsonData in order to be stored securely as an encrypted blob in the database. Then, the\nencrypted fields are listed under secureJsonFields section in the response.\n\nIf you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:write` and scopes: `datasources:*`, `datasources:uid:*` and `datasources:uid:1` (single data source).", - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Update an existing data source.", "operationId": "updateDataSourceByUID", "parameters": [ @@ -4523,9 +4213,7 @@ }, "delete": { "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:delete` and scopes: `datasources:*`, `datasources:uid:*` and `datasources:uid:kLtEtcRGk` (single data source).", - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Delete an existing data source by UID.", "operationId": "deleteDataSourceByUID", "parameters": [ @@ -4557,9 +4245,7 @@ }, "/datasources/uid/{uid}/correlations/{correlationUID}": { "delete": { - "tags": [ - "correlations" - ], + "tags": ["correlations"], "summary": "Delete a correlation.", "operationId": "deleteCorrelation", "parameters": [ @@ -4597,9 +4283,7 @@ }, "/datasources/uid/{uid}/health": { "get": { - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Sends a health check request to the plugin datasource identified by the UID.", "operationId": "checkDatasourceHealthWithUID", "parameters": [ @@ -4631,9 +4315,7 @@ }, "/datasources/uid/{uid}/resources/{datasource_proxy_route}": { "get": { - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Fetch data source resources.", "operationId": "callDatasourceResourceWithUID", "parameters": [ @@ -4675,10 +4357,7 @@ "/datasources/{datasourceId}/disable-permissions": { "post": { "description": "Disables permissions for the data source with the given id. All existing permissions will be removed and anyone will be able to query the data source.\n\nYou need to have a permission with action `datasources.permissions:toggle` and scopes `datasources:*`, `datasources:id:*`, `datasources:id:1` (single data source).", - "tags": [ - "datasource_permissions", - "enterprise" - ], + "tags": ["datasource_permissions", "enterprise"], "summary": "Disable permissions for a data source.", "operationId": "disablePermissions", "parameters": [ @@ -4714,10 +4393,7 @@ "/datasources/{datasourceId}/enable-permissions": { "post": { "description": "Enables permissions for the data source with the given id.\nNo one except Org Admins will be able to query the data source until permissions have been added\nwhich permit certain users or teams to query the data source.\n\nYou need to have a permission with action `datasources.permissions:toggle` and scopes `datasources:*`, `datasources:id:*`, `datasources:id:1` (single data source).", - "tags": [ - "datasource_permissions", - "enterprise" - ], + "tags": ["datasource_permissions", "enterprise"], "summary": "Enable permissions for a data source.", "operationId": "enablePermissions", "parameters": [ @@ -4753,10 +4429,7 @@ "/datasources/{datasourceId}/permissions": { "get": { "description": "Gets all existing permissions for the data source with the given id.\n\nYou need to have a permission with action `datasources.permissions:read` and scopes `datasources:*`, `datasources:id:*`, `datasources:id:1` (single data source).", - "tags": [ - "datasource_permissions", - "enterprise" - ], + "tags": ["datasource_permissions", "enterprise"], "summary": "Get permissions for a data source.", "operationId": "getAllPermissions", "parameters": [ @@ -4787,10 +4460,7 @@ }, "post": { "description": "You need to have a permission with action `datasources.permissions:read` and scopes `datasources:*`, `datasources:id:*`, `datasources:id:1` (single data source).", - "tags": [ - "datasource_permissions", - "enterprise" - ], + "tags": ["datasource_permissions", "enterprise"], "summary": "Add permissions for a data source.", "operationId": "addPermission", "parameters": [ @@ -4846,10 +4516,7 @@ "/datasources/{datasourceId}/permissions/{permissionId}": { "delete": { "description": "Removes the permission with the given permissionId for the data source with the given id.\n\nYou need to have a permission with action `datasources.permissions:delete` and scopes `datasources:*`, `datasources:id:*`, `datasources:id:1` (single data source).", - "tags": [ - "datasource_permissions", - "enterprise" - ], + "tags": ["datasource_permissions", "enterprise"], "summary": "Remove permission for a data source.", "operationId": "deletePermissions", "parameters": [ @@ -4885,9 +4552,7 @@ "/datasources/{id}": { "get": { "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:read` and scopes: `datasources:*`, `datasources:id:*` and `datasources:id:1` (single data source).\n\nPlease refer to [updated API](#/datasources/getDataSourceByUID) instead", - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Get a single data source by Id.", "operationId": "getDataSourceByID", "deprecated": true, @@ -4922,9 +4587,7 @@ }, "put": { "description": "Similar to creating a data source, `password` and `basicAuthPassword` should be defined under\nsecureJsonData in order to be stored securely as an encrypted blob in the database. Then, the\nencrypted fields are listed under secureJsonFields section in the response.\n\nIf you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:write` and scopes: `datasources:*`, `datasources:id:*` and `datasources:id:1` (single data source).\n\nPlease refer to [updated API](#/datasources/updateDataSourceByUID) instead", - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Update an existing data source by its sequential ID.", "operationId": "updateDataSourceByID", "deprecated": true, @@ -4961,9 +4624,7 @@ }, "delete": { "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:delete` and scopes: `datasources:*`, `datasources:id:*` and `datasources:id:1` (single data source).\n\nPlease refer to [updated API](#/datasources/deleteDataSourceByUID) instead", - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Delete an existing data source by id.", "operationId": "deleteDataSourceByID", "deprecated": true, @@ -4997,9 +4658,7 @@ "/datasources/{id}/health": { "get": { "description": "Please refer to [updated API](#/datasources/checkDatasourceHealthWithUID) instead", - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Sends a health check request to the plugin datasource identified by the ID.", "operationId": "checkDatasourceHealthByID", "deprecated": true, @@ -5033,9 +4692,7 @@ "/datasources/{id}/resources/{datasource_proxy_route}": { "get": { "description": "Please refer to [updated API](#/datasources/callDatasourceResourceWithUID) instead", - "tags": [ - "datasources" - ], + "tags": ["datasources"], "summary": "Fetch data source resources by Id.", "operationId": "callDatasourceResourceByID", "deprecated": true, @@ -5078,9 +4735,7 @@ "/ds/query": { "post": { "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:query`.", - "tags": [ - "ds" - ], + "tags": ["ds"], "summary": "DataSource query metrics with expressions", "operationId": "queryMetricsWithExpressions", "parameters": [ @@ -5118,9 +4773,7 @@ "/folders": { "get": { "description": "Returns all folders that the authenticated user has permission to view.", - "tags": [ - "folders" - ], + "tags": ["folders"], "summary": "Get all folders.", "operationId": "getFolders", "parameters": [ @@ -5157,9 +4810,7 @@ } }, "post": { - "tags": [ - "folders" - ], + "tags": ["folders"], "summary": "Create folder.", "operationId": "createFolder", "parameters": [ @@ -5197,9 +4848,7 @@ "/folders/id/{folder_id}": { "get": { "description": "Returns the folder identified by id.", - "tags": [ - "folders" - ], + "tags": ["folders"], "summary": "Get folder by id.", "operationId": "getFolderByID", "parameters": [ @@ -5232,9 +4881,7 @@ }, "/folders/{folder_uid}": { "get": { - "tags": [ - "folders" - ], + "tags": ["folders"], "summary": "Get folder by uid.", "operationId": "getFolderByUID", "parameters": [ @@ -5264,9 +4911,7 @@ } }, "put": { - "tags": [ - "folders" - ], + "tags": ["folders"], "summary": "Update folder.", "operationId": "updateFolder", "parameters": [ @@ -5312,9 +4957,7 @@ }, "delete": { "description": "Deletes an existing folder identified by UID along with all dashboards (and their alerts) stored in the folder. This operation cannot be reverted.", - "tags": [ - "folders" - ], + "tags": ["folders"], "summary": "Delete folder.", "operationId": "deleteFolder", "parameters": [ @@ -5356,9 +4999,7 @@ }, "/folders/{folder_uid}/permissions": { "get": { - "tags": [ - "folder_permissions" - ], + "tags": ["folder_permissions"], "summary": "Gets all existing permissions for the folder with the given `uid`.", "operationId": "getFolderPermissionList", "parameters": [ @@ -5388,9 +5029,7 @@ } }, "post": { - "tags": [ - "folder_permissions" - ], + "tags": ["folder_permissions"], "summary": "Updates permissions for a folder. This operation will remove existing permissions if they’re not included in the request.", "operationId": "updateFolderPermissions", "parameters": [ @@ -5431,9 +5070,7 @@ "/library-elements": { "get": { "description": "Returns a list of all library elements the authenticated user has permission to view.\nUse the `perPage` query parameter to control the maximum number of library elements returned; the default limit is `100`.\nYou can also use the `page` query parameter to fetch library elements from any page other than the first one.", - "tags": [ - "library_elements" - ], + "tags": ["library_elements"], "summary": "Get all library elements.", "operationId": "getLibraryElements", "parameters": [ @@ -5444,10 +5081,7 @@ "in": "query" }, { - "enum": [ - 1, - 2 - ], + "enum": [1, 2], "type": "integer", "format": "int64", "description": "Kind of element to search for.", @@ -5455,10 +5089,7 @@ "in": "query" }, { - "enum": [ - "alpha-asc", - "alpha-desc" - ], + "enum": ["alpha-asc", "alpha-desc"], "type": "string", "description": "Sort order of elements.", "name": "sortDirection", @@ -5513,9 +5144,7 @@ }, "post": { "description": "Creates a new library element.", - "tags": [ - "library_elements" - ], + "tags": ["library_elements"], "summary": "Create library element.", "operationId": "createLibraryElement", "parameters": [ @@ -5553,9 +5182,7 @@ "/library-elements/name/{library_element_name}": { "get": { "description": "Returns a library element with the given name.", - "tags": [ - "library_elements" - ], + "tags": ["library_elements"], "summary": "Get library element by name.", "operationId": "getLibraryElementByName", "parameters": [ @@ -5585,9 +5212,7 @@ "/library-elements/{library_element_uid}": { "get": { "description": "Returns a library element with the given UID.", - "tags": [ - "library_elements" - ], + "tags": ["library_elements"], "summary": "Get library element by UID.", "operationId": "getLibraryElementByUID", "parameters": [ @@ -5615,9 +5240,7 @@ }, "delete": { "description": "Deletes an existing library element as specified by the UID. This operation cannot be reverted.\nYou cannot delete a library element that is connected. This operation cannot be reverted.", - "tags": [ - "library_elements" - ], + "tags": ["library_elements"], "summary": "Delete library element.", "operationId": "deleteLibraryElementByUID", "parameters": [ @@ -5651,9 +5274,7 @@ }, "patch": { "description": "Updates an existing library element identified by uid.", - "tags": [ - "library_elements" - ], + "tags": ["library_elements"], "summary": "Update library element.", "operationId": "updateLibraryElement", "parameters": [ @@ -5700,9 +5321,7 @@ "/library-elements/{library_element_uid}/connections/": { "get": { "description": "Returns a list of connections for a library element based on the UID specified.", - "tags": [ - "library_elements" - ], + "tags": ["library_elements"], "summary": "Get library element connections.", "operationId": "getLibraryElementConnections", "parameters": [ @@ -5731,10 +5350,7 @@ }, "/licensing/check": { "get": { - "tags": [ - "licensing", - "enterprise" - ], + "tags": ["licensing", "enterprise"], "summary": "Check license availability.", "operationId": "getStatus", "responses": { @@ -5747,10 +5363,7 @@ "/licensing/custom-permissions": { "get": { "description": "You need to have a permission with action `licensing.reports:read`.", - "tags": [ - "licensing", - "enterprise" - ], + "tags": ["licensing", "enterprise"], "summary": "Get custom permissions report.", "operationId": "getCustomPermissionsReport", "responses": { @@ -5766,13 +5379,8 @@ "/licensing/custom-permissions-csv": { "get": { "description": "You need to have a permission with action `licensing.reports:read`.", - "produces": [ - "text/csv" - ], - "tags": [ - "licensing", - "enterprise" - ], + "produces": ["text/csv"], + "tags": ["licensing", "enterprise"], "summary": "Get custom permissions report in CSV format.", "operationId": "getCustomPermissionsCSV", "responses": { @@ -5788,10 +5396,7 @@ "/licensing/refresh-stats": { "get": { "description": "You need to have a permission with action `licensing:read`.", - "tags": [ - "licensing", - "enterprise" - ], + "tags": ["licensing", "enterprise"], "summary": "Refresh license stats.", "operationId": "refreshLicenseStats", "responses": { @@ -5807,10 +5412,7 @@ "/licensing/token": { "get": { "description": "You need to have a permission with action `licensing:read`.", - "tags": [ - "licensing", - "enterprise" - ], + "tags": ["licensing", "enterprise"], "summary": "Get license token.", "operationId": "getLicenseToken", "responses": { @@ -5821,10 +5423,7 @@ }, "post": { "description": "You need to have a permission with action `licensing:update`.", - "tags": [ - "licensing", - "enterprise" - ], + "tags": ["licensing", "enterprise"], "summary": "Create license token.", "operationId": "postLicenseToken", "parameters": [ @@ -5848,10 +5447,7 @@ }, "delete": { "description": "Removes the license stored in the Grafana database. Available in Grafana Enterprise v7.4+.\n\nYou need to have a permission with action `licensing:delete`.", - "tags": [ - "licensing", - "enterprise" - ], + "tags": ["licensing", "enterprise"], "summary": "Remove license from database.", "operationId": "deleteLicenseToken", "parameters": [ @@ -5889,10 +5485,7 @@ "/licensing/token/renew": { "post": { "description": "Manually ask license issuer for a new token. Available in Grafana Enterprise v7.4+.\n\nYou need to have a permission with action `licensing:update`.", - "tags": [ - "licensing", - "enterprise" - ], + "tags": ["licensing", "enterprise"], "summary": "Manually force license refresh.", "operationId": "postRenewLicenseToken", "parameters": [ @@ -5920,10 +5513,7 @@ }, "/logout/saml": { "get": { - "tags": [ - "saml", - "enterprise" - ], + "tags": ["saml", "enterprise"], "summary": "GetLogout initiates single logout process.", "operationId": "getSAMLLogout", "responses": { @@ -5942,9 +5532,7 @@ "/org": { "get": { "description": "Get current Organization", - "tags": [ - "org" - ], + "tags": ["org"], "operationId": "getCurrentOrg", "responses": { "200": { @@ -5962,9 +5550,7 @@ } }, "put": { - "tags": [ - "org" - ], + "tags": ["org"], "summary": "Update current Organization.", "operationId": "updateCurrentOrg", "parameters": [ @@ -5998,9 +5584,7 @@ }, "/org/address": { "put": { - "tags": [ - "org" - ], + "tags": ["org"], "summary": "Update current Organization's address.", "operationId": "updateCurrentOrgAddress", "parameters": [ @@ -6034,9 +5618,7 @@ }, "/org/invites": { "get": { - "tags": [ - "org_invites" - ], + "tags": ["org_invites"], "summary": "Get pending invites.", "operationId": "getPendingOrgInvites", "responses": { @@ -6055,9 +5637,7 @@ } }, "post": { - "tags": [ - "org_invites" - ], + "tags": ["org_invites"], "summary": "Add invite.", "operationId": "addOrgInvite", "parameters": [ @@ -6094,9 +5674,7 @@ }, "/org/invites/{invitation_code}/revoke": { "delete": { - "tags": [ - "org_invites" - ], + "tags": ["org_invites"], "summary": "Revoke invite.", "operationId": "revokeInvite", "parameters": [ @@ -6128,9 +5706,7 @@ }, "/org/preferences": { "get": { - "tags": [ - "org_preferences" - ], + "tags": ["org_preferences"], "summary": "Get Current Org Prefs.", "operationId": "getOrgPreferences", "responses": { @@ -6149,9 +5725,7 @@ } }, "put": { - "tags": [ - "org_preferences" - ], + "tags": ["org_preferences"], "summary": "Update Current Org Prefs.", "operationId": "updateOrgPreferences", "parameters": [ @@ -6183,9 +5757,7 @@ } }, "patch": { - "tags": [ - "org_preferences" - ], + "tags": ["org_preferences"], "summary": "Patch Current Org Prefs.", "operationId": "patchOrgPreferences", "parameters": [ @@ -6220,9 +5792,7 @@ "/org/users": { "get": { "description": "Returns all org users within the current organization. Accessible to users with org admin role.\nIf you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `org.users:read` with scope `users:*`.", - "tags": [ - "org" - ], + "tags": ["org"], "summary": "Get all users within the current organization.", "operationId": "getOrgUsersForCurrentOrg", "responses": { @@ -6242,9 +5812,7 @@ }, "post": { "description": "Adds a global user to the current organization.\n\nIf you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `org.users:add` with scope `users:*`.", - "tags": [ - "org" - ], + "tags": ["org"], "summary": "Add a new user to the current organization", "operationId": "addOrgUserToCurrentOrg", "parameters": [ @@ -6276,9 +5844,7 @@ "/org/users/lookup": { "get": { "description": "Returns all org users within the current organization, but with less detailed information.\nAccessible to users with org admin role, admin in any folder or admin of any team.\nMainly used by Grafana UI for providing list of users when adding team members and when editing folder/dashboard permissions.", - "tags": [ - "org" - ], + "tags": ["org"], "summary": "Get all users within the current organization (lookup)", "operationId": "getOrgUsersForCurrentOrgLookup", "parameters": [ @@ -6313,9 +5879,7 @@ "/org/users/{user_id}": { "delete": { "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `org.users:remove` with scope `users:*`.", - "tags": [ - "org" - ], + "tags": ["org"], "summary": "Delete user in current organization", "operationId": "removeOrgUserForCurrentOrg", "parameters": [ @@ -6347,9 +5911,7 @@ }, "patch": { "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `org.users.role:update` with scope `users:*`.", - "tags": [ - "org" - ], + "tags": ["org"], "summary": "Updates the given user", "operationId": "updateOrgUserForCurrentOrg", "parameters": [ @@ -6396,9 +5958,7 @@ } ], "description": "Search all Organizations", - "tags": [ - "orgs" - ], + "tags": ["orgs"], "operationId": "searchOrgs", "parameters": [ { @@ -6448,9 +6008,7 @@ }, "post": { "description": "Only works if [users.allow_org_create](https://grafana.com/docs/grafana/latest/administration/configuration/#allow_org_create) is set.", - "tags": [ - "orgs" - ], + "tags": ["orgs"], "summary": "Create Organization.", "operationId": "createOrg", "parameters": [ @@ -6489,9 +6047,7 @@ "basic": [] } ], - "tags": [ - "orgs" - ], + "tags": ["orgs"], "summary": "Get Organization by ID.", "operationId": "getOrgByName", "parameters": [ @@ -6525,9 +6081,7 @@ "basic": [] } ], - "tags": [ - "orgs" - ], + "tags": ["orgs"], "summary": "Get Organization by ID.", "operationId": "getOrgByID", "parameters": [ @@ -6560,9 +6114,7 @@ "basic": [] } ], - "tags": [ - "orgs" - ], + "tags": ["orgs"], "summary": "Update Organization.", "operationId": "updateOrg", "parameters": [ @@ -6606,9 +6158,7 @@ "basic": [] } ], - "tags": [ - "orgs" - ], + "tags": ["orgs"], "summary": "Delete Organization.", "operationId": "deleteOrgByID", "parameters": [ @@ -6644,9 +6194,7 @@ }, "/orgs/{org_id}/address": { "put": { - "tags": [ - "orgs" - ], + "tags": ["orgs"], "summary": "Update Organization's address.", "operationId": "updateOrgAddress", "parameters": [ @@ -6688,9 +6236,7 @@ "/orgs/{org_id}/quotas": { "get": { "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `orgs.quotas:read` and scope `org:id:1` (orgIDScope).\nlist", - "tags": [ - "orgs" - ], + "tags": ["orgs"], "summary": "Fetch Organization quota.", "operationId": "getOrgQuota", "parameters": [ @@ -6729,9 +6275,7 @@ } ], "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `orgs.quotas:write` and scope `org:id:1` (orgIDScope).", - "tags": [ - "orgs" - ], + "tags": ["orgs"], "summary": "Update user quota.", "operationId": "updateOrgQuota", "parameters": [ @@ -6784,9 +6328,7 @@ } ], "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `org.users:read` with scope `users:*`.", - "tags": [ - "orgs" - ], + "tags": ["orgs"], "summary": "Get Users in Organization.", "operationId": "getOrgUsers", "parameters": [ @@ -6815,9 +6357,7 @@ }, "post": { "description": "Adds a global user to the current organization.\n\nIf you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `org.users:add` with scope `users:*`.", - "tags": [ - "orgs" - ], + "tags": ["orgs"], "summary": "Add a new user to the current organization", "operationId": "addOrgUser", "parameters": [ @@ -6856,9 +6396,7 @@ "/orgs/{org_id}/users/{user_id}": { "delete": { "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `org.users:remove` with scope `users:*`.", - "tags": [ - "orgs" - ], + "tags": ["orgs"], "summary": "Delete user in current organization", "operationId": "removeOrgUser", "parameters": [ @@ -6897,9 +6435,7 @@ }, "patch": { "description": "If you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `org.users.role:update` with scope `users:*`.", - "tags": [ - "orgs" - ], + "tags": ["orgs"], "summary": "Update Users in Organization.", "operationId": "updateOrgUser", "parameters": [ @@ -6947,9 +6483,7 @@ }, "/playlists": { "get": { - "tags": [ - "playlists" - ], + "tags": ["playlists"], "summary": "Get playlists.", "operationId": "searchPlaylists", "parameters": [ @@ -6976,9 +6510,7 @@ } }, "post": { - "tags": [ - "playlists" - ], + "tags": ["playlists"], "summary": "Create playlist.", "operationId": "createPlaylist", "parameters": [ @@ -7012,9 +6544,7 @@ }, "/playlists/{uid}": { "get": { - "tags": [ - "playlists" - ], + "tags": ["playlists"], "summary": "Get playlist.", "operationId": "getPlaylist", "parameters": [ @@ -7044,9 +6574,7 @@ } }, "put": { - "tags": [ - "playlists" - ], + "tags": ["playlists"], "summary": "Update playlist.", "operationId": "updatePlaylist", "parameters": [ @@ -7084,9 +6612,7 @@ } }, "delete": { - "tags": [ - "playlists" - ], + "tags": ["playlists"], "summary": "Delete playlist.", "operationId": "deletePlaylist", "parameters": [ @@ -7118,9 +6644,7 @@ }, "/playlists/{uid}/dashboards": { "get": { - "tags": [ - "playlists" - ], + "tags": ["playlists"], "summary": "Get playlist dashboards.", "operationId": "getPlaylistDashboards", "parameters": [ @@ -7152,9 +6676,7 @@ }, "/playlists/{uid}/items": { "get": { - "tags": [ - "playlists" - ], + "tags": ["playlists"], "summary": "Get playlist items.", "operationId": "getPlaylistItems", "parameters": [ @@ -7187,9 +6709,7 @@ "/query-history": { "get": { "description": "Returns a list of queries in the query history that matches the search criteria.\nQuery history search supports pagination. Use the `limit` parameter to control the maximum number of queries returned; the default limit is 100.\nYou can also use the `page` query parameter to fetch queries from any page other than the first one.", - "tags": [ - "query_history" - ], + "tags": ["query_history"], "summary": "Query history search.", "operationId": "searchQueries", "parameters": [ @@ -7216,10 +6736,7 @@ "in": "query" }, { - "enum": [ - "time-desc", - "time-asc" - ], + "enum": ["time-desc", "time-asc"], "type": "string", "default": "time-desc", "description": "Sort method", @@ -7269,9 +6786,7 @@ }, "post": { "description": "Adds new query to query history.", - "tags": [ - "query_history" - ], + "tags": ["query_history"], "summary": "Add query to query history.", "operationId": "createQuery", "parameters": [ @@ -7303,9 +6818,7 @@ "/query-history/migrate": { "post": { "description": "Adds multiple queries to query history.", - "tags": [ - "query_history" - ], + "tags": ["query_history"], "summary": "Migrate queries to query history.", "operationId": "migrateQueries", "parameters": [ @@ -7337,9 +6850,7 @@ "/query-history/star/{query_history_uid}": { "post": { "description": "Adds star to query in query history as specified by the UID.", - "tags": [ - "query_history" - ], + "tags": ["query_history"], "summary": "Add star to query in query history.", "operationId": "starQuery", "parameters": [ @@ -7364,9 +6875,7 @@ }, "delete": { "description": "Removes star from query in query history as specified by the UID.", - "tags": [ - "query_history" - ], + "tags": ["query_history"], "summary": "Remove star to query in query history.", "operationId": "unstarQuery", "parameters": [ @@ -7393,9 +6902,7 @@ "/query-history/{query_history_uid}": { "delete": { "description": "Deletes an existing query in query history as specified by the UID. This operation cannot be reverted.", - "tags": [ - "query_history" - ], + "tags": ["query_history"], "summary": "Delete query in query history.", "operationId": "deleteQuery", "parameters": [ @@ -7420,9 +6927,7 @@ }, "patch": { "description": "Updates comment for query in query history as specified by the UID.", - "tags": [ - "query_history" - ], + "tags": ["query_history"], "summary": "Update comment for query in query history.", "operationId": "patchQueryComment", "parameters": [ @@ -7460,10 +6965,7 @@ "/recording-rules": { "get": { "description": "Lists all rules in the database: active or deleted", - "tags": [ - "recording_rules", - "enterprise" - ], + "tags": ["recording_rules", "enterprise"], "operationId": "listRecordingRules", "responses": { "200": { @@ -7485,10 +6987,7 @@ }, "put": { "description": "Update the active status of a rule", - "tags": [ - "recording_rules", - "enterprise" - ], + "tags": ["recording_rules", "enterprise"], "operationId": "updateRecordingRule", "parameters": [ { @@ -7520,10 +7019,7 @@ }, "post": { "description": "Create a recording rule that is then registered and started", - "tags": [ - "recording_rules", - "enterprise" - ], + "tags": ["recording_rules", "enterprise"], "operationId": "createRecordingRule", "parameters": [ { @@ -7556,10 +7052,7 @@ }, "/recording-rules/test": { "post": { - "tags": [ - "recording_rules", - "enterprise" - ], + "tags": ["recording_rules", "enterprise"], "summary": "Test a recording rule.", "operationId": "testCreateRecordingRule", "parameters": [ @@ -7597,10 +7090,7 @@ "/recording-rules/writer": { "get": { "description": "Return the prometheus remote write target", - "tags": [ - "recording_rules", - "enterprise" - ], + "tags": ["recording_rules", "enterprise"], "operationId": "getRecordingRuleWriteTarget", "responses": { "200": { @@ -7622,10 +7112,7 @@ }, "post": { "description": "It returns a 422 if there is not an existing prometheus data source configured", - "tags": [ - "recording_rules", - "enterprise" - ], + "tags": ["recording_rules", "enterprise"], "summary": "Create a remote write target.", "operationId": "createRecordingRuleWriteTarget", "parameters": [ @@ -7660,10 +7147,7 @@ } }, "delete": { - "tags": [ - "recording_rules", - "enterprise" - ], + "tags": ["recording_rules", "enterprise"], "summary": "Delete the remote write target.", "operationId": "deleteRecordingRuleWriteTarget", "responses": { @@ -7687,10 +7171,7 @@ }, "/recording-rules/{recordingRuleID}": { "delete": { - "tags": [ - "recording_rules", - "enterprise" - ], + "tags": ["recording_rules", "enterprise"], "summary": "Delete removes the rule from the registry and stops it.", "operationId": "deleteRecordingRule", "parameters": [ @@ -7724,10 +7205,7 @@ "/reports": { "get": { "description": "Available to org admins only and with a valid or expired license\n\nYou need to have a permission with action `reports:read` with scope `reports:*`.", - "tags": [ - "reports", - "enterprise" - ], + "tags": ["reports", "enterprise"], "summary": "List reports.", "operationId": "getReports", "responses": { @@ -7747,10 +7225,7 @@ }, "post": { "description": "Available to org admins only and with a valid license.\n\nYou need to have a permission with action `reports.admin:create`.", - "tags": [ - "reports", - "enterprise" - ], + "tags": ["reports", "enterprise"], "summary": "Create a report.", "operationId": "createReport", "parameters": [ @@ -7788,10 +7263,7 @@ "/reports/email": { "post": { "description": "Generate and send a report. This API waits for the report to be generated before returning. We recommend that you set the client’s timeout to at least 60 seconds. Available to org admins only and with a valid license.\n\nOnly available in Grafana Enterprise v7.0+.\nThis API endpoint is experimental and may be deprecated in a future release. On deprecation, a migration strategy will be provided and the endpoint will remain functional until the next major release of Grafana.\n\nYou need to have a permission with action `reports:send`.", - "tags": [ - "reports", - "enterprise" - ], + "tags": ["reports", "enterprise"], "summary": "Send a report.", "operationId": "sendReport", "parameters": [ @@ -7829,13 +7301,8 @@ "/reports/render/pdf/{dashboardID}": { "get": { "description": "Please refer to [reports enterprise](#/reports/renderReportPDFs) instead. This will be removed in Grafana 10.", - "produces": [ - "application/pdf" - ], - "tags": [ - "reports", - "enterprise" - ], + "produces": ["application/pdf"], + "tags": ["reports", "enterprise"], "summary": "Render report for dashboard.", "operationId": "renderReportPDF", "deprecated": true, @@ -7904,13 +7371,8 @@ "/reports/render/pdfs": { "get": { "description": "Available to all users and with a valid license.", - "produces": [ - "application/pdf" - ], - "tags": [ - "reports", - "enterprise" - ], + "produces": ["application/pdf"], + "tags": ["reports", "enterprise"], "summary": "Render report for multiple dashboards.", "operationId": "renderReportPDFs", "parameters": [ @@ -7949,10 +7411,7 @@ "/reports/settings": { "get": { "description": "Available to org admins only and with a valid or expired license\n\nYou need to have a permission with action `reports.settings:read`x.", - "tags": [ - "reports", - "enterprise" - ], + "tags": ["reports", "enterprise"], "summary": "Get settings.", "operationId": "getReportSettings", "responses": { @@ -7972,10 +7431,7 @@ }, "post": { "description": "Available to org admins only and with a valid or expired license\n\nYou need to have a permission with action `reports.settings:write`xx.", - "tags": [ - "reports", - "enterprise" - ], + "tags": ["reports", "enterprise"], "summary": "Save settings.", "operationId": "saveReportSettings", "parameters": [ @@ -8010,10 +7466,7 @@ "/reports/test-email": { "post": { "description": "Available to org admins only and with a valid license.\n\nYou need to have a permission with action `reports:send`.", - "tags": [ - "reports", - "enterprise" - ], + "tags": ["reports", "enterprise"], "summary": "Send test report via email.", "operationId": "sendTestEmail", "parameters": [ @@ -8051,10 +7504,7 @@ "/reports/{id}": { "get": { "description": "Available to org admins only and with a valid or expired license\n\nYou need to have a permission with action `reports:read` with scope `reports:id:\u003creport ID\u003e`.", - "tags": [ - "reports", - "enterprise" - ], + "tags": ["reports", "enterprise"], "summary": "Get a report.", "operationId": "getReport", "parameters": [ @@ -8089,10 +7539,7 @@ }, "put": { "description": "Available to org admins only and with a valid or expired license\n\nYou need to have a permission with action `reports.admin:write` with scope `reports:id:\u003creport ID\u003e`.", - "tags": [ - "reports", - "enterprise" - ], + "tags": ["reports", "enterprise"], "summary": "Update a report.", "operationId": "updateReport", "parameters": [ @@ -8135,10 +7582,7 @@ }, "delete": { "description": "Available to org admins only and with a valid or expired license\n\nYou need to have a permission with action `reports.delete` with scope `reports:id:\u003creport ID\u003e`.", - "tags": [ - "reports", - "enterprise" - ], + "tags": ["reports", "enterprise"], "summary": "Delete a report.", "operationId": "deleteReport", "parameters": [ @@ -8174,10 +7618,7 @@ }, "/saml/acs": { "post": { - "tags": [ - "saml", - "enterprise" - ], + "tags": ["saml", "enterprise"], "summary": "It performs assertion Consumer Service (ACS).", "operationId": "postACS", "parameters": [ @@ -8202,13 +7643,8 @@ }, "/saml/metadata": { "get": { - "produces": [ - "application/xml;application/samlmetadata+xml" - ], - "tags": [ - "saml", - "enterprise" - ], + "produces": ["application/xml;application/samlmetadata+xml"], + "tags": ["saml", "enterprise"], "summary": "It exposes the SP (Grafana's) metadata for the IdP's consumption.", "operationId": "getMetadata", "responses": { @@ -8221,10 +7657,7 @@ "/saml/slo": { "post": { "description": "There might be two possible requests:\n1. Logout response (callback) when Grafana initiates single logout and IdP returns response to logout request.\n2. Logout request when another SP initiates single logout and IdP sends logout request to the Grafana,\nor in case of IdP-initiated logout.", - "tags": [ - "saml", - "enterprise" - ], + "tags": ["saml", "enterprise"], "summary": "It performs Single Logout (SLO) callback.", "operationId": "postSLO", "parameters": [ @@ -8257,9 +7690,7 @@ }, "/search": { "get": { - "tags": [ - "search" - ], + "tags": ["search"], "operationId": "search", "parameters": [ { @@ -8279,10 +7710,7 @@ "in": "query" }, { - "enum": [ - "dash-folder", - "dash-db" - ], + "enum": ["dash-folder", "dash-db"], "type": "string", "description": "Type to search for, dash-folder or dash-db", "name": "type", @@ -8338,10 +7766,7 @@ "in": "query" }, { - "enum": [ - "Edit", - "View" - ], + "enum": ["Edit", "View"], "type": "string", "default": "View", "description": "Set to `Edit` to return dashboards/folders that the user can edit", @@ -8349,10 +7774,7 @@ "in": "query" }, { - "enum": [ - "alpha-asc", - "alpha-desc" - ], + "enum": ["alpha-asc", "alpha-desc"], "type": "string", "default": "alpha-asc", "description": "Sort method; for listing all the possible sort methods use the search sorting endpoint.", @@ -8379,9 +7801,7 @@ "/search/sorting": { "get": { "description": "List search sorting options", - "tags": [ - "search" - ], + "tags": ["search"], "operationId": "listSortOptions", "responses": { "200": { @@ -8396,9 +7816,7 @@ "/serviceaccounts": { "post": { "description": "Required permissions (See note in the [introduction](https://grafana.com/docs/grafana/latest/developers/http_api/serviceaccount/#service-account-api) for an explanation):\naction: `serviceaccounts:write` scope: `serviceaccounts:*`\n\nRequires basic authentication and that the authenticated user is a Grafana Admin.", - "tags": [ - "service_accounts" - ], + "tags": ["service_accounts"], "summary": "Create service account", "operationId": "createServiceAccount", "parameters": [ @@ -8432,9 +7850,7 @@ "/serviceaccounts/search": { "get": { "description": "Required permissions (See note in the [introduction](https://grafana.com/docs/grafana/latest/developers/http_api/serviceaccount/#service-account-api) for an explanation):\naction: `serviceaccounts:read` scope: `serviceaccounts:*`", - "tags": [ - "service_accounts" - ], + "tags": ["service_accounts"], "summary": "Search service accounts with paging", "operationId": "searchOrgServiceAccountsWithPaging", "parameters": [ @@ -8488,9 +7904,7 @@ "/serviceaccounts/{serviceAccountId}": { "get": { "description": "Required permissions (See note in the [introduction](https://grafana.com/docs/grafana/latest/developers/http_api/serviceaccount/#service-account-api) for an explanation):\naction: `serviceaccounts:read` scope: `serviceaccounts:id:1` (single service account)", - "tags": [ - "service_accounts" - ], + "tags": ["service_accounts"], "summary": "Get single serviceaccount by Id", "operationId": "retrieveServiceAccount", "parameters": [ @@ -8525,9 +7939,7 @@ }, "delete": { "description": "Required permissions (See note in the [introduction](https://grafana.com/docs/grafana/latest/developers/http_api/serviceaccount/#service-account-api) for an explanation):\naction: `serviceaccounts:delete` scope: `serviceaccounts:id:1` (single service account)", - "tags": [ - "service_accounts" - ], + "tags": ["service_accounts"], "summary": "Delete service account", "operationId": "deleteServiceAccount", "parameters": [ @@ -8559,9 +7971,7 @@ }, "patch": { "description": "Required permissions (See note in the [introduction](https://grafana.com/docs/grafana/latest/developers/http_api/serviceaccount/#service-account-api) for an explanation):\naction: `serviceaccounts:write` scope: `serviceaccounts:id:1` (single service account)", - "tags": [ - "service_accounts" - ], + "tags": ["service_accounts"], "summary": "Update service account", "operationId": "updateServiceAccount", "parameters": [ @@ -8605,9 +8015,7 @@ "/serviceaccounts/{serviceAccountId}/tokens": { "get": { "description": "Required permissions (See note in the [introduction](https://grafana.com/docs/grafana/latest/developers/http_api/serviceaccount/#service-account-api) for an explanation):\naction: `serviceaccounts:read` scope: `global:serviceaccounts:id:1` (single service account)\n\nRequires basic authentication and that the authenticated user is a Grafana Admin.", - "tags": [ - "service_accounts" - ], + "tags": ["service_accounts"], "summary": "Get service account tokens", "operationId": "listTokens", "parameters": [ @@ -8639,9 +8047,7 @@ }, "post": { "description": "Required permissions (See note in the [introduction](https://grafana.com/docs/grafana/latest/developers/http_api/serviceaccount/#service-account-api) for an explanation):\naction: `serviceaccounts:write` scope: `serviceaccounts:id:1` (single service account)", - "tags": [ - "service_accounts" - ], + "tags": ["service_accounts"], "summary": "CreateNewToken adds a token to a service account", "operationId": "createToken", "parameters": [ @@ -8688,9 +8094,7 @@ "/serviceaccounts/{serviceAccountId}/tokens/{tokenId}": { "delete": { "description": "Required permissions (See note in the [introduction](https://grafana.com/docs/grafana/latest/developers/http_api/serviceaccount/#service-account-api) for an explanation):\naction: `serviceaccounts:write` scope: `serviceaccounts:id:1` (single service account)\n\nRequires basic authentication and that the authenticated user is a Grafana Admin.", - "tags": [ - "service_accounts" - ], + "tags": ["service_accounts"], "summary": "DeleteToken deletes service account tokens", "operationId": "deleteToken", "parameters": [ @@ -8733,9 +8137,7 @@ }, "/snapshot/shared-options": { "get": { - "tags": [ - "snapshots" - ], + "tags": ["snapshots"], "summary": "Get snapshot sharing settings.", "operationId": "getSharingOptions", "responses": { @@ -8751,9 +8153,7 @@ "/snapshots": { "post": { "description": "Snapshot public mode should be enabled or authentication is required.", - "tags": [ - "snapshots" - ], + "tags": ["snapshots"], "summary": "When creating a snapshot using the API, you have to provide the full dashboard payload including the snapshot data. This endpoint is designed for the Grafana UI.", "operationId": "createDashboardSnapshot", "parameters": [ @@ -8785,9 +8185,7 @@ "/snapshots-delete/{deleteKey}": { "get": { "description": "Snapshot public mode should be enabled or authentication is required.", - "tags": [ - "snapshots" - ], + "tags": ["snapshots"], "summary": "Delete Snapshot by deleteKey.", "operationId": "deleteDashboardSnapshotByDeleteKey", "parameters": [ @@ -8819,9 +8217,7 @@ }, "/snapshots/{key}": { "get": { - "tags": [ - "snapshots" - ], + "tags": ["snapshots"], "summary": "Get Snapshot by Key.", "operationId": "getDashboardSnapshot", "parameters": [ @@ -8845,9 +8241,7 @@ } }, "delete": { - "tags": [ - "snapshots" - ], + "tags": ["snapshots"], "summary": "Delete Snapshot by Key.", "operationId": "deleteDashboardSnapshot", "parameters": [ @@ -8876,9 +8270,7 @@ }, "/teams": { "post": { - "tags": [ - "teams" - ], + "tags": ["teams"], "summary": "Add Team.", "operationId": "createTeam", "parameters": [ @@ -8912,9 +8304,7 @@ }, "/teams/search": { "get": { - "tags": [ - "teams" - ], + "tags": ["teams"], "summary": "Team Search With Paging.", "operationId": "searchTeams", "parameters": [ @@ -8963,10 +8353,7 @@ }, "/teams/{teamId}/groups": { "get": { - "tags": [ - "sync_team_groups", - "enterprise" - ], + "tags": ["sync_team_groups", "enterprise"], "summary": "Get External Groups.", "operationId": "getTeamGroupsApi", "parameters": [ @@ -9000,10 +8387,7 @@ } }, "post": { - "tags": [ - "sync_team_groups", - "enterprise" - ], + "tags": ["sync_team_groups", "enterprise"], "summary": "Add External Group.", "operationId": "addTeamGroupApi", "parameters": [ @@ -9047,10 +8431,7 @@ }, "/teams/{teamId}/groups/{groupId}": { "delete": { - "tags": [ - "sync_team_groups", - "enterprise" - ], + "tags": ["sync_team_groups", "enterprise"], "summary": "Remove External Group.", "operationId": "removeTeamGroupApi", "parameters": [ @@ -9092,9 +8473,7 @@ }, "/teams/{team_id}": { "get": { - "tags": [ - "teams" - ], + "tags": ["teams"], "summary": "Get Team By ID.", "operationId": "getTeamByID", "parameters": [ @@ -9124,9 +8503,7 @@ } }, "put": { - "tags": [ - "teams" - ], + "tags": ["teams"], "summary": "Update Team.", "operationId": "updateTeam", "parameters": [ @@ -9167,9 +8544,7 @@ } }, "delete": { - "tags": [ - "teams" - ], + "tags": ["teams"], "summary": "Delete Team By ID.", "operationId": "deleteTeamByID", "parameters": [ @@ -9201,9 +8576,7 @@ }, "/teams/{team_id}/members": { "get": { - "tags": [ - "teams" - ], + "tags": ["teams"], "summary": "Get Team Members.", "operationId": "getTeamMembers", "parameters": [ @@ -9233,9 +8606,7 @@ } }, "post": { - "tags": [ - "teams" - ], + "tags": ["teams"], "summary": "Add Team Member.", "operationId": "addTeamMember", "parameters": [ @@ -9275,9 +8646,7 @@ }, "/teams/{team_id}/members/{user_id}": { "put": { - "tags": [ - "teams" - ], + "tags": ["teams"], "summary": "Update Team Member.", "operationId": "updateTeamMember", "parameters": [ @@ -9322,9 +8691,7 @@ } }, "delete": { - "tags": [ - "teams" - ], + "tags": ["teams"], "summary": "Remove Member From Team.", "operationId": "removeTeamMember", "parameters": [ @@ -9363,9 +8730,7 @@ }, "/teams/{team_id}/preferences": { "get": { - "tags": [ - "teams" - ], + "tags": ["teams"], "summary": "Get Team Preferences.", "operationId": "getTeamPreferences", "parameters": [ @@ -9389,9 +8754,7 @@ } }, "put": { - "tags": [ - "teams" - ], + "tags": ["teams"], "summary": "Update Team Preferences.", "operationId": "updateTeamPreferences", "parameters": [ @@ -9429,9 +8792,7 @@ "/user": { "get": { "description": "Get (current authenticated user)", - "tags": [ - "signed_in_user" - ], + "tags": ["signed_in_user"], "operationId": "getSignedInUser", "responses": { "200": { @@ -9452,9 +8813,7 @@ } }, "put": { - "tags": [ - "signed_in_user" - ], + "tags": ["signed_in_user"], "summary": "Update signed in User.", "operationId": "updateSignedInUser", "parameters": [ @@ -9487,9 +8846,7 @@ "/user/auth-tokens": { "get": { "description": "Return a list of all auth tokens (devices) that the actual user currently have logged in from.", - "tags": [ - "signed_in_user" - ], + "tags": ["signed_in_user"], "summary": "Auth tokens of the actual User.", "operationId": "getUserAuthTokens", "responses": { @@ -9510,9 +8867,7 @@ }, "/user/helpflags/clear": { "get": { - "tags": [ - "signed_in_user" - ], + "tags": ["signed_in_user"], "summary": "Clear user help flag.", "operationId": "clearHelpFlags", "responses": { @@ -9533,9 +8888,7 @@ }, "/user/helpflags/{flag_id}": { "put": { - "tags": [ - "signed_in_user" - ], + "tags": ["signed_in_user"], "summary": "Set user help flag.", "operationId": "setHelpFlag", "parameters": [ @@ -9570,9 +8923,7 @@ } ], "description": "Return a list of all organizations of the current user.", - "tags": [ - "signed_in_user" - ], + "tags": ["signed_in_user"], "summary": "Organizations of the actual User.", "operationId": "getSignedInUserOrgList", "responses": { @@ -9599,9 +8950,7 @@ } ], "description": "Changes the password for the user.", - "tags": [ - "signed_in_user" - ], + "tags": ["signed_in_user"], "summary": "Change Password.", "operationId": "changeUserPassword", "parameters": [ @@ -9636,9 +8985,7 @@ }, "/user/preferences": { "get": { - "tags": [ - "user_preferences" - ], + "tags": ["user_preferences"], "summary": "Get user preferences.", "operationId": "getUserPreferences", "responses": { @@ -9655,9 +9002,7 @@ }, "put": { "description": "Omitting a key (`theme`, `homeDashboardId`, `timezone`) will cause the current value to be replaced with the system default value.", - "tags": [ - "user_preferences" - ], + "tags": ["user_preferences"], "summary": "Update user preferences.", "operationId": "updateUserPreferences", "parameters": [ @@ -9686,9 +9031,7 @@ } }, "patch": { - "tags": [ - "user_preferences" - ], + "tags": ["user_preferences"], "summary": "Patch user preferences.", "operationId": "patchUserPreferences", "parameters": [ @@ -9719,9 +9062,7 @@ }, "/user/quotas": { "get": { - "tags": [ - "signed_in_user" - ], + "tags": ["signed_in_user"], "summary": "Fetch user quota.", "operationId": "getUserQuotas", "responses": { @@ -9746,9 +9087,7 @@ "/user/revoke-auth-token": { "post": { "description": "Revokes the given auth token (device) for the actual user. User of issued auth token (device) will no longer be logged in and will be required to authenticate again upon next activity.", - "tags": [ - "signed_in_user" - ], + "tags": ["signed_in_user"], "summary": "Revoke an auth token of the actual User.", "operationId": "revokeUserAuthToken", "parameters": [ @@ -9783,9 +9122,7 @@ "/user/stars/dashboard/{dashboard_id}": { "post": { "description": "Stars the given Dashboard for the actual user.", - "tags": [ - "signed_in_user" - ], + "tags": ["signed_in_user"], "summary": "Star a dashboard.", "operationId": "starDashboard", "parameters": [ @@ -9816,9 +9153,7 @@ }, "delete": { "description": "Deletes the starring of the given Dashboard for the actual user.", - "tags": [ - "signed_in_user" - ], + "tags": ["signed_in_user"], "summary": "Unstar a dashboard.", "operationId": "unstarDashboard", "parameters": [ @@ -9851,9 +9186,7 @@ "/user/teams": { "get": { "description": "Return a list of all teams that the current user is member of.", - "tags": [ - "signed_in_user" - ], + "tags": ["signed_in_user"], "summary": "Teams that the actual User is member of.", "operationId": "getSignedInUserTeamList", "responses": { @@ -9875,9 +9208,7 @@ "/user/using/{org_id}": { "post": { "description": "Switch user context to the given organization.", - "tags": [ - "signed_in_user" - ], + "tags": ["signed_in_user"], "summary": "Switch user context for signed in user.", "operationId": "userSetUsingOrg", "parameters": [ @@ -9911,9 +9242,7 @@ "/users": { "get": { "description": "Returns all users that the authenticated user has permission to view, admin permission required.", - "tags": [ - "users" - ], + "tags": ["users"], "summary": "Get users.", "operationId": "searchUsers", "parameters": [ @@ -9952,9 +9281,7 @@ }, "/users/lookup": { "get": { - "tags": [ - "users" - ], + "tags": ["users"], "summary": "Get user by login or email.", "operationId": "getUserByLoginOrEmail", "parameters": [ @@ -9987,9 +9314,7 @@ }, "/users/search": { "get": { - "tags": [ - "users" - ], + "tags": ["users"], "summary": "Get users with paging.", "operationId": "searchUsersWithPaging", "responses": { @@ -10013,9 +9338,7 @@ }, "/users/{user_id}": { "get": { - "tags": [ - "users" - ], + "tags": ["users"], "summary": "Get user by id.", "operationId": "getUserByID", "parameters": [ @@ -10047,9 +9370,7 @@ }, "put": { "description": "Update the user identified by id.", - "tags": [ - "users" - ], + "tags": ["users"], "summary": "Update user.", "operationId": "updateUser", "parameters": [ @@ -10092,9 +9413,7 @@ "/users/{user_id}/orgs": { "get": { "description": "Get organizations for user identified by id.", - "tags": [ - "users" - ], + "tags": ["users"], "summary": "Get organizations for user.", "operationId": "getUserOrgList", "parameters": [ @@ -10128,9 +9447,7 @@ "/users/{user_id}/teams": { "get": { "description": "Get teams for user identified by id.", - "tags": [ - "users" - ], + "tags": ["users"], "summary": "Get teams for user.", "operationId": "getUserTeams", "parameters": [ @@ -10211,11 +9528,7 @@ }, "role": { "type": "string", - "enum": [ - "Viewer", - "Editor", - "Admin" - ] + "enum": ["Viewer", "Editor", "Admin"] }, "secondsToLive": { "type": "integer", @@ -10228,12 +9541,7 @@ "properties": { "builtInRole": { "type": "string", - "enum": [ - "Viewer", - " Editor", - " Admin", - " Grafana Admin" - ] + "enum": ["Viewer", " Editor", " Admin", " Grafana Admin"] }, "global": { "description": "A flag indicating if the assignment is global or not. If set to false, the default org ID of the authenticated user will be used from the request to create organization local assignment. Refer to the Built-in role assignments for more information.", @@ -10303,11 +9611,7 @@ }, "role": { "type": "string", - "enum": [ - "Viewer", - "Editor", - "Admin" - ] + "enum": ["Viewer", "Editor", "Admin"] }, "sendEmail": { "type": "boolean" @@ -10322,11 +9626,7 @@ }, "role": { "type": "string", - "enum": [ - "Viewer", - "Editor", - "Admin" - ] + "enum": ["Viewer", "Editor", "Admin"] } } }, @@ -10549,12 +9849,7 @@ "Alert": { "type": "object", "title": "Alert has info for an alert.", - "required": [ - "labels", - "annotations", - "state", - "value" - ], + "required": ["labels", "annotations", "state", "value"], "properties": { "activeAt": { "type": "string", @@ -10577,9 +9872,7 @@ "AlertDiscovery": { "type": "object", "title": "AlertDiscovery has info for all active alerts.", - "required": [ - "alerts" - ], + "required": ["alerts"], "properties": { "alerts": { "type": "array", @@ -10776,9 +10069,7 @@ }, "AlertResponse": { "type": "object", - "required": [ - "status" - ], + "required": ["status"], "properties": { "data": { "$ref": "#/definitions/AlertDiscovery" @@ -10818,11 +10109,7 @@ }, "ExecErrState": { "type": "string", - "enum": [ - "Alerting", - "Error", - "OK" - ] + "enum": ["Alerting", "Error", "OK"] }, "For": { "$ref": "#/definitions/Duration" @@ -10846,11 +10133,7 @@ }, "NoDataState": { "type": "string", - "enum": [ - "Alerting", - "NoData", - "OK" - ] + "enum": ["Alerting", "NoData", "OK"] }, "OrgID": { "type": "integer", @@ -10998,15 +10281,7 @@ "AlertingRule": { "description": "adapted from cortex", "type": "object", - "required": [ - "name", - "query", - "health", - "type", - "state", - "annotations", - "alerts" - ], + "required": ["name", "query", "health", "type", "state", "annotations", "alerts"], "properties": { "alerts": { "type": "array", @@ -11097,11 +10372,7 @@ }, "role": { "type": "string", - "enum": [ - "Viewer", - "Editor", - "Admin" - ] + "enum": ["Viewer", "Editor", "Admin"] } } }, @@ -11432,9 +10703,7 @@ }, "CreateDashboardSnapshotCommand": { "type": "object", - "required": [ - "dashboard" - ], + "required": ["dashboard"], "properties": { "Result": { "$ref": "#/definitions/DashboardSnapshot" @@ -11495,10 +10764,7 @@ "description": "Kind of element to create, Use 1 for library panels or 2 for c.\nDescription:\n1 - library panels\n2 - library variables", "type": "integer", "format": "int64", - "enum": [ - 1, - 2 - ] + "enum": [1, 2] }, "model": { "description": "The JSON model for the library element.", @@ -11595,9 +10861,7 @@ "CreateQueryInQueryHistoryCommand": { "description": "CreateQueryInQueryHistoryCommand is the command for adding query history", "type": "object", - "required": [ - "queries" - ], + "required": ["queries"], "properties": { "datasourceUid": { "description": "UID of the data source for which are queries stored.", @@ -11658,11 +10922,7 @@ }, "role": { "type": "string", - "enum": [ - "Viewer", - "Editor", - "Admin" - ], + "enum": ["Viewer", "Editor", "Admin"], "example": "Admin" } } @@ -11754,11 +11014,7 @@ }, "role": { "type": "string", - "enum": [ - "Viewer", - "Editor", - "Admin" - ] + "enum": ["Viewer", "Editor", "Admin"] }, "slug": { "type": "string" @@ -11812,11 +11068,7 @@ }, "role": { "type": "string", - "enum": [ - "Viewer", - "Editor", - "Admin" - ] + "enum": ["Viewer", "Editor", "Admin"] }, "teamId": { "type": "integer", @@ -12436,9 +11688,7 @@ }, "DiscoveryBase": { "type": "object", - "required": [ - "status" - ], + "required": ["status"], "properties": { "error": { "type": "string" @@ -12519,10 +11769,7 @@ "EmbeddedContactPoint": { "description": "EmbeddedContactPoint is the contact point type that is used\nby grafanas embedded alertmanager implementation.", "type": "object", - "required": [ - "type", - "settings" - ], + "required": ["type", "settings"], "properties": { "disableResolveMessage": { "type": "boolean", @@ -12573,9 +11820,7 @@ }, "ErrorResponseBody": { "type": "object", - "required": [ - "message" - ], + "required": ["message"], "properties": { "error": { "description": "Error An optional detailed description of the actual error. Only included if running in developer mode.", @@ -13224,11 +12469,7 @@ }, "exec_err_state": { "type": "string", - "enum": [ - "OK", - "Alerting", - "Error" - ] + "enum": ["OK", "Alerting", "Error"] }, "id": { "type": "integer", @@ -13247,11 +12488,7 @@ }, "no_data_state": { "type": "string", - "enum": [ - "Alerting", - "NoData", - "OK" - ] + "enum": ["Alerting", "NoData", "OK"] }, "orgId": { "type": "integer", @@ -13290,11 +12527,7 @@ }, "alertmanagersChoice": { "type": "string", - "enum": [ - "all", - "internal", - "external" - ] + "enum": ["all", "internal", "external"] } } }, @@ -13323,12 +12556,7 @@ }, "GettableStatus": { "type": "object", - "required": [ - "cluster", - "config", - "uptime", - "versionInfo" - ], + "required": ["cluster", "config", "uptime", "versionInfo"], "properties": { "cluster": { "$ref": "#/definitions/clusterStatus" @@ -14160,11 +13388,7 @@ }, "MetricRequest": { "type": "object", - "required": [ - "from", - "to", - "queries" - ], + "required": ["from", "to", "queries"], "properties": { "debug": { "type": "boolean" @@ -14693,10 +13917,7 @@ "description": "Kind of element to create, Use 1 for library panels or 2 for c.\nDescription:\n1 - library panels\n2 - library variables", "type": "integer", "format": "int64", - "enum": [ - 1, - 2 - ] + "enum": [1, 2] }, "model": { "description": "The JSON model for the library element.", @@ -14739,17 +13960,11 @@ }, "theme": { "type": "string", - "enum": [ - "light", - "dark" - ] + "enum": ["light", "dark"] }, "timezone": { "type": "string", - "enum": [ - "utc", - "browser" - ] + "enum": ["utc", "browser"] }, "weekStart": { "type": "string" @@ -15175,19 +14390,11 @@ }, "exec_err_state": { "type": "string", - "enum": [ - "OK", - "Alerting", - "Error" - ] + "enum": ["OK", "Alerting", "Error"] }, "no_data_state": { "type": "string", - "enum": [ - "Alerting", - "NoData", - "OK" - ] + "enum": ["Alerting", "NoData", "OK"] }, "title": { "type": "string" @@ -15208,11 +14415,7 @@ }, "alertmanagersChoice": { "type": "string", - "enum": [ - "all", - "internal", - "external" - ] + "enum": ["all", "internal", "external"] } } }, @@ -15333,10 +14536,7 @@ "conditions": [ { "evaluator": { - "params": [ - 0, - 0 - ], + "params": [0, 0], "type": "gt" }, "operator": { @@ -15374,11 +14574,7 @@ }, "execErrState": { "type": "string", - "enum": [ - "Alerting", - "Error", - "OK" - ] + "enum": ["Alerting", "Error", "OK"] }, "folderUID": { "type": "string", @@ -15402,11 +14598,7 @@ }, "noDataState": { "type": "string", - "enum": [ - "Alerting", - "NoData", - "OK" - ] + "enum": ["Alerting", "NoData", "OK"] }, "orgID": { "type": "integer", @@ -16004,12 +15196,7 @@ "Rule": { "description": "adapted from cortex", "type": "object", - "required": [ - "name", - "query", - "health", - "type" - ], + "required": ["name", "query", "health", "type"], "properties": { "evaluationTime": { "type": "number", @@ -16041,9 +15228,7 @@ }, "RuleDiscovery": { "type": "object", - "required": [ - "groups" - ], + "required": ["groups"], "properties": { "groups": { "type": "array", @@ -16055,12 +15240,7 @@ }, "RuleGroup": { "type": "object", - "required": [ - "name", - "file", - "rules", - "interval" - ], + "required": ["name", "file", "rules", "interval"], "properties": { "evaluationTime": { "type": "number", @@ -16114,9 +15294,7 @@ }, "RuleResponse": { "type": "object", - "required": [ - "status" - ], + "required": ["status"], "properties": { "data": { "$ref": "#/definitions/RuleDiscovery" @@ -16911,11 +16089,7 @@ }, "role": { "type": "string", - "enum": [ - "Viewer", - "Editor", - "Admin" - ] + "enum": ["Viewer", "Editor", "Admin"] }, "status": { "$ref": "#/definitions/TempUserStatus" @@ -17542,11 +16716,7 @@ "properties": { "role": { "type": "string", - "enum": [ - "Viewer", - "Editor", - "Admin" - ] + "enum": ["Viewer", "Editor", "Admin"] } } }, @@ -17593,17 +16763,11 @@ }, "theme": { "type": "string", - "enum": [ - "light", - "dark" - ] + "enum": ["light", "dark"] }, "timezone": { "type": "string", - "enum": [ - "utc", - "browser" - ] + "enum": ["utc", "browser"] }, "weekStart": { "type": "string" @@ -17654,11 +16818,7 @@ }, "role": { "type": "string", - "enum": [ - "Viewer", - "Editor", - "Admin" - ] + "enum": ["Viewer", "Editor", "Admin"] } } }, @@ -17753,11 +16913,7 @@ }, "role": { "type": "string", - "enum": [ - "Viewer", - "Editor", - "Admin" - ] + "enum": ["Viewer", "Editor", "Admin"] } } }, @@ -18096,9 +17252,7 @@ "alert": { "description": "Alert alert", "type": "object", - "required": [ - "labels" - ], + "required": ["labels"], "properties": { "generatorURL": { "description": "generator URL\nFormat: uri", @@ -18113,11 +17267,7 @@ "alertGroup": { "description": "AlertGroup alert group", "type": "object", - "required": [ - "alerts", - "labels", - "receiver" - ], + "required": ["alerts", "labels", "receiver"], "properties": { "alerts": { "description": "alerts", @@ -18143,11 +17293,7 @@ "alertStatus": { "description": "AlertStatus alert status", "type": "object", - "required": [ - "inhibitedBy", - "silencedBy", - "state" - ], + "required": ["inhibitedBy", "silencedBy", "state"], "properties": { "inhibitedBy": { "description": "inhibited by", @@ -18166,18 +17312,14 @@ "state": { "description": "state", "type": "string", - "enum": [ - "[unprocessed active suppressed]" - ] + "enum": ["[unprocessed active suppressed]"] } } }, "alertmanagerConfig": { "description": "AlertmanagerConfig alertmanager config", "type": "object", - "required": [ - "original" - ], + "required": ["original"], "properties": { "original": { "description": "original", @@ -18188,12 +17330,7 @@ "alertmanagerStatus": { "description": "AlertmanagerStatus alertmanager status", "type": "object", - "required": [ - "cluster", - "config", - "uptime", - "versionInfo" - ], + "required": ["cluster", "config", "uptime", "versionInfo"], "properties": { "cluster": { "$ref": "#/definitions/clusterStatus" @@ -18214,9 +17351,7 @@ "clusterStatus": { "description": "ClusterStatus cluster status", "type": "object", - "required": [ - "status" - ], + "required": ["status"], "properties": { "name": { "description": "name", @@ -18232,9 +17367,7 @@ "status": { "description": "status", "type": "string", - "enum": [ - "[ready settling disabled]" - ] + "enum": ["[ready settling disabled]"] } } }, @@ -18244,16 +17377,7 @@ "gettableAlert": { "description": "GettableAlert gettable alert", "type": "object", - "required": [ - "labels", - "annotations", - "endsAt", - "fingerprint", - "receivers", - "startsAt", - "status", - "updatedAt" - ], + "required": ["labels", "annotations", "endsAt", "fingerprint", "receivers", "startsAt", "status", "updatedAt"], "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -18306,16 +17430,7 @@ }, "gettableSilence": { "type": "object", - "required": [ - "comment", - "createdBy", - "endsAt", - "matchers", - "startsAt", - "id", - "status", - "updatedAt" - ], + "required": ["comment", "createdBy", "endsAt", "matchers", "startsAt", "id", "status", "updatedAt"], "properties": { "comment": { "description": "comment", @@ -18369,11 +17484,7 @@ "matcher": { "description": "Matcher matcher", "type": "object", - "required": [ - "isRegex", - "name", - "value" - ], + "required": ["isRegex", "name", "value"], "properties": { "isEqual": { "description": "is equal", @@ -18411,10 +17522,7 @@ "peerStatus": { "description": "PeerStatus peer status", "type": "object", - "required": [ - "address", - "name" - ], + "required": ["address", "name"], "properties": { "address": { "description": "address", @@ -18429,9 +17537,7 @@ "postableAlert": { "description": "PostableAlert postable alert", "type": "object", - "required": [ - "labels" - ], + "required": ["labels"], "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -18466,13 +17572,7 @@ "postableSilence": { "description": "PostableSilence postable silence", "type": "object", - "required": [ - "comment", - "createdBy", - "endsAt", - "matchers", - "startsAt" - ], + "required": ["comment", "createdBy", "endsAt", "matchers", "startsAt"], "properties": { "comment": { "description": "comment", @@ -18503,9 +17603,7 @@ }, "receiver": { "type": "object", - "required": [ - "name" - ], + "required": ["name"], "properties": { "name": { "description": "name", @@ -18516,13 +17614,7 @@ "silence": { "description": "Silence silence", "type": "object", - "required": [ - "comment", - "createdBy", - "endsAt", - "matchers", - "startsAt" - ], + "required": ["comment", "createdBy", "endsAt", "matchers", "startsAt"], "properties": { "comment": { "description": "comment", @@ -18550,30 +17642,19 @@ "silenceStatus": { "description": "SilenceStatus silence status", "type": "object", - "required": [ - "state" - ], + "required": ["state"], "properties": { "state": { "description": "state", "type": "string", - "enum": [ - "[expired active pending]" - ] + "enum": ["[expired active pending]"] } } }, "versionInfo": { "description": "VersionInfo version info", "type": "object", - "required": [ - "branch", - "buildDate", - "buildUser", - "goVersion", - "revision", - "version" - ], + "required": ["branch", "buildDate", "buildUser", "goVersion", "revision", "version"], "properties": { "branch": { "description": "branch", @@ -18723,12 +17804,7 @@ "description": "(empty)", "schema": { "type": "object", - "required": [ - "id", - "name", - "message", - "datasource" - ], + "required": ["id", "name", "message", "datasource"], "properties": { "datasource": { "$ref": "#/definitions/DataSource" @@ -18756,10 +17832,7 @@ "description": "(empty)", "schema": { "type": "object", - "required": [ - "orgId", - "message" - ], + "required": ["orgId", "message"], "properties": { "message": { "description": "Message Message of the created org.", @@ -18854,10 +17927,7 @@ "description": "(empty)", "schema": { "type": "object", - "required": [ - "id", - "message" - ], + "required": ["id", "message"], "properties": { "id": { "description": "ID Identifier of the deleted notification channel.", @@ -18882,11 +17952,7 @@ "description": "(empty)", "schema": { "type": "object", - "required": [ - "id", - "title", - "message" - ], + "required": ["id", "title", "message"], "properties": { "id": { "description": "ID Identifier of the deleted dashboard.", @@ -18911,10 +17977,7 @@ "description": "(empty)", "schema": { "type": "object", - "required": [ - "id", - "message" - ], + "required": ["id", "message"], "properties": { "id": { "description": "ID Identifier of the deleted data source.", @@ -18934,11 +17997,7 @@ "description": "(empty)", "schema": { "type": "object", - "required": [ - "id", - "title", - "message" - ], + "required": ["id", "title", "message"], "properties": { "id": { "description": "ID Identifier of the deleted folder.", @@ -19116,9 +18175,7 @@ "description": "(empty)", "schema": { "type": "object", - "required": [ - "id" - ], + "required": ["id"], "properties": { "id": { "description": "ID Identifier of the data source.", @@ -19518,10 +18575,7 @@ "description": "(empty)", "schema": { "type": "object", - "required": [ - "alertId", - "message" - ], + "required": ["alertId", "message"], "properties": { "alertId": { "type": "integer", @@ -19541,10 +18595,7 @@ "description": "(empty)", "schema": { "type": "object", - "required": [ - "alertsAffected", - "message" - ], + "required": ["alertsAffected", "message"], "properties": { "alertsAffected": { "description": "AlertsAffected is the number of the affected alerts.", @@ -19571,10 +18622,7 @@ "description": "(empty)", "schema": { "type": "object", - "required": [ - "id", - "message" - ], + "required": ["id", "message"], "properties": { "id": { "description": "ID Identifier of the created annotation.", @@ -19593,14 +18641,7 @@ "description": "(empty)", "schema": { "type": "object", - "required": [ - "status", - "title", - "version", - "id", - "uid", - "url" - ], + "required": ["status", "title", "version", "id", "uid", "url"], "properties": { "id": { "description": "ID The unique identifier (id) of the created/updated dashboard.", @@ -19889,4 +18930,4 @@ "name": "service_accounts" } ] -} \ No newline at end of file +}