merge with main
This commit is contained in:
@@ -12,6 +12,8 @@ Developers modifying the HTTP API endpoints need to make sure to add the necessa
|
||||
|
||||
The following route defines a `PATCH` endpoint under the `/serviceaccounts/{serviceAccountId}` path with tag `service_accounts` (used for grouping together several routes) and operation ID `updateServiceAccount` (used for uniquely identifying routes and associate parameters and response with them).
|
||||
|
||||
> For enterprise endpoints make sure you add the `enterprise` tag as well.
|
||||
|
||||
```go
|
||||
|
||||
// swagger:route PATCH /serviceaccounts/{serviceAccountId} service_accounts updateServiceAccount
|
||||
|
||||
@@ -60,12 +60,15 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
||||
Role: ac.RoleDTO{
|
||||
Name: "fixed:datasources:explorer",
|
||||
DisplayName: "Explorer",
|
||||
Description: "Enable the Explore feature. Data source permissions still apply; you can only query data sources for which you have query permissions.",
|
||||
Description: "Enable the Explore and Drilldown features. Data source permissions still apply; you can only query data sources for which you have query permissions.",
|
||||
Group: "Data sources",
|
||||
Permissions: []ac.Permission{
|
||||
{
|
||||
Action: ac.ActionDatasourcesExplore,
|
||||
},
|
||||
{
|
||||
Action: ac.ActionDatasourcesDrilldown,
|
||||
},
|
||||
},
|
||||
},
|
||||
Grants: []string{string(org.RoleEditor)},
|
||||
@@ -606,6 +609,45 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
||||
Grants: []string{"Admin"},
|
||||
}
|
||||
|
||||
snapshotsCreatorRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Name: "fixed:snapshots:creator",
|
||||
DisplayName: "Creator",
|
||||
Description: "Create snapshots",
|
||||
Group: "Snapshots",
|
||||
Permissions: []ac.Permission{
|
||||
{Action: dashboards.ActionSnapshotsCreate},
|
||||
},
|
||||
},
|
||||
Grants: []string{string(org.RoleEditor)},
|
||||
}
|
||||
|
||||
snapshotsDeleterRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Name: "fixed:snapshots:deleter",
|
||||
DisplayName: "Deleter",
|
||||
Description: "Delete snapshots",
|
||||
Group: "Snapshots",
|
||||
Permissions: []ac.Permission{
|
||||
{Action: dashboards.ActionSnapshotsDelete},
|
||||
},
|
||||
},
|
||||
Grants: []string{string(org.RoleEditor)},
|
||||
}
|
||||
|
||||
snapshotsReaderRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Name: "fixed:snapshots:reader",
|
||||
DisplayName: "Reader",
|
||||
Description: "Read snapshots",
|
||||
Group: "Snapshots",
|
||||
Permissions: []ac.Permission{
|
||||
{Action: dashboards.ActionSnapshotsRead},
|
||||
},
|
||||
},
|
||||
Grants: []string{string(org.RoleViewer)},
|
||||
}
|
||||
|
||||
roles := []ac.RoleRegistration{provisioningWriterRole, datasourcesReaderRole, builtInDatasourceReader, datasourcesWriterRole,
|
||||
datasourcesIdReaderRole, datasourcesCreatorRole, orgReaderRole, orgWriterRole,
|
||||
orgMaintainerRole, teamsCreatorRole, teamsWriterRole, teamsReaderRole, datasourcesExplorerRole,
|
||||
@@ -613,7 +655,8 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
||||
dashboardsCreatorRole, dashboardsReaderRole, dashboardsWriterRole,
|
||||
foldersCreatorRole, foldersReaderRole, generalFolderReaderRole, foldersWriterRole, apikeyReaderRole, apikeyWriterRole,
|
||||
publicDashboardsWriterRole, featuremgmtReaderRole, featuremgmtWriterRole, libraryPanelsCreatorRole,
|
||||
libraryPanelsReaderRole, libraryPanelsWriterRole, libraryPanelsGeneralReaderRole, libraryPanelsGeneralWriterRole}
|
||||
libraryPanelsReaderRole, libraryPanelsWriterRole, libraryPanelsGeneralReaderRole, libraryPanelsGeneralWriterRole,
|
||||
snapshotsCreatorRole, snapshotsDeleterRole, snapshotsReaderRole}
|
||||
|
||||
if hs.Features.IsEnabled(context.Background(), featuremgmt.FlagAnnotationPermissionUpdate) {
|
||||
allAnnotationsReaderRole := ac.RoleRegistration{
|
||||
|
||||
@@ -8,10 +8,9 @@ import (
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
authlib "github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
@@ -71,6 +70,7 @@ func (hs *HTTPServer) AdminCreateUser(c *contextmodel.ReqContext) response.Respo
|
||||
result := user.AdminCreateUserResponse{
|
||||
Message: "User created",
|
||||
ID: usr.ID,
|
||||
UID: usr.UID,
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, result)
|
||||
@@ -158,8 +158,7 @@ func (hs *HTTPServer) AdminUpdateUserPermissions(c *contextmodel.ReqContext) res
|
||||
}
|
||||
|
||||
if authInfo, err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &login.GetAuthInfoQuery{UserId: userID}); err == nil && authInfo != nil {
|
||||
oauthInfo := hs.SocialService.GetOAuthInfoProvider(authInfo.AuthModule)
|
||||
if login.IsGrafanaAdminExternallySynced(hs.Cfg, oauthInfo, authInfo.AuthModule) {
|
||||
if hs.isGrafanaAdminExternallySynced(hs.Cfg, authInfo.AuthModule) {
|
||||
return response.Error(http.StatusForbidden, "Cannot change Grafana Admin role for externally synced user", nil)
|
||||
}
|
||||
}
|
||||
@@ -362,12 +361,13 @@ func (hs *HTTPServer) AdminEnableUser(c *contextmodel.ReqContext) response.Respo
|
||||
// 404: notFoundError
|
||||
// 500: internalServerError
|
||||
func (hs *HTTPServer) AdminLogoutUser(c *contextmodel.ReqContext) response.Response {
|
||||
userID, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
|
||||
id := web.Params(c.Req)[":id"]
|
||||
userID, err := strconv.ParseInt(id, 10, 64)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "id is invalid", err)
|
||||
}
|
||||
|
||||
if c.SignedInUser.GetID() == identity.NewTypedID(claims.TypeUser, userID) {
|
||||
if c.SignedInUser.GetID() == authlib.NewTypeID(authlib.TypeUser, id) {
|
||||
return response.Error(http.StatusBadRequest, "You cannot logout yourself", nil)
|
||||
}
|
||||
|
||||
|
||||
+64
-29
@@ -14,10 +14,11 @@ import (
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/db/dbtest"
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/login/social/socialtest"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/auth/authtest"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/authn/authntest"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
"github.com/grafana/grafana/pkg/services/login/authinfotest"
|
||||
@@ -243,39 +244,72 @@ func Test_AdminUpdateUserPermissions(t *testing.T) {
|
||||
authEnabled bool
|
||||
skipOrgRoleSync bool
|
||||
expectedRespCode int
|
||||
enabledAuthnClients []string
|
||||
authnClientConfig authn.SSOClientConfig
|
||||
}{
|
||||
// oauth
|
||||
{
|
||||
name: "Should allow updating an externally synced OAuth user if Grafana Admin role is not synced",
|
||||
authModule: login.GenericOAuthModule,
|
||||
authEnabled: true,
|
||||
allowAssignGrafanaAdmin: false,
|
||||
skipOrgRoleSync: false,
|
||||
expectedRespCode: http.StatusOK,
|
||||
name: "Should allow updating an externally synced OAuth user if Grafana Admin role is not synced",
|
||||
authModule: login.GenericOAuthModule,
|
||||
enabledAuthnClients: []string{authn.ClientWithPrefix("generic_oauth")},
|
||||
authnClientConfig: &authntest.FakeSSOClientConfig{
|
||||
ExpectedIsSkipOrgRoleSyncEnabled: false,
|
||||
ExpectedIsAllowAssignGrafanaAdminEnabled: false,
|
||||
},
|
||||
expectedRespCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Should allow updating an externally synced OAuth user if OAuth provider is not enabled",
|
||||
authModule: login.GenericOAuthModule,
|
||||
authEnabled: false,
|
||||
allowAssignGrafanaAdmin: true,
|
||||
skipOrgRoleSync: false,
|
||||
expectedRespCode: http.StatusOK,
|
||||
name: "Should allow updating an externally synced OAuth user if OAuth provider is not enabled",
|
||||
authModule: login.GenericOAuthModule,
|
||||
expectedRespCode: http.StatusOK,
|
||||
enabledAuthnClients: []string{},
|
||||
authnClientConfig: &authntest.FakeSSOClientConfig{
|
||||
ExpectedIsSkipOrgRoleSyncEnabled: false,
|
||||
ExpectedIsAllowAssignGrafanaAdminEnabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Should allow updating an externally synced OAuth user if org roles are not being synced",
|
||||
authModule: login.GenericOAuthModule,
|
||||
authEnabled: true,
|
||||
allowAssignGrafanaAdmin: true,
|
||||
skipOrgRoleSync: true,
|
||||
expectedRespCode: http.StatusOK,
|
||||
name: "Should allow updating an externally synced OAuth user if org roles are not being synced",
|
||||
authModule: login.GenericOAuthModule,
|
||||
expectedRespCode: http.StatusOK,
|
||||
enabledAuthnClients: []string{authn.ClientWithPrefix("generic_oauth")},
|
||||
authnClientConfig: &authntest.FakeSSOClientConfig{
|
||||
ExpectedIsSkipOrgRoleSyncEnabled: true,
|
||||
ExpectedIsAllowAssignGrafanaAdminEnabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Should not allow updating an externally synced OAuth user",
|
||||
authModule: login.GenericOAuthModule,
|
||||
authEnabled: true,
|
||||
allowAssignGrafanaAdmin: true,
|
||||
skipOrgRoleSync: false,
|
||||
expectedRespCode: http.StatusForbidden,
|
||||
name: "Should not allow updating an externally synced OAuth user",
|
||||
authModule: login.GenericOAuthModule,
|
||||
expectedRespCode: http.StatusForbidden,
|
||||
enabledAuthnClients: []string{authn.ClientWithPrefix("generic_oauth")},
|
||||
authnClientConfig: &authntest.FakeSSOClientConfig{
|
||||
ExpectedIsSkipOrgRoleSyncEnabled: false,
|
||||
ExpectedIsAllowAssignGrafanaAdminEnabled: true,
|
||||
},
|
||||
},
|
||||
// saml
|
||||
{
|
||||
name: "Should allow updating an externally synced SAML user if org roles are not being synced",
|
||||
authModule: login.SAMLAuthModule,
|
||||
expectedRespCode: http.StatusOK,
|
||||
enabledAuthnClients: []string{authn.ClientSAML},
|
||||
authnClientConfig: &authntest.FakeSSOClientConfig{
|
||||
ExpectedIsSkipOrgRoleSyncEnabled: true,
|
||||
ExpectedIsAllowAssignGrafanaAdminEnabled: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Should not allow updating an externally synced SAML user",
|
||||
authModule: login.SAMLAuthModule,
|
||||
expectedRespCode: http.StatusForbidden,
|
||||
enabledAuthnClients: []string{authn.ClientSAML},
|
||||
authnClientConfig: &authntest.FakeSSOClientConfig{
|
||||
ExpectedIsSkipOrgRoleSyncEnabled: false,
|
||||
ExpectedIsAllowAssignGrafanaAdminEnabled: true,
|
||||
},
|
||||
},
|
||||
// jwt
|
||||
{
|
||||
name: "Should allow updating an externally synced JWT user if Grafana Admin role is not synced",
|
||||
authModule: login.JWTModule,
|
||||
@@ -316,10 +350,7 @@ func Test_AdminUpdateUserPermissions(t *testing.T) {
|
||||
socialService := &socialtest.FakeSocialService{}
|
||||
cfg := setting.NewCfg()
|
||||
|
||||
switch tc.authModule {
|
||||
case login.GenericOAuthModule:
|
||||
socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{AllowAssignGrafanaAdmin: tc.allowAssignGrafanaAdmin, Enabled: tc.authEnabled, SkipOrgRoleSync: tc.skipOrgRoleSync}
|
||||
case login.JWTModule:
|
||||
if tc.authModule == login.JWTModule {
|
||||
cfg.JWTAuth.Enabled = tc.authEnabled
|
||||
cfg.JWTAuth.SkipOrgRoleSync = tc.skipOrgRoleSync
|
||||
cfg.JWTAuth.AllowAssignGrafanaAdmin = tc.allowAssignGrafanaAdmin
|
||||
@@ -330,6 +361,10 @@ func Test_AdminUpdateUserPermissions(t *testing.T) {
|
||||
authInfoService: authInfoService,
|
||||
SocialService: socialService,
|
||||
userService: usertest.NewUserServiceFake(),
|
||||
authnService: &authntest.FakeService{
|
||||
ExpectedClientConfig: tc.authnClientConfig,
|
||||
EnabledClients: tc.enabledAuthnClients,
|
||||
},
|
||||
}
|
||||
|
||||
sc := setupScenarioContext(t, "/api/admin/users/1/permissions")
|
||||
|
||||
@@ -587,7 +587,7 @@ func (hs *HTTPServer) GetAnnotationTags(c *contextmodel.ReqContext) response.Res
|
||||
// where <type> is the type of annotation with id <id>.
|
||||
// If annotationPermissionUpdate feature toggle is enabled, dashboard annotation scope will be resolved to the corresponding
|
||||
// dashboard and folder scopes (eg, "dashboards:uid:<annotation_dashboard_uid>", "folders:uid:<parent_folder_uid>" etc).
|
||||
func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, features featuremgmt.FeatureToggles, dashSvc dashboards.DashboardService, folderStore folder.Store) (string, accesscontrol.ScopeAttributeResolver) {
|
||||
func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, features featuremgmt.FeatureToggles, dashSvc dashboards.DashboardService, folderSvc folder.Service) (string, accesscontrol.ScopeAttributeResolver) {
|
||||
prefix := accesscontrol.ScopeAnnotationsProvider.GetResourceScope("")
|
||||
return prefix, accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, initialScope string) ([]string, error) {
|
||||
scopeParts := strings.Split(initialScope, ":")
|
||||
@@ -649,7 +649,7 @@ func AnnotationTypeScopeResolver(annotationsRepo annotations.Repository, feature
|
||||
// Append dashboard parent scopes if dashboard is in a folder or the general scope if dashboard is not in a folder
|
||||
if dashboard.FolderUID != "" {
|
||||
scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(dashboard.FolderUID))
|
||||
inheritedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, dashboard.FolderUID, folderStore)
|
||||
inheritedScopes, err := dashboards.GetInheritedScopes(ctx, orgID, dashboard.FolderUID, folderSvc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
@@ -397,15 +396,14 @@ func TestAPI_Annotations(t *testing.T) {
|
||||
dashService := &dashboards.FakeDashboardService{}
|
||||
dashService.On("GetDashboard", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{UID: dashUID, FolderUID: folderUID, FolderID: 1}, nil)
|
||||
folderService := &foldertest.FakeService{}
|
||||
fStore := folder.NewFakeStore()
|
||||
folderService.ExpectedFolder = &folder.Folder{UID: folderUID, ID: 1}
|
||||
folderDB := &foldertest.FakeFolderStore{}
|
||||
folderDB.On("GetFolderByID", mock.Anything, mock.Anything, mock.Anything).Return(&folder.Folder{UID: folderUID, ID: 1}, nil)
|
||||
hs.DashboardService = dashService
|
||||
hs.folderService = folderService
|
||||
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
|
||||
hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo, hs.Features, dashService, fStore))
|
||||
hs.AccessControl.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(folderDB, dashService, fStore))
|
||||
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
|
||||
hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo, hs.Features, dashService, folderService))
|
||||
hs.AccessControl.RegisterScopeAttributeResolver(dashboards.NewDashboardIDScopeResolver(dashService, folderService))
|
||||
})
|
||||
var body io.Reader
|
||||
if tt.body != "" {
|
||||
@@ -505,7 +503,7 @@ func TestService_AnnotationTypeScopeResolver(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
features := featuremgmt.WithFeatures(tc.featureToggles...)
|
||||
prefix, resolver := AnnotationTypeScopeResolver(fakeAnnoRepo, features, dashSvc, folder.NewFakeStore())
|
||||
prefix, resolver := AnnotationTypeScopeResolver(fakeAnnoRepo, features, dashSvc, &foldertest.FakeService{})
|
||||
require.Equal(t, "annotations:id:", prefix)
|
||||
|
||||
resolved, err := resolver.Resolve(context.Background(), 1, tc.given)
|
||||
|
||||
+66
-34
@@ -30,6 +30,11 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/middleware/requestmeta"
|
||||
@@ -37,6 +42,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/ssoutils"
|
||||
"github.com/grafana/grafana/pkg/services/apikey"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/cloudmigration"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/correlations"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
@@ -46,7 +53,7 @@ import (
|
||||
publicdashboardsapi "github.com/grafana/grafana/pkg/services/publicdashboards/api"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"go.opentelemetry.io/otel"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/api")
|
||||
@@ -60,11 +67,13 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
reqGrafanaAdmin := middleware.ReqGrafanaAdmin
|
||||
reqOrgAdmin := middleware.ReqOrgAdmin
|
||||
reqRoleForAppRoute := middleware.RoleAppPluginAuth(hs.AccessControl, hs.pluginStore, hs.Features, hs.log)
|
||||
reqSnapshotPublicModeOrSignedIn := middleware.SnapshotPublicModeOrSignedIn(hs.Cfg)
|
||||
reqSnapshotPublicModeOrCreate := middleware.SnapshotPublicModeOrCreate(hs.Cfg, hs.AccessControl)
|
||||
reqSnapshotPublicModeOrDelete := middleware.SnapshotPublicModeOrDelete(hs.Cfg, hs.AccessControl)
|
||||
redirectFromLegacyPanelEditURL := middleware.RedirectFromLegacyPanelEditURL(hs.Cfg)
|
||||
authorize := ac.Middleware(hs.AccessControl)
|
||||
authorizeInOrg := ac.AuthorizeInOrgMiddleware(hs.AccessControl, hs.authnService)
|
||||
quota := middleware.Quota(hs.QuotaService)
|
||||
userUIDResolver := middlewareUserUIDResolver(hs.userService, ":id")
|
||||
|
||||
r := hs.RouteRegister
|
||||
|
||||
@@ -72,6 +81,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/logout", hs.Logout)
|
||||
r.Post("/login", requestmeta.SetOwner(requestmeta.TeamAuth), quota(string(auth.QuotaTargetSrv)), routing.Wrap(hs.LoginPost))
|
||||
r.Get("/login/:name", quota(string(auth.QuotaTargetSrv)), hs.OAuthLogin)
|
||||
|
||||
r.Get("/login", hs.LoginView)
|
||||
r.Get("/invite/:code", hs.Index)
|
||||
|
||||
@@ -106,13 +116,9 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/admin/orgs", authorizeInOrg(ac.UseGlobalOrg, ac.OrgsAccessEvaluator), hs.Index)
|
||||
r.Get("/admin/orgs/edit/:id", authorizeInOrg(ac.UseGlobalOrg, ac.OrgsAccessEvaluator), hs.Index)
|
||||
r.Get("/admin/stats", authorize(ac.EvalPermission(ac.ActionServerStatsRead)), hs.Index)
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagStorage) {
|
||||
r.Get("/admin/storage", reqSignedIn, hs.Index)
|
||||
r.Get("/admin/storage/*", reqSignedIn, hs.Index)
|
||||
}
|
||||
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagOnPremToCloudMigrations) {
|
||||
r.Get("/admin/migrate-to-cloud", reqOrgAdmin, hs.Index)
|
||||
r.Get("/admin/migrate-to-cloud", authorize(cloudmigration.MigrationAssistantAccess), hs.Index)
|
||||
}
|
||||
|
||||
// feature toggle admin page
|
||||
@@ -158,7 +164,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/dashboards/*", reqSignedIn, hs.Index)
|
||||
r.Get("/goto/:uid", reqSignedIn, hs.redirectFromShortURL, hs.Index)
|
||||
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagPublicDashboards) && hs.Cfg.PublicDashboardsEnabled {
|
||||
if hs.Cfg.PublicDashboardsEnabled {
|
||||
// list public dashboards
|
||||
r.Get("/public-dashboards/list", reqSignedIn, hs.Index)
|
||||
|
||||
@@ -177,6 +183,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
}
|
||||
|
||||
r.Get("/explore", authorize(ac.EvalPermission(ac.ActionDatasourcesExplore)), hs.Index)
|
||||
r.Get("/drilldown", authorize(ac.EvalPermission(ac.ActionDatasourcesDrilldown)), hs.Index)
|
||||
|
||||
r.Get("/playlists/", reqSignedIn, hs.Index)
|
||||
r.Get("/playlists/*", reqSignedIn, hs.Index)
|
||||
@@ -201,6 +208,11 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Post("/api/user/email/start-verify", reqSignedInNoAnonymous, routing.Wrap(hs.StartEmailVerificaton))
|
||||
}
|
||||
|
||||
if hs.Cfg.PasswordlessMagicLinkAuth.Enabled && hs.Features.IsEnabledGlobally(featuremgmt.FlagPasswordlessMagicLinkAuthentication) {
|
||||
r.Post("/api/login/passwordless/start", requestmeta.SetOwner(requestmeta.TeamAuth), quota(string(auth.QuotaTargetSrv)), hs.StartPasswordless)
|
||||
r.Post("/api/login/passwordless/authenticate", requestmeta.SetOwner(requestmeta.TeamAuth), quota(string(auth.QuotaTargetSrv)), routing.Wrap(hs.LoginPasswordless))
|
||||
}
|
||||
|
||||
// invited
|
||||
r.Get("/api/user/invite/:code", routing.Wrap(hs.GetInviteInfoByCode))
|
||||
r.Post("/api/user/invite/complete", routing.Wrap(hs.CompleteInvite))
|
||||
@@ -282,13 +294,13 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
userIDScope := ac.Scope("global.users", "id", ac.Parameter(":id"))
|
||||
usersRoute.Get("/", authorize(ac.EvalPermission(ac.ActionUsersRead)), routing.Wrap(hs.searchUsersService.SearchUsers))
|
||||
usersRoute.Get("/search", authorize(ac.EvalPermission(ac.ActionUsersRead)), routing.Wrap(hs.searchUsersService.SearchUsersWithPaging))
|
||||
usersRoute.Get("/:id", authorize(ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(hs.GetUserByID))
|
||||
usersRoute.Get("/:id/teams", authorize(ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(hs.GetUserTeams))
|
||||
usersRoute.Get("/:id/orgs", authorize(ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(hs.GetUserOrgList))
|
||||
usersRoute.Get("/:id", userUIDResolver, authorize(ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(hs.GetUserByID))
|
||||
usersRoute.Get("/:id/teams", userUIDResolver, authorize(ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(hs.GetUserTeams))
|
||||
usersRoute.Get("/:id/orgs", userUIDResolver, authorize(ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(hs.GetUserOrgList))
|
||||
// query parameters /users/lookup?loginOrEmail=admin@example.com
|
||||
usersRoute.Get("/lookup", authorize(ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll)), routing.Wrap(hs.GetUserByLoginOrEmail))
|
||||
usersRoute.Put("/:id", authorize(ac.EvalPermission(ac.ActionUsersWrite, userIDScope)), routing.Wrap(hs.UpdateUser))
|
||||
usersRoute.Post("/:id/using/:orgId", authorize(ac.EvalPermission(ac.ActionUsersWrite, userIDScope)), routing.Wrap(hs.UpdateUserActiveOrg))
|
||||
usersRoute.Put("/:id", userUIDResolver, authorize(ac.EvalPermission(ac.ActionUsersWrite, userIDScope)), routing.Wrap(hs.UpdateUser))
|
||||
usersRoute.Post("/:id/using/:orgId", userUIDResolver, authorize(ac.EvalPermission(ac.ActionUsersWrite, userIDScope)), routing.Wrap(hs.UpdateUserActiveOrg))
|
||||
}, requestmeta.SetOwner(requestmeta.TeamAuth))
|
||||
|
||||
// org information available to all users.
|
||||
@@ -306,10 +318,6 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
apiRoute.Group("/search-v2", hs.SearchV2HTTPService.RegisterHTTPRoutes)
|
||||
}
|
||||
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageSearch) {
|
||||
apiRoute.Group("/unified-search", hs.UnifiedSearchHTTPService.RegisterHTTPRoutes)
|
||||
}
|
||||
|
||||
// current org
|
||||
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
|
||||
userIDScope := ac.Scope("users", "id", ac.Parameter(":userId"))
|
||||
@@ -355,6 +363,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
// search all orgs
|
||||
apiRoute.Get("/orgs", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionOrgsRead)), routing.Wrap(hs.SearchOrgs))
|
||||
|
||||
orgUserUIDResolver := middlewareUserUIDResolver(hs.userService, ":userId")
|
||||
// orgs (admin routes)
|
||||
apiRoute.Group("/orgs/:orgId", func(orgsRoute routing.RouteRegister) {
|
||||
userIDScope := ac.Scope("users", "id", ac.Parameter(":userId"))
|
||||
@@ -365,8 +374,8 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
orgsRoute.Get("/users", requestmeta.SetOwner(requestmeta.TeamAuth), authorizeInOrg(ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.GetOrgUsers))
|
||||
orgsRoute.Get("/users/search", requestmeta.SetOwner(requestmeta.TeamAuth), authorizeInOrg(ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.SearchOrgUsers))
|
||||
orgsRoute.Post("/users", requestmeta.SetOwner(requestmeta.TeamAuth), authorizeInOrg(ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersAdd, ac.ScopeUsersAll)), routing.Wrap(hs.AddOrgUser))
|
||||
orgsRoute.Patch("/users/:userId", requestmeta.SetOwner(requestmeta.TeamAuth), authorizeInOrg(ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersWrite, userIDScope)), routing.Wrap(hs.UpdateOrgUser))
|
||||
orgsRoute.Delete("/users/:userId", requestmeta.SetOwner(requestmeta.TeamAuth), authorizeInOrg(ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(hs.RemoveOrgUser))
|
||||
orgsRoute.Patch("/users/:userId", orgUserUIDResolver, requestmeta.SetOwner(requestmeta.TeamAuth), authorizeInOrg(ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersWrite, userIDScope)), routing.Wrap(hs.UpdateOrgUser))
|
||||
orgsRoute.Delete("/users/:userId", orgUserUIDResolver, requestmeta.SetOwner(requestmeta.TeamAuth), authorizeInOrg(ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(hs.RemoveOrgUser))
|
||||
orgsRoute.Get("/quotas", authorizeInOrg(ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgsQuotasRead)), routing.Wrap(hs.GetOrgQuotas))
|
||||
orgsRoute.Put("/quotas/:target", authorizeInOrg(ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgsQuotasWrite)), routing.Wrap(hs.UpdateOrgQuota))
|
||||
})
|
||||
@@ -416,7 +425,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
apiRoute.Any("/plugin-proxy/:pluginId/*", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), authorize(ac.EvalPermission(pluginaccesscontrol.ActionAppAccess, pluginIDScope)), checkAppEnabled(hs.pluginStore, hs.PluginSettings), hs.ProxyPluginRequest)
|
||||
apiRoute.Any("/plugin-proxy/:pluginId", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), authorize(ac.EvalPermission(pluginaccesscontrol.ActionAppAccess, pluginIDScope)), checkAppEnabled(hs.pluginStore, hs.PluginSettings), hs.ProxyPluginRequest)
|
||||
|
||||
if hs.Cfg.PluginAdminEnabled && (hs.Features.IsEnabledGlobally(featuremgmt.FlagManagedPluginsInstall) || !hs.Cfg.PluginAdminExternalManageEnabled) {
|
||||
if hs.Cfg.PluginAdminEnabled {
|
||||
apiRoute.Group("/plugins", func(pluginRoute routing.RouteRegister) {
|
||||
pluginRoute.Post("/:pluginId/install", authorizeInOrg(ac.UseGlobalOrSingleOrg(hs.Cfg), ac.EvalPermission(pluginaccesscontrol.ActionInstall)), routing.Wrap(hs.InstallPlugin))
|
||||
pluginRoute.Post("/:pluginId/uninstall", authorizeInOrg(ac.UseGlobalOrSingleOrg(hs.Cfg), ac.EvalPermission(pluginaccesscontrol.ActionInstall)), routing.Wrap(hs.UninstallPlugin))
|
||||
@@ -499,7 +508,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
|
||||
// Dashboard snapshots
|
||||
apiRoute.Group("/dashboard/snapshots", func(dashboardRoute routing.RouteRegister) {
|
||||
dashboardRoute.Get("/", routing.Wrap(hs.SearchDashboardSnapshots))
|
||||
dashboardRoute.Get("/", authorize(ac.EvalPermission(dashboards.ActionSnapshotsRead)), routing.Wrap(hs.SearchDashboardSnapshots))
|
||||
})
|
||||
|
||||
// Playlist
|
||||
@@ -577,17 +586,17 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
userIDScope := ac.Scope("global.users", "id", ac.Parameter(":id"))
|
||||
|
||||
adminUserRoute.Post("/", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersCreate)), routing.Wrap(hs.AdminCreateUser))
|
||||
adminUserRoute.Put("/:id/password", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersPasswordUpdate, userIDScope)), routing.Wrap(hs.AdminUpdateUserPassword))
|
||||
adminUserRoute.Put("/:id/permissions", reqGrafanaAdmin, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersPermissionsUpdate, userIDScope)), routing.Wrap(hs.AdminUpdateUserPermissions))
|
||||
adminUserRoute.Delete("/:id", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersDelete, userIDScope)), routing.Wrap(hs.AdminDeleteUser))
|
||||
adminUserRoute.Post("/:id/disable", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersDisable, userIDScope)), routing.Wrap(hs.AdminDisableUser))
|
||||
adminUserRoute.Post("/:id/enable", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersEnable, userIDScope)), routing.Wrap(hs.AdminEnableUser))
|
||||
adminUserRoute.Get("/:id/quotas", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersQuotasList, userIDScope)), routing.Wrap(hs.GetUserQuotas))
|
||||
adminUserRoute.Put("/:id/quotas/:target", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersQuotasUpdate, userIDScope)), routing.Wrap(hs.UpdateUserQuota))
|
||||
adminUserRoute.Put("/:id/password", userUIDResolver, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersPasswordUpdate, userIDScope)), routing.Wrap(hs.AdminUpdateUserPassword))
|
||||
adminUserRoute.Put("/:id/permissions", userUIDResolver, reqGrafanaAdmin, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersPermissionsUpdate, userIDScope)), routing.Wrap(hs.AdminUpdateUserPermissions))
|
||||
adminUserRoute.Delete("/:id", userUIDResolver, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersDelete, userIDScope)), routing.Wrap(hs.AdminDeleteUser))
|
||||
adminUserRoute.Post("/:id/disable", userUIDResolver, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersDisable, userIDScope)), routing.Wrap(hs.AdminDisableUser))
|
||||
adminUserRoute.Post("/:id/enable", userUIDResolver, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersEnable, userIDScope)), routing.Wrap(hs.AdminEnableUser))
|
||||
adminUserRoute.Get("/:id/quotas", userUIDResolver, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersQuotasList, userIDScope)), routing.Wrap(hs.GetUserQuotas))
|
||||
adminUserRoute.Put("/:id/quotas/:target", userUIDResolver, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersQuotasUpdate, userIDScope)), routing.Wrap(hs.UpdateUserQuota))
|
||||
|
||||
adminUserRoute.Post("/:id/logout", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersLogout, userIDScope)), routing.Wrap(hs.AdminLogoutUser))
|
||||
adminUserRoute.Get("/:id/auth-tokens", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersAuthTokenList, userIDScope)), routing.Wrap(hs.AdminGetUserAuthTokens))
|
||||
adminUserRoute.Post("/:id/revoke-auth-token", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersAuthTokenUpdate, userIDScope)), routing.Wrap(hs.AdminRevokeUserAuthToken))
|
||||
adminUserRoute.Post("/:id/logout", userUIDResolver, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersLogout, userIDScope)), routing.Wrap(hs.AdminLogoutUser))
|
||||
adminUserRoute.Get("/:id/auth-tokens", userUIDResolver, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersAuthTokenList, userIDScope)), routing.Wrap(hs.AdminGetUserAuthTokens))
|
||||
adminUserRoute.Post("/:id/revoke-auth-token", userUIDResolver, authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionUsersAuthTokenUpdate, userIDScope)), routing.Wrap(hs.AdminRevokeUserAuthToken))
|
||||
}, reqSignedIn)
|
||||
|
||||
// rendering
|
||||
@@ -600,9 +609,32 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/avatar/:hash", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), hs.AvatarCacheServer.Handler)
|
||||
|
||||
// Snapshots
|
||||
r.Post("/api/snapshots/", reqSnapshotPublicModeOrSignedIn, hs.getCreatedSnapshotHandler())
|
||||
r.Get("/api/snapshot/shared-options/", reqSignedIn, hs.GetSharingOptions)
|
||||
|
||||
r.Post("/api/snapshots/", reqSnapshotPublicModeOrCreate, hs.getCreatedSnapshotHandler())
|
||||
r.Get("/api/snapshots/:key", routing.Wrap(hs.GetDashboardSnapshot))
|
||||
r.Get("/api/snapshots-delete/:deleteKey", reqSnapshotPublicModeOrSignedIn, routing.Wrap(hs.DeleteDashboardSnapshotByDeleteKey))
|
||||
r.Delete("/api/snapshots/:key", reqSignedIn, routing.Wrap(hs.DeleteDashboardSnapshot))
|
||||
r.Delete("/api/snapshots/:key", authorize(ac.EvalPermission(dashboards.ActionSnapshotsDelete)), routing.Wrap(hs.DeleteDashboardSnapshot))
|
||||
|
||||
// Snapshots delete for public mode or using the deleteKey
|
||||
r.Get("/api/snapshots-delete/:deleteKey", reqSnapshotPublicModeOrDelete, routing.Wrap(hs.DeleteDashboardSnapshotByDeleteKey))
|
||||
}
|
||||
|
||||
func middlewareUserUIDResolver(userService user.Service, paramName string) web.Handler {
|
||||
handler := user.UIDToIDHandler(userService)
|
||||
|
||||
return func(c *contextmodel.ReqContext) {
|
||||
userID := web.Params(c.Req)[paramName]
|
||||
id, err := handler(c.Req.Context(), userID)
|
||||
if err == nil {
|
||||
gotParams := web.Params(c.Req)
|
||||
gotParams[paramName] = id
|
||||
web.SetURLParams(c.Req, gotParams)
|
||||
} else {
|
||||
if errors.Is(err, user.ErrUserNotFound) {
|
||||
c.JsonApiErr(http.StatusNotFound, "User not found", nil)
|
||||
} else {
|
||||
c.JsonApiErr(http.StatusInternalServerError, "Failed to resolve user", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package apierrors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
@@ -34,8 +36,7 @@ func ToFolderErrorResponse(err error) response.Response {
|
||||
return response.JSON(http.StatusNotFound, util.DynMap{"status": "not-found", "message": dashboards.ErrFolderNotFound.Error()})
|
||||
}
|
||||
|
||||
if errors.Is(err, dashboards.ErrFolderSameNameExists) ||
|
||||
errors.Is(err, dashboards.ErrFolderWithSameUIDExists) {
|
||||
if errors.Is(err, dashboards.ErrFolderWithSameUIDExists) {
|
||||
return response.Error(http.StatusConflict, err.Error(), nil)
|
||||
}
|
||||
|
||||
@@ -43,26 +44,51 @@ func ToFolderErrorResponse(err error) response.Response {
|
||||
return response.JSON(http.StatusPreconditionFailed, util.DynMap{"status": "version-mismatch", "message": dashboards.ErrFolderVersionMismatch.Error()})
|
||||
}
|
||||
|
||||
// folder errors are wrapped in an error util, so this is the only way of comparing errors
|
||||
if err.Error() == folder.ErrMaximumDepthReached.Error() {
|
||||
return response.JSON(http.StatusBadRequest, util.DynMap{"messageId": "folder.maximum-depth-reached", "message": "Maximum nested folder depth reached"})
|
||||
}
|
||||
|
||||
return response.ErrOrFallback(http.StatusInternalServerError, "Folder API error", err)
|
||||
}
|
||||
|
||||
func ToFolderStatusError(err error) k8sErrors.StatusError {
|
||||
resp := ToFolderErrorResponse(err)
|
||||
defaultErr := k8sErrors.StatusError{
|
||||
ErrStatus: metav1.Status{
|
||||
Message: "Folder API error",
|
||||
Code: http.StatusInternalServerError,
|
||||
},
|
||||
}
|
||||
|
||||
normResp, ok := resp.(*response.NormalResponse)
|
||||
if !ok {
|
||||
return k8sErrors.StatusError{
|
||||
ErrStatus: metav1.Status{
|
||||
Message: "Folder API error",
|
||||
Code: http.StatusInternalServerError,
|
||||
},
|
||||
}
|
||||
return defaultErr
|
||||
}
|
||||
|
||||
var dat map[string]interface{}
|
||||
if err := json.Unmarshal(normResp.Body(), &dat); err != nil {
|
||||
return defaultErr
|
||||
}
|
||||
|
||||
m, ok := dat["message"]
|
||||
if !ok {
|
||||
return defaultErr
|
||||
}
|
||||
|
||||
message, ok := m.(string)
|
||||
if !ok {
|
||||
return defaultErr
|
||||
}
|
||||
|
||||
return k8sErrors.StatusError{
|
||||
ErrStatus: metav1.Status{
|
||||
Message: normResp.ErrMessage(),
|
||||
Message: message,
|
||||
Code: int32(normResp.Status()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func IsForbidden(err error) bool {
|
||||
return k8sErrors.IsForbidden(err) || errors.Is(err, dashboards.ErrFolderAccessDenied)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package apierrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestToFolderErrorResponse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input error
|
||||
want response.Response
|
||||
}{
|
||||
{
|
||||
name: "dashboard error",
|
||||
input: dashboards.DashboardErr{StatusCode: 400, Reason: "Dashboard Error", Status: "error"},
|
||||
want: response.Error(400, "Dashboard Error", dashboards.DashboardErr{StatusCode: 400, Reason: "Dashboard Error", Status: "error"}),
|
||||
},
|
||||
{
|
||||
name: "folder title empty",
|
||||
input: dashboards.ErrFolderTitleEmpty,
|
||||
want: response.Error(400, "folder title cannot be empty", nil),
|
||||
},
|
||||
{
|
||||
name: "dashboard type mismatch",
|
||||
input: dashboards.ErrDashboardTypeMismatch,
|
||||
want: response.Error(400, "Dashboard cannot be changed to a folder", dashboards.ErrDashboardTypeMismatch),
|
||||
},
|
||||
{
|
||||
name: "dashboard invalid uid",
|
||||
input: dashboards.ErrDashboardInvalidUid,
|
||||
want: response.Error(400, "uid contains illegal characters", dashboards.ErrDashboardInvalidUid),
|
||||
},
|
||||
{
|
||||
name: "dashboard uid too long",
|
||||
input: dashboards.ErrDashboardUidTooLong,
|
||||
want: response.Error(400, "uid too long, max 40 characters", dashboards.ErrDashboardUidTooLong),
|
||||
},
|
||||
{
|
||||
name: "folder access denied",
|
||||
input: dashboards.ErrFolderAccessDenied,
|
||||
want: response.Error(http.StatusForbidden, "Access denied", dashboards.ErrFolderAccessDenied),
|
||||
},
|
||||
{
|
||||
name: "folder not found",
|
||||
input: dashboards.ErrFolderNotFound,
|
||||
want: response.JSON(http.StatusNotFound, util.DynMap{"status": "not-found", "message": dashboards.ErrFolderNotFound.Error()}),
|
||||
},
|
||||
{
|
||||
name: "folder with same uid exists",
|
||||
input: dashboards.ErrFolderWithSameUIDExists,
|
||||
want: response.Error(http.StatusConflict, dashboards.ErrFolderWithSameUIDExists.Error(), nil),
|
||||
},
|
||||
{
|
||||
name: "folder version mismatch",
|
||||
input: dashboards.ErrFolderVersionMismatch,
|
||||
want: response.JSON(http.StatusPreconditionFailed, util.DynMap{"status": "version-mismatch", "message": dashboards.ErrFolderVersionMismatch.Error()}),
|
||||
},
|
||||
{
|
||||
name: "folder max depth reached",
|
||||
input: folder.ErrMaximumDepthReached,
|
||||
want: response.JSON(http.StatusBadRequest, util.DynMap{"messageId": "folder.maximum-depth-reached", "message": "Maximum nested folder depth reached"}),
|
||||
},
|
||||
{
|
||||
name: "fallback error",
|
||||
input: errors.New("some error"),
|
||||
want: response.ErrOrFallback(http.StatusInternalServerError, "Folder API error", errors.New("some error")),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resp := ToFolderErrorResponse(tt.input)
|
||||
require.Equal(t, tt.want, resp)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,11 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
claims "github.com/grafana/authlib/types"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
@@ -27,7 +28,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/auth/authtest"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/authn/authntest"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
|
||||
@@ -189,7 +189,6 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHa
|
||||
|
||||
return contexthandler.ProvideService(
|
||||
cfg,
|
||||
tracing.InitializeTracerForTest(),
|
||||
&authntest.FakeService{ExpectedIdentity: &authn.Identity{ID: "0", Type: claims.TypeAnonymous, SessionToken: &usertoken.UserToken{}}},
|
||||
featuremgmt.WithFeatures(),
|
||||
)
|
||||
@@ -271,7 +270,7 @@ func setupSimpleHTTPServer(features featuremgmt.FeatureToggles) *HTTPServer {
|
||||
Cfg: cfg,
|
||||
Features: features,
|
||||
License: &licensing.OSSLicensingService{},
|
||||
AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()),
|
||||
AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures()),
|
||||
annotationsRepo: annotationstest.NewFakeAnnotationsRepo(),
|
||||
authInfoService: &authinfotest.FakeService{
|
||||
ExpectedLabels: map[int64]string{int64(1): login.GetAuthProviderLabel(login.LDAPAuthModule)},
|
||||
@@ -314,7 +313,7 @@ func SetupAPITestServer(t *testing.T, opts ...APITestServerOption) *webtest.Serv
|
||||
}
|
||||
|
||||
if hs.AccessControl == nil {
|
||||
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
|
||||
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
|
||||
}
|
||||
|
||||
hs.registerRoutes()
|
||||
|
||||
+78
-36
@@ -11,7 +11,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
claims "github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana/pkg/api/apierrors"
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
@@ -24,7 +24,9 @@ import (
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
|
||||
"github.com/grafana/grafana/pkg/services/dashboardversion/dashverimpl"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
pref "github.com/grafana/grafana/pkg/services/preference"
|
||||
@@ -84,6 +86,8 @@ func dashboardGuardianResponse(err error) response.Response {
|
||||
// 403: forbiddenError
|
||||
// 404: notFoundError
|
||||
// 500: internalServerError
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response {
|
||||
ctx, span := tracer.Start(c.Req.Context(), "api.GetDashboard")
|
||||
defer span.End()
|
||||
@@ -101,7 +105,7 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response
|
||||
)
|
||||
|
||||
// If public dashboards is enabled and we have a public dashboard, update meta values
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagPublicDashboards) && hs.Cfg.PublicDashboardsEnabled {
|
||||
if hs.Cfg.PublicDashboardsEnabled {
|
||||
publicDashboard, err := hs.PublicDashboardsApi.PublicDashboardService.FindByDashboardUid(ctx, c.SignedInUser.GetOrgID(), dash.UID)
|
||||
if err != nil && !errors.Is(err, publicdashboardModels.ErrPublicDashboardNotFound) {
|
||||
return response.Error(http.StatusInternalServerError, "Error while retrieving public dashboards", err)
|
||||
@@ -124,6 +128,11 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response
|
||||
if isEmptyData {
|
||||
return response.Error(http.StatusInternalServerError, "Error while loading dashboard, dashboard data is invalid", nil)
|
||||
}
|
||||
|
||||
// the dashboard id is no longer set in the spec for unified storage, set it here to keep api compatibility
|
||||
if dash.Data.Get("id").MustString() == "" {
|
||||
dash.Data.Set("id", dash.ID)
|
||||
}
|
||||
}
|
||||
guardian, err := guardian.NewByDashboard(ctx, dash, c.SignedInUser.GetOrgID(), c.SignedInUser)
|
||||
if err != nil {
|
||||
@@ -145,10 +154,10 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response
|
||||
// Finding creator and last updater of the dashboard
|
||||
updater, creator := anonString, anonString
|
||||
if dash.UpdatedBy > 0 {
|
||||
updater = hs.getUserLogin(ctx, dash.UpdatedBy)
|
||||
updater = hs.getIdentityName(ctx, dash.OrgID, dash.UpdatedBy)
|
||||
}
|
||||
if dash.CreatedBy > 0 {
|
||||
creator = hs.getUserLogin(ctx, dash.CreatedBy)
|
||||
creator = hs.getIdentityName(ctx, dash.OrgID, dash.CreatedBy)
|
||||
}
|
||||
|
||||
annotationPermissions := &dashboardsV0.AnnotationPermission{}
|
||||
@@ -182,11 +191,35 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response
|
||||
PublicDashboardEnabled: publicDashboardEnabled,
|
||||
}
|
||||
metrics.MFolderIDsAPICount.WithLabelValues(metrics.GetDashboard).Inc()
|
||||
// lookup folder title
|
||||
// nolint:staticcheck
|
||||
if dash.FolderID > 0 {
|
||||
// nolint:staticcheck
|
||||
query := dashboards.GetDashboardQuery{ID: dash.FolderID, OrgID: c.SignedInUser.GetOrgID()}
|
||||
// lookup folder title & url
|
||||
if dash.FolderUID != "" && hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
queryResult, err := hs.folderService.Get(ctx, &folder.GetFolderQuery{
|
||||
OrgID: c.SignedInUser.GetOrgID(),
|
||||
UID: &dash.FolderUID,
|
||||
SignedInUser: c.SignedInUser,
|
||||
})
|
||||
if errors.Is(err, dashboards.ErrFolderNotFound) {
|
||||
return response.Error(http.StatusNotFound, "Folder not found", err)
|
||||
}
|
||||
if apierrors.IsForbidden(err) {
|
||||
// the dashboard is in a folder the user can't access, so return the dashboard without folder info
|
||||
err = nil
|
||||
queryResult = &folder.Folder{
|
||||
UID: dash.FolderUID,
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
hs.log.Error("Failed to get dashboard folder", "error", err)
|
||||
return response.Error(http.StatusInternalServerError, "Dashboard folder could not be read", err)
|
||||
}
|
||||
|
||||
meta.FolderUid = queryResult.UID
|
||||
meta.FolderTitle = queryResult.Title
|
||||
meta.FolderId = queryResult.ID // nolint:staticcheck
|
||||
queryResult = queryResult.WithURL()
|
||||
meta.FolderUrl = queryResult.URL
|
||||
} else if dash.FolderID > 0 { // nolint:staticcheck
|
||||
query := dashboards.GetDashboardQuery{ID: dash.FolderID, OrgID: c.SignedInUser.GetOrgID()} // nolint:staticcheck
|
||||
metrics.MFolderIDsAPICount.WithLabelValues(metrics.GetDashboard).Inc()
|
||||
queryResult, err := hs.DashboardService.GetDashboard(ctx, &query)
|
||||
if err != nil {
|
||||
@@ -260,16 +293,24 @@ func (hs *HTTPServer) getAnnotationPermissionsByScope(c *contextmodel.ReqContext
|
||||
}
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) getUserLogin(ctx context.Context, userID int64) string {
|
||||
ctx, span := tracer.Start(ctx, "api.getUserLogin")
|
||||
// getIdentityName returns name of either user or service account
|
||||
func (hs *HTTPServer) getIdentityName(ctx context.Context, orgID, id int64) string {
|
||||
ctx, span := tracer.Start(ctx, "api.getIdentityName")
|
||||
defer span.End()
|
||||
|
||||
query := user.GetUserByIDQuery{ID: userID}
|
||||
user, err := hs.userService.GetByID(ctx, &query)
|
||||
// We use GetSignedInUser here instead of GetByID so both user and service accounts are resolved.
|
||||
ident, err := hs.userService.GetSignedInUser(ctx, &user.GetSignedInUserQuery{
|
||||
UserID: id,
|
||||
OrgID: orgID,
|
||||
})
|
||||
if err != nil {
|
||||
return anonString
|
||||
}
|
||||
return user.Login
|
||||
|
||||
if ident.IsIdentityType(claims.TypeServiceAccount) {
|
||||
return ident.GetName()
|
||||
}
|
||||
return ident.GetLogin()
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) getDashboardHelper(ctx context.Context, orgID int64, id int64, uid string) (*dashboards.Dashboard, response.Response) {
|
||||
@@ -472,13 +513,7 @@ func (hs *HTTPServer) deleteDashboard(c *contextmodel.ReqContext) response.Respo
|
||||
"error", err)
|
||||
}
|
||||
|
||||
// deletes all related public dashboard entities
|
||||
err = hs.PublicDashboardsApi.PublicDashboardService.DeleteByDashboard(c.Req.Context(), dash)
|
||||
if err != nil {
|
||||
hs.log.Error("Failed to delete public dashboard")
|
||||
}
|
||||
|
||||
err = hs.DashboardService.DeleteDashboard(c.Req.Context(), dash.ID, c.SignedInUser.GetOrgID())
|
||||
err = hs.DashboardService.DeleteDashboard(c.Req.Context(), dash.ID, dash.UID, c.SignedInUser.GetOrgID())
|
||||
if err != nil {
|
||||
var dashboardErr dashboards.DashboardErr
|
||||
if ok := errors.As(err, &dashboardErr); ok {
|
||||
@@ -806,21 +841,22 @@ func (hs *HTTPServer) GetDashboardVersions(c *contextmodel.ReqContext) response.
|
||||
}
|
||||
|
||||
query := dashver.ListDashboardVersionsQuery{
|
||||
OrgID: c.SignedInUser.GetOrgID(),
|
||||
DashboardID: dash.ID,
|
||||
DashboardUID: dash.UID,
|
||||
Limit: c.QueryInt("limit"),
|
||||
Start: c.QueryInt("start"),
|
||||
OrgID: c.SignedInUser.GetOrgID(),
|
||||
DashboardID: dash.ID,
|
||||
DashboardUID: dash.UID,
|
||||
Limit: c.QueryInt("limit"),
|
||||
Start: c.QueryInt("start"),
|
||||
ContinueToken: c.Query("continueToken"),
|
||||
}
|
||||
|
||||
versions, err := hs.dashboardVersionService.List(c.Req.Context(), &query)
|
||||
resp, err := hs.dashboardVersionService.List(c.Req.Context(), &query)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusNotFound, fmt.Sprintf("No versions found for dashboardId %d", dash.ID), err)
|
||||
}
|
||||
|
||||
loginMem := make(map[int64]string, len(versions))
|
||||
res := make([]dashver.DashboardVersionMeta, 0, len(versions))
|
||||
for _, version := range versions {
|
||||
loginMem := make(map[int64]string, len(resp.Versions))
|
||||
res := make([]dashver.DashboardVersionMeta, 0, len(resp.Versions))
|
||||
for _, version := range resp.Versions {
|
||||
msg := version.Message
|
||||
if version.RestoredFrom == version.Version {
|
||||
msg = "Initial save (created by migration)"
|
||||
@@ -840,7 +876,7 @@ func (hs *HTTPServer) GetDashboardVersions(c *contextmodel.ReqContext) response.
|
||||
if found {
|
||||
creator = login
|
||||
} else {
|
||||
creator = hs.getUserLogin(c.Req.Context(), version.CreatedBy)
|
||||
creator = hs.getIdentityName(c.Req.Context(), c.SignedInUser.GetOrgID(), version.CreatedBy)
|
||||
if creator != anonString {
|
||||
loginMem[version.CreatedBy] = creator
|
||||
}
|
||||
@@ -861,7 +897,10 @@ func (hs *HTTPServer) GetDashboardVersions(c *contextmodel.ReqContext) response.
|
||||
})
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, res)
|
||||
return response.JSON(http.StatusOK, dashver.DashboardVersionResponseMeta{
|
||||
Versions: res,
|
||||
ContinueToken: resp.ContinueToken,
|
||||
})
|
||||
}
|
||||
|
||||
// swagger:route GET /dashboards/id/{DashboardID}/versions/{DashboardVersionID} dashboard_versions getDashboardVersionByID
|
||||
@@ -921,12 +960,15 @@ func (hs *HTTPServer) GetDashboardVersion(c *contextmodel.ReqContext) response.R
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
version, _ := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 32)
|
||||
version, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
|
||||
if err != nil {
|
||||
return response.Err(err)
|
||||
}
|
||||
query := dashver.GetDashboardVersionQuery{
|
||||
OrgID: c.SignedInUser.GetOrgID(),
|
||||
DashboardID: dash.ID,
|
||||
DashboardUID: dash.UID,
|
||||
Version: int(version),
|
||||
Version: version,
|
||||
}
|
||||
|
||||
res, err := hs.dashboardVersionService.Get(c.Req.Context(), &query)
|
||||
@@ -936,7 +978,7 @@ func (hs *HTTPServer) GetDashboardVersion(c *contextmodel.ReqContext) response.R
|
||||
|
||||
creator := anonString
|
||||
if res.CreatedBy > 0 {
|
||||
creator = hs.getUserLogin(c.Req.Context(), res.CreatedBy)
|
||||
creator = hs.getIdentityName(c.Req.Context(), dash.OrgID, res.CreatedBy)
|
||||
}
|
||||
|
||||
dashVersionMeta := &dashver.DashboardVersionMeta{
|
||||
@@ -1136,7 +1178,7 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *contextmodel.ReqContext) respon
|
||||
saveCmd.Dashboard = version.Data
|
||||
saveCmd.Dashboard.Set("version", dash.Version)
|
||||
saveCmd.Dashboard.Set("uid", dash.UID)
|
||||
saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version)
|
||||
saveCmd.Message = dashverimpl.DashboardRestoreMessage(version.Version)
|
||||
// nolint:staticcheck
|
||||
saveCmd.FolderID = dash.FolderID
|
||||
metrics.MFolderIDsAPICount.WithLabelValues(metrics.RestoreDashboardVersion).Inc()
|
||||
|
||||
@@ -212,10 +212,10 @@ func (hs *HTTPServer) getDashboardACL(ctx context.Context, user identity.Request
|
||||
FolderID: dashboard.FolderID, // nolint:staticcheck
|
||||
Created: p.Created,
|
||||
Updated: p.Updated,
|
||||
UserID: p.UserId,
|
||||
UserID: p.UserID,
|
||||
UserLogin: p.UserLogin,
|
||||
UserEmail: p.UserEmail,
|
||||
TeamID: p.TeamId,
|
||||
TeamID: p.TeamID,
|
||||
TeamEmail: p.TeamEmail,
|
||||
Team: p.Team,
|
||||
Role: role,
|
||||
|
||||
@@ -58,8 +58,8 @@ func TestHTTPServer_GetDashboardPermissionList(t *testing.T) {
|
||||
hs.DashboardService = svc
|
||||
hs.dashboardPermissionsService = &actest.FakePermissionsService{
|
||||
ExpectedPermissions: []accesscontrol.ResourcePermission{
|
||||
{UserId: 1, UserLogin: "regular", IsManaged: true},
|
||||
{UserId: 2, UserLogin: "hidden", IsManaged: true},
|
||||
{UserID: 1, UserLogin: "regular", IsManaged: true},
|
||||
{UserID: 2, UserLogin: "hidden", IsManaged: true},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -77,7 +77,7 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *contextmodel.ReqContext) {
|
||||
|
||||
// Do not check permissions when the instance snapshot public mode is enabled
|
||||
if !hs.Cfg.SnapshotPublicMode {
|
||||
evaluator := ac.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(cmd.Dashboard.GetNestedString("uid")))
|
||||
evaluator := ac.EvalAll(ac.EvalPermission(dashboards.ActionSnapshotsCreate), ac.EvalPermission(dashboards.ActionDashboardsRead, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(cmd.Dashboard.GetNestedString("uid"))))
|
||||
if canSave, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator); err != nil || !canSave {
|
||||
c.JsonApiErr(http.StatusForbidden, "forbidden", err)
|
||||
return
|
||||
|
||||
@@ -15,9 +15,9 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/db/dbtest"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -40,18 +40,18 @@ func TestHTTPServer_DeleteDashboardSnapshot(t *testing.T) {
|
||||
|
||||
hs.DashboardService = svc
|
||||
|
||||
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
|
||||
guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService)
|
||||
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
|
||||
guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger())
|
||||
})
|
||||
}
|
||||
|
||||
allowedUser := userWithPermissions(1, []accesscontrol.Permission{
|
||||
{Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:1"},
|
||||
{Action: dashboards.ActionSnapshotsDelete},
|
||||
})
|
||||
|
||||
t.Run("User should not be able to delete snapshot without permissions", func(t *testing.T) {
|
||||
svc := dashboards.NewFakeDashboardService(t)
|
||||
svc.On("GetDashboard", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{UID: "1"}, nil)
|
||||
server := setup(t, svc, 0, "")
|
||||
|
||||
res, err := server.Send(webtest.RequestWithSignedInUser(
|
||||
@@ -116,7 +116,7 @@ func TestHTTPServer_DeleteDashboardSnapshot(t *testing.T) {
|
||||
server := setup(t, svc, 1, "")
|
||||
res, err := server.Send(webtest.RequestWithSignedInUser(
|
||||
server.NewRequest(http.MethodDelete, "/api/snapshots/12345", nil),
|
||||
&user.SignedInUser{UserID: 1, OrgID: 1},
|
||||
allowedUser,
|
||||
))
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -406,7 +406,7 @@ func setUpSnapshotTest(t *testing.T, userId int64, deleteUrl string) dashboardsn
|
||||
res.External = true
|
||||
res.ExternalDeleteURL = deleteUrl
|
||||
}
|
||||
dashSnapSvc.On("GetDashboardSnapshot", mock.Anything, mock.AnythingOfType("*dashboardsnapshots.GetDashboardSnapshotQuery")).Return(res, nil)
|
||||
dashSnapSvc.On("GetDashboardSnapshot", mock.Anything, mock.AnythingOfType("*dashboardsnapshots.GetDashboardSnapshotQuery")).Return(res, nil).Maybe()
|
||||
dashSnapSvc.On("DeleteDashboardSnapshot", mock.Anything, mock.AnythingOfType("*dashboardsnapshots.DeleteDashboardSnapshotCommand")).Return(nil).Maybe()
|
||||
return dashSnapSvc
|
||||
}
|
||||
|
||||
+32
-31
@@ -30,7 +30,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/client"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
@@ -55,12 +55,14 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/publicdashboards/api"
|
||||
publicdashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/star/startest"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
|
||||
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"github.com/grafana/grafana/pkg/web/webtest"
|
||||
)
|
||||
@@ -131,7 +133,7 @@ func newTestLive(t *testing.T, store db.DB) *live.GrafanaLive {
|
||||
nil,
|
||||
&usagestats.UsageStatsMock{T: t},
|
||||
nil,
|
||||
features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()), &dashboards.FakeDashboardService{}, annotationstest.NewFakeAnnotationsRepo(), nil)
|
||||
features, acimpl.ProvideAccessControl(features), &dashboards.FakeDashboardService{}, annotationstest.NewFakeAnnotationsRepo(), nil)
|
||||
require.NoError(t, err)
|
||||
return gLive
|
||||
}
|
||||
@@ -148,11 +150,11 @@ func TestHTTPServer_GetDashboard_AccessControl(t *testing.T) {
|
||||
hs.DashboardService = dashSvc
|
||||
|
||||
hs.Cfg = setting.NewCfg()
|
||||
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
|
||||
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
|
||||
hs.starService = startest.NewStarServiceFake()
|
||||
hs.dashboardProvisioningService = mockDashboardProvisioningService{}
|
||||
|
||||
guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService)
|
||||
guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -263,24 +265,22 @@ func TestHTTPServer_DeleteDashboardByUID_AccessControl(t *testing.T) {
|
||||
|
||||
dashSvc := dashboards.NewFakeDashboardService(t)
|
||||
dashSvc.On("GetDashboard", mock.Anything, mock.Anything).Return(dash, nil).Maybe()
|
||||
dashSvc.On("DeleteDashboard", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
|
||||
dashSvc.On("DeleteDashboard", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
|
||||
hs.DashboardService = dashSvc
|
||||
|
||||
hs.Cfg = setting.NewCfg()
|
||||
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
|
||||
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
|
||||
hs.starService = startest.NewStarServiceFake()
|
||||
|
||||
hs.LibraryPanelService = &mockLibraryPanelService{}
|
||||
hs.LibraryElementService = &libraryelementsfake.LibraryElementService{}
|
||||
|
||||
pubDashService := publicdashboards.NewFakePublicDashboardService(t)
|
||||
pubDashService.On("DeleteByDashboard", mock.Anything, mock.Anything).Return(nil).Maybe()
|
||||
middleware := publicdashboards.NewFakePublicDashboardMiddleware(t)
|
||||
license := licensingtest.NewFakeLicensing()
|
||||
license.On("FeatureEnabled", publicdashboardModels.FeaturePublicDashboardsEmailSharing).Return(false)
|
||||
hs.PublicDashboardsApi = api.ProvideApi(pubDashService, nil, hs.AccessControl, featuremgmt.WithFeatures(), middleware, hs.Cfg, license)
|
||||
hs.PublicDashboardsApi = api.ProvideApi(nil, nil, hs.AccessControl, featuremgmt.WithFeatures(), middleware, hs.Cfg, license)
|
||||
|
||||
guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService)
|
||||
guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger())
|
||||
})
|
||||
}
|
||||
deleteDashboard := func(server *webtest.Server, permissions []accesscontrol.Permission) (*http.Response, error) {
|
||||
@@ -323,7 +323,7 @@ func TestHTTPServer_GetDashboardVersions_AccessControl(t *testing.T) {
|
||||
hs.DashboardService = dashSvc
|
||||
|
||||
hs.Cfg = setting.NewCfg()
|
||||
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
|
||||
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
|
||||
hs.starService = startest.NewStarServiceFake()
|
||||
|
||||
hs.dashboardVersionService = &dashvertest.FakeDashboardVersionService{
|
||||
@@ -331,7 +331,7 @@ func TestHTTPServer_GetDashboardVersions_AccessControl(t *testing.T) {
|
||||
ExpectedDashboardVersion: &dashver.DashboardVersionDTO{},
|
||||
}
|
||||
|
||||
guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService)
|
||||
guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -476,13 +476,10 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
||||
{SaveError: dashboards.ErrDashboardNotFound, ExpectedStatusCode: http.StatusNotFound},
|
||||
{SaveError: dashboards.ErrFolderNotFound, ExpectedStatusCode: http.StatusBadRequest},
|
||||
{SaveError: dashboards.ErrDashboardWithSameUIDExists, ExpectedStatusCode: http.StatusBadRequest},
|
||||
{SaveError: dashboards.ErrDashboardWithSameNameInFolderExists, ExpectedStatusCode: http.StatusPreconditionFailed},
|
||||
{SaveError: dashboards.ErrDashboardVersionMismatch, ExpectedStatusCode: http.StatusPreconditionFailed},
|
||||
{SaveError: dashboards.ErrDashboardTitleEmpty, ExpectedStatusCode: http.StatusBadRequest},
|
||||
{SaveError: dashboards.ErrDashboardFolderCannotHaveParent, ExpectedStatusCode: http.StatusBadRequest},
|
||||
{SaveError: dashboards.ErrDashboardTypeMismatch, ExpectedStatusCode: http.StatusBadRequest},
|
||||
{SaveError: dashboards.ErrDashboardFolderWithSameNameAsDashboard, ExpectedStatusCode: http.StatusBadRequest},
|
||||
{SaveError: dashboards.ErrDashboardWithSameNameAsFolder, ExpectedStatusCode: http.StatusBadRequest},
|
||||
{SaveError: dashboards.ErrDashboardFolderNameExists, ExpectedStatusCode: http.StatusBadRequest},
|
||||
{SaveError: dashboards.ErrDashboardUpdateAccessDenied, ExpectedStatusCode: http.StatusForbidden},
|
||||
{SaveError: dashboards.ErrDashboardInvalidUid, ExpectedStatusCode: http.StatusBadRequest},
|
||||
@@ -745,14 +742,14 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) {
|
||||
},
|
||||
}
|
||||
getHS(&usertest.FakeUserService{
|
||||
ExpectedUser: &user.User{ID: 1, Login: "test-user"},
|
||||
ExpectedSignedInUser: &user.SignedInUser{Login: "test-user"},
|
||||
}).callGetDashboardVersions(sc)
|
||||
|
||||
assert.Equal(t, http.StatusOK, sc.resp.Code)
|
||||
var versions []dashver.DashboardVersionMeta
|
||||
var versions dashver.DashboardVersionResponseMeta
|
||||
err := json.NewDecoder(sc.resp.Body).Decode(&versions)
|
||||
require.NoError(t, err)
|
||||
for _, v := range versions {
|
||||
for _, v := range versions.Versions {
|
||||
assert.Equal(t, "test-user", v.CreatedBy)
|
||||
}
|
||||
}, mockSQLStore)
|
||||
@@ -775,10 +772,10 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) {
|
||||
}).callGetDashboardVersions(sc)
|
||||
|
||||
assert.Equal(t, http.StatusOK, sc.resp.Code)
|
||||
var versions []dashver.DashboardVersionMeta
|
||||
var versions dashver.DashboardVersionResponseMeta
|
||||
err := json.NewDecoder(sc.resp.Body).Decode(&versions)
|
||||
require.NoError(t, err)
|
||||
for _, v := range versions {
|
||||
for _, v := range versions.Versions {
|
||||
assert.Equal(t, anonString, v.CreatedBy)
|
||||
}
|
||||
}, mockSQLStore)
|
||||
@@ -801,10 +798,10 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) {
|
||||
}).callGetDashboardVersions(sc)
|
||||
|
||||
assert.Equal(t, http.StatusOK, sc.resp.Code)
|
||||
var versions []dashver.DashboardVersionMeta
|
||||
var versions dashver.DashboardVersionResponseMeta
|
||||
err := json.NewDecoder(sc.resp.Body).Decode(&versions)
|
||||
require.NoError(t, err)
|
||||
for _, v := range versions {
|
||||
for _, v := range versions.Versions {
|
||||
assert.Equal(t, anonString, v.CreatedBy)
|
||||
}
|
||||
}, mockSQLStore)
|
||||
@@ -821,8 +818,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
|
||||
var err error
|
||||
if dashboardStore == nil {
|
||||
sql, cfg := db.InitTestDBWithCfg(t)
|
||||
quotaService := quotatest.New(false, nil)
|
||||
dashboardStore, err = database.ProvideDashboardStore(sql, cfg, features, tagimpl.ProvideService(sql), quotaService)
|
||||
dashboardStore, err = database.ProvideDashboardStore(sql, cfg, features, tagimpl.ProvideService(sql))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -835,20 +831,25 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
|
||||
|
||||
db := db.InitTestDB(t)
|
||||
fStore := folderimpl.ProvideStore(db)
|
||||
folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||
dashboardStore, folderStore, db, features, cfg, folderPermissions,
|
||||
supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
quotaService := quotatest.New(false, nil)
|
||||
folderSvc := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
|
||||
nil, db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil,
|
||||
dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
if dashboardService == nil {
|
||||
dashboardService, err = service.ProvideDashboardServiceImpl(
|
||||
cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions,
|
||||
ac, folderSvc, fStore, nil,
|
||||
cfg, dashboardStore, folderStore, features, folderPermissions,
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil,
|
||||
dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
dashboardService.(dashboards.PermissionsRegistrationService).RegisterDashboardPermissions(dashboardPermissions)
|
||||
}
|
||||
|
||||
dashboardProvisioningService, err := service.ProvideDashboardServiceImpl(
|
||||
cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions,
|
||||
ac, folderSvc, fStore, nil,
|
||||
cfg, dashboardStore, folderStore, features, folderPermissions,
|
||||
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil,
|
||||
dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
+14
-109
@@ -6,14 +6,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/prometheus/prometheus/promql/parser"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/datasource"
|
||||
@@ -22,10 +18,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
@@ -339,7 +333,7 @@ func validateURL(cmdType string, url string) response.Response {
|
||||
// validateJSONData prevents the user from adding a custom header with name that matches the auth proxy header name.
|
||||
// This is done to prevent data source proxy from being used to circumvent auth proxy.
|
||||
// For more context take a look at CVE-2022-35957
|
||||
func validateJSONData(ctx context.Context, jsonData *simplejson.Json, cfg *setting.Cfg, features featuremgmt.FeatureToggles) error {
|
||||
func validateJSONData(jsonData *simplejson.Json, cfg *setting.Cfg) error {
|
||||
if jsonData == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -356,69 +350,9 @@ func validateJSONData(ctx context.Context, jsonData *simplejson.Json, cfg *setti
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent adding a data source team header with a name that matches the auth proxy header name
|
||||
if features.IsEnabled(ctx, featuremgmt.FlagTeamHttpHeaders) {
|
||||
err := validateTeamHTTPHeaderJSON(jsonData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// we only allow for now the following headers to be added to a data source team header
|
||||
var validHeaders = []string{"X-Prom-Label-Policy"}
|
||||
|
||||
func validateTeamHTTPHeaderJSON(jsonData *simplejson.Json) error {
|
||||
teamHTTPHeadersJSON, err := datasources.GetTeamHTTPHeaders(jsonData)
|
||||
if err != nil {
|
||||
datasourcesLogger.Error("Unable to marshal TeamHTTPHeaders")
|
||||
return errors.New("validation error, invalid format of TeamHTTPHeaders")
|
||||
}
|
||||
if teamHTTPHeadersJSON == nil {
|
||||
return nil
|
||||
}
|
||||
// whitelisting ValidHeaders
|
||||
// each teams headers
|
||||
for _, teamheaders := range teamHTTPHeadersJSON.Headers {
|
||||
for _, header := range teamheaders {
|
||||
if !slices.ContainsFunc(validHeaders, func(v string) bool {
|
||||
return http.CanonicalHeaderKey(v) == http.CanonicalHeaderKey(header.Header)
|
||||
}) {
|
||||
datasourcesLogger.Error("Cannot add a data source team header that is different than", "headerName", header.Header)
|
||||
return errors.New("validation error, invalid header name specified")
|
||||
}
|
||||
if !validateLBACHeader(header.Value) {
|
||||
datasourcesLogger.Error("Cannot add a data source team header value with invalid value", "headerValue", header.Value)
|
||||
return errors.New("validation error, invalid header value syntax")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateLBACHeader returns true if the header value matches the syntax
|
||||
// 1234:{ name!="value",foo!~"bar" }
|
||||
func validateLBACHeader(headervalue string) bool {
|
||||
exp := `^\d+:(.+)`
|
||||
pattern, err := regexp.Compile(exp)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
match := pattern.FindSubmatch([]byte(strings.TrimSpace(headervalue)))
|
||||
if match == nil || len(match) < 2 {
|
||||
return false
|
||||
}
|
||||
_, err = parser.ParseMetricSelector(string(match[1]))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func evaluateTeamHTTPHeaderPermissions(hs *HTTPServer, c *contextmodel.ReqContext, scope string) (bool, error) {
|
||||
ev := ac.EvalPermission(datasources.ActionPermissionsWrite, ac.Scope(scope))
|
||||
return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, ev)
|
||||
}
|
||||
|
||||
// swagger:route POST /datasources datasources addDataSource
|
||||
//
|
||||
// Create a data source.
|
||||
@@ -453,10 +387,18 @@ func (hs *HTTPServer) AddDataSource(c *contextmodel.ReqContext) response.Respons
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateJSONData(c.Req.Context(), cmd.JsonData, hs.Cfg, hs.Features); err != nil {
|
||||
if err := validateJSONData(cmd.JsonData, hs.Cfg); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "Failed to add datasource", err)
|
||||
}
|
||||
|
||||
// It's forbidden to update the rules from the datasource api.
|
||||
// team HTTP headers update have to be done through `updateDatasourceLBACRules`
|
||||
if cmd.JsonData != nil {
|
||||
if _, ok := cmd.JsonData.CheckGet("teamHttpHeaders"); ok {
|
||||
return response.Error(http.StatusForbidden, "Cannot create datasource with team HTTP headers, need to use updateDatasourceLBACRules API", nil)
|
||||
}
|
||||
}
|
||||
|
||||
dataSource, err := hs.DataSourcesService.AddDataSource(c.Req.Context(), &cmd)
|
||||
if err != nil {
|
||||
if errors.Is(err, datasources.ErrDataSourceNameExists) || errors.Is(err, datasources.ErrDataSourceUidExists) {
|
||||
@@ -518,7 +460,7 @@ func (hs *HTTPServer) UpdateDataSourceByID(c *contextmodel.ReqContext) response.
|
||||
if resp := validateURL(cmd.Type, cmd.URL); resp != nil {
|
||||
return resp
|
||||
}
|
||||
if err := validateJSONData(c.Req.Context(), cmd.JsonData, hs.Cfg, hs.Features); err != nil {
|
||||
if err := validateJSONData(cmd.JsonData, hs.Cfg); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "Failed to update datasource", err)
|
||||
}
|
||||
ds, err := hs.getRawDataSourceById(c.Req.Context(), cmd.ID, cmd.OrgID)
|
||||
@@ -529,12 +471,6 @@ func (hs *HTTPServer) UpdateDataSourceByID(c *contextmodel.ReqContext) response.
|
||||
return response.Error(http.StatusInternalServerError, "Failed to update datasource", err)
|
||||
}
|
||||
|
||||
// check if LBAC rules have been modified
|
||||
hasAccess, errAccess := checkTeamHTTPHeaderPermissions(hs, c, ds, cmd)
|
||||
if !hasAccess {
|
||||
return response.Error(http.StatusForbidden, fmt.Sprintf("You'll need additional permissions to perform this action. Permissions needed: %s", datasources.ActionPermissionsWrite), errAccess)
|
||||
}
|
||||
|
||||
return hs.updateDataSourceByID(c, ds, cmd)
|
||||
}
|
||||
|
||||
@@ -553,6 +489,7 @@ func (hs *HTTPServer) UpdateDataSourceByID(c *contextmodel.ReqContext) response.
|
||||
// 200: createOrUpdateDatasourceResponse
|
||||
// 401: unauthorisedError
|
||||
// 403: forbiddenError
|
||||
// 409: conflictError
|
||||
// 500: internalServerError
|
||||
func (hs *HTTPServer) UpdateDataSourceByUID(c *contextmodel.ReqContext) response.Response {
|
||||
cmd := datasources.UpdateDataSourceCommand{}
|
||||
@@ -564,7 +501,7 @@ func (hs *HTTPServer) UpdateDataSourceByUID(c *contextmodel.ReqContext) response
|
||||
if resp := validateURL(cmd.Type, cmd.URL); resp != nil {
|
||||
return resp
|
||||
}
|
||||
if err := validateJSONData(c.Req.Context(), cmd.JsonData, hs.Cfg, hs.Features); err != nil {
|
||||
if err := validateJSONData(cmd.JsonData, hs.Cfg); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "Failed to update datasource", err)
|
||||
}
|
||||
|
||||
@@ -577,36 +514,9 @@ func (hs *HTTPServer) UpdateDataSourceByUID(c *contextmodel.ReqContext) response
|
||||
}
|
||||
cmd.ID = ds.ID
|
||||
|
||||
// check if LBAC rules have been modified
|
||||
hasAccess, errAccess := checkTeamHTTPHeaderPermissions(hs, c, ds, cmd)
|
||||
if !hasAccess {
|
||||
return response.Error(http.StatusForbidden, fmt.Sprintf("You'll need additional permissions to perform this action. Permissions needed: %s", datasources.ActionPermissionsWrite), errAccess)
|
||||
}
|
||||
|
||||
return hs.updateDataSourceByID(c, ds, cmd)
|
||||
}
|
||||
|
||||
func getEncodedString(jsonData *simplejson.Json, key string) string {
|
||||
if jsonData == nil {
|
||||
return ""
|
||||
}
|
||||
jsonValues, exists := jsonData.CheckGet(key)
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
val, _ := jsonValues.Encode()
|
||||
return string(val)
|
||||
}
|
||||
|
||||
func checkTeamHTTPHeaderPermissions(hs *HTTPServer, c *contextmodel.ReqContext, ds *datasources.DataSource, cmd datasources.UpdateDataSourceCommand) (bool, error) {
|
||||
currentTeamHTTPHeaders := getEncodedString(ds.JsonData, "teamHttpHeaders")
|
||||
newTeamHTTPHeaders := getEncodedString(cmd.JsonData, "teamHttpHeaders")
|
||||
if (currentTeamHTTPHeaders != "" || newTeamHTTPHeaders != "") && currentTeamHTTPHeaders != newTeamHTTPHeaders {
|
||||
return evaluateTeamHTTPHeaderPermissions(hs, c, datasources.ScopePrefix+ds.UID)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) updateDataSourceByID(c *contextmodel.ReqContext, ds *datasources.DataSource, cmd datasources.UpdateDataSourceCommand) response.Response {
|
||||
if ds.ReadOnly {
|
||||
return response.Error(http.StatusForbidden, "Cannot update read-only data source", nil)
|
||||
@@ -928,12 +838,7 @@ func (hs *HTTPServer) checkDatasourceHealth(c *contextmodel.ReqContext, ds *data
|
||||
Headers: map[string]string{},
|
||||
}
|
||||
|
||||
var dsURL string
|
||||
if req.PluginContext.DataSourceInstanceSettings != nil {
|
||||
dsURL = req.PluginContext.DataSourceInstanceSettings.URL
|
||||
}
|
||||
|
||||
err = hs.PluginRequestValidator.Validate(dsURL, c.Req)
|
||||
err = hs.DataSourceRequestValidator.Validate(ds, c.Req)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusForbidden, "Access denied", err)
|
||||
}
|
||||
|
||||
+49
-125
@@ -19,7 +19,6 @@ import (
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/datasources/guardian"
|
||||
@@ -116,7 +115,7 @@ func TestAddDataSource_URLWithoutProtocol(t *testing.T) {
|
||||
expectedDatasource: &datasources.DataSource{},
|
||||
},
|
||||
Cfg: setting.NewCfg(),
|
||||
AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()),
|
||||
AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures()),
|
||||
accesscontrolService: actest.FakeService{},
|
||||
}
|
||||
|
||||
@@ -223,104 +222,58 @@ func TestUpdateDataSource_InvalidJSONData(t *testing.T) {
|
||||
|
||||
assert.Equal(t, 400, sc.resp.Code)
|
||||
}
|
||||
|
||||
// Using a team HTTP header whose name matches the name specified for auth proxy header should fail
|
||||
func TestUpdateDataSourceTeamHTTPHeaders_InvalidJSONData(t *testing.T) {
|
||||
func TestAddDataSourceTeamHTTPHeaders(t *testing.T) {
|
||||
tenantID := "1234"
|
||||
testcases := []struct {
|
||||
desc string
|
||||
data datasources.TeamHTTPHeaders
|
||||
want int
|
||||
}{
|
||||
{
|
||||
desc: "We should only allow for headers being X-Prom-Label-Policy",
|
||||
data: datasources.TeamHTTPHeaders{
|
||||
Headers: datasources.TeamHeaders{
|
||||
tenantID: []datasources.TeamHTTPHeader{
|
||||
{
|
||||
Header: "Authorization",
|
||||
Value: "foo!=bar",
|
||||
},
|
||||
},
|
||||
}},
|
||||
want: 400,
|
||||
hs := &HTTPServer{
|
||||
DataSourcesService: &dataSourcesServiceMock{
|
||||
expectedDatasource: &datasources.DataSource{},
|
||||
},
|
||||
{
|
||||
desc: "Allowed header but no team id",
|
||||
data: datasources.TeamHTTPHeaders{
|
||||
Headers: datasources.TeamHeaders{"": []datasources.TeamHTTPHeader{
|
||||
{
|
||||
Header: "X-Prom-Label-Policy",
|
||||
Value: "foo=bar",
|
||||
},
|
||||
},
|
||||
}},
|
||||
want: 400,
|
||||
},
|
||||
{
|
||||
desc: "Allowed team id and header name with invalid header values ",
|
||||
data: datasources.TeamHTTPHeaders{
|
||||
Headers: datasources.TeamHeaders{tenantID: []datasources.TeamHTTPHeader{
|
||||
{
|
||||
Header: "X-Prom-Label-Policy",
|
||||
Value: "Bad value",
|
||||
},
|
||||
},
|
||||
}},
|
||||
want: 400,
|
||||
},
|
||||
// Complete valid case, with team id, header name and header value
|
||||
{
|
||||
desc: "Allowed header and header values ",
|
||||
data: datasources.TeamHTTPHeaders{
|
||||
Headers: datasources.TeamHeaders{tenantID: []datasources.TeamHTTPHeader{
|
||||
{
|
||||
Header: "X-Prom-Label-Policy",
|
||||
Value: `1234:{ name!="value",foo!~"bar" }`,
|
||||
},
|
||||
},
|
||||
}},
|
||||
want: 200,
|
||||
Cfg: setting.NewCfg(),
|
||||
Features: featuremgmt.WithFeatures(),
|
||||
accesscontrolService: actest.FakeService{},
|
||||
AccessControl: actest.FakeAccessControl{
|
||||
ExpectedEvaluate: true,
|
||||
ExpectedErr: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
hs := &HTTPServer{
|
||||
DataSourcesService: &dataSourcesServiceMock{
|
||||
expectedDatasource: &datasources.DataSource{},
|
||||
sc := setupScenarioContext(t, fmt.Sprintf("/api/datasources/%s", tenantID))
|
||||
hs.Cfg.AuthProxy.Enabled = true
|
||||
|
||||
jsonData := simplejson.New()
|
||||
jsonData.Set("teamHttpHeaders", datasources.TeamHTTPHeaders{
|
||||
Headers: datasources.TeamHeaders{
|
||||
tenantID: []datasources.TeamHTTPHeader{
|
||||
{
|
||||
Header: "Authorization",
|
||||
Value: "foo!=bar",
|
||||
},
|
||||
Cfg: setting.NewCfg(),
|
||||
Features: featuremgmt.WithFeatures(featuremgmt.FlagTeamHttpHeaders),
|
||||
accesscontrolService: actest.FakeService{},
|
||||
AccessControl: actest.FakeAccessControl{
|
||||
ExpectedEvaluate: true,
|
||||
ExpectedErr: nil,
|
||||
},
|
||||
}
|
||||
sc := setupScenarioContext(t, fmt.Sprintf("/api/datasources/%s", tenantID))
|
||||
hs.Cfg.AuthProxy.Enabled = true
|
||||
|
||||
jsonData := simplejson.New()
|
||||
jsonData.Set("teamHttpHeaders", tc.data)
|
||||
sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
|
||||
c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{
|
||||
Name: "Test",
|
||||
URL: "localhost:5432",
|
||||
Access: "direct",
|
||||
Type: "test",
|
||||
JsonData: jsonData,
|
||||
})
|
||||
c.SignedInUser = authedUserWithPermissions(1, 1, []ac.Permission{
|
||||
{Action: datasources.ActionPermissionsWrite, Scope: datasources.ScopeAll},
|
||||
})
|
||||
return hs.AddDataSource(c)
|
||||
}))
|
||||
|
||||
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
|
||||
|
||||
assert.Equal(t, tc.want, sc.resp.Code)
|
||||
},
|
||||
},
|
||||
})
|
||||
sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
|
||||
c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{
|
||||
Name: "Test",
|
||||
URL: "localhost:5432",
|
||||
Access: "direct",
|
||||
Type: "test",
|
||||
JsonData: jsonData,
|
||||
})
|
||||
}
|
||||
c.SignedInUser = authedUserWithPermissions(1, 1, []ac.Permission{
|
||||
{Action: datasources.ActionPermissionsWrite, Scope: datasources.ScopeAll},
|
||||
})
|
||||
return hs.AddDataSource(c)
|
||||
}))
|
||||
|
||||
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
|
||||
assert.Equal(t, http.StatusForbidden, sc.resp.Code)
|
||||
|
||||
// Parse the JSON response
|
||||
var response map[string]string
|
||||
err := json.Unmarshal(sc.resp.Body.Bytes(), &response)
|
||||
assert.NoError(t, err, "Failed to parse JSON response")
|
||||
|
||||
// Check the error message in the JSON response
|
||||
assert.Equal(t, "Cannot create datasource with team HTTP headers, need to use updateDatasourceLBACRules API", response["message"])
|
||||
}
|
||||
|
||||
// Updating data sources with URLs not specifying protocol should work.
|
||||
@@ -333,7 +286,7 @@ func TestUpdateDataSource_URLWithoutProtocol(t *testing.T) {
|
||||
expectedDatasource: &datasources.DataSource{},
|
||||
},
|
||||
Cfg: setting.NewCfg(),
|
||||
AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()),
|
||||
AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures()),
|
||||
accesscontrolService: actest.FakeService{},
|
||||
}
|
||||
|
||||
@@ -366,7 +319,7 @@ func TestUpdateDataSourceByID_DataSourceNameExists(t *testing.T) {
|
||||
},
|
||||
},
|
||||
Cfg: setting.NewCfg(),
|
||||
AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()),
|
||||
AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures()),
|
||||
accesscontrolService: actest.FakeService{},
|
||||
Live: newTestLive(t, nil),
|
||||
}
|
||||
@@ -493,35 +446,6 @@ func TestAPI_datasources_AccessControl(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLBACHeader(t *testing.T) {
|
||||
testcases := []struct {
|
||||
desc string
|
||||
teamHeaderValue string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
desc: "Should allow valid header",
|
||||
teamHeaderValue: `1234:{ name!="value",foo!~"bar" }`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
desc: "Should allow valid selector",
|
||||
teamHeaderValue: `1234:{ name!="value",foo!~"bar/baz.foo" }`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
desc: "Should return false for incorrect header value",
|
||||
teamHeaderValue: `1234:!="value",foo!~"bar" }`,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
assert.Equal(t, tc.want, validateLBACHeader(tc.teamHeaderValue))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type dataSourcesServiceMock struct {
|
||||
datasources.DataSourceService
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/web/webtest"
|
||||
)
|
||||
|
||||
type fakePluginRequestValidator struct {
|
||||
type fakeDataSourceRequestValidator struct {
|
||||
err error
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ type secretsErrorResponseBody struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (rv *fakePluginRequestValidator) Validate(dsURL string, req *http.Request) error {
|
||||
func (rv *fakeDataSourceRequestValidator) Validate(ds *datasources.DataSource, req *http.Request) error {
|
||||
return rv.err
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) {
|
||||
cfg,
|
||||
nil,
|
||||
nil,
|
||||
&fakePluginRequestValidator{},
|
||||
&fakeDataSourceRequestValidator{},
|
||||
&fakePluginClient{
|
||||
QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
resp := backend.Responses{
|
||||
@@ -114,7 +114,7 @@ func TestAPIEndpoint_Metrics_PluginDecryptionFailure(t *testing.T) {
|
||||
cfg,
|
||||
nil,
|
||||
nil,
|
||||
&fakePluginRequestValidator{},
|
||||
&fakeDataSourceRequestValidator{},
|
||||
&fakePluginClient{
|
||||
QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
resp := backend.Responses{
|
||||
@@ -249,7 +249,7 @@ func TestDataSourceQueryError(t *testing.T) {
|
||||
expectedStatus: errutil.StatusInternal.HTTPStatus(),
|
||||
expectedBody: body{
|
||||
Message: "An error occurred within the plugin",
|
||||
MessageId: "plugin.downstreamError",
|
||||
MessageId: "plugin.requestFailureError",
|
||||
StatusCode: 500,
|
||||
},
|
||||
},
|
||||
@@ -309,7 +309,7 @@ func TestDataSourceQueryError(t *testing.T) {
|
||||
cfg,
|
||||
&fakeDatasources.FakeCacheService{},
|
||||
nil,
|
||||
&fakePluginRequestValidator{},
|
||||
&fakeDataSourceRequestValidator{},
|
||||
pluginClient.ProvideService(r),
|
||||
plugincontext.ProvideService(cfg, localcache.ProvideService(), &pluginstore.FakePluginStore{
|
||||
PluginList: []pluginstore.Plugin{pluginstore.ToGrafanaDTO(p)},
|
||||
|
||||
@@ -54,10 +54,10 @@ type CalculateDiffOptions struct {
|
||||
|
||||
type CalculateDiffTarget struct {
|
||||
DashboardId int64 `json:"dashboardId"`
|
||||
Version int `json:"version"`
|
||||
Version int64 `json:"version"`
|
||||
UnsavedDashboard *simplejson.Json `json:"unsavedDashboard"`
|
||||
}
|
||||
|
||||
type RestoreDashboardVersionCommand struct {
|
||||
Version int `json:"version" binding:"Required"`
|
||||
Version int64 `json:"version" binding:"Required"`
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ type Folder struct {
|
||||
ParentUID string `json:"parentUid,omitempty"`
|
||||
// the parent folders starting from the root going down
|
||||
Parents []Folder `json:"parents,omitempty"`
|
||||
|
||||
// When the folder belongs to a repository
|
||||
// NOTE: this is only populated when folders are managed by unified storage
|
||||
Repository string `json:"repository,omitempty"`
|
||||
}
|
||||
|
||||
type FolderSearchHit struct {
|
||||
@@ -35,4 +39,8 @@ type FolderSearchHit struct {
|
||||
UID string `json:"uid" xorm:"uid"`
|
||||
Title string `json:"title"`
|
||||
ParentUID string `json:"parentUid,omitempty"`
|
||||
|
||||
// When the folder belongs to a repository
|
||||
// NOTE: this is only populated when folders are managed by unified storage
|
||||
Repository string `json:"repository,omitempty"`
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ type FrontendSettingsAuthDTO struct {
|
||||
|
||||
DisableLogin bool `json:"disableLogin"`
|
||||
BasicAuthStrongPasswordPolicy bool `json:"basicAuthStrongPasswordPolicy"`
|
||||
PasswordlessEnabled bool `json:"passwordlessEnabled"`
|
||||
}
|
||||
|
||||
type FrontendSettingsBuildInfoDTO struct {
|
||||
@@ -194,6 +195,10 @@ type FrontendSettingsDTO struct {
|
||||
RudderstackConfigUrl string `json:"rudderstackConfigUrl"`
|
||||
RudderstackIntegrationsUrl string `json:"rudderstackIntegrationsUrl"`
|
||||
|
||||
AnalyticsConsoleReporting bool `json:"analyticsConsoleReporting"`
|
||||
|
||||
DashboardPerformanceMetrics []string `json:"dashboardPerformanceMetrics"`
|
||||
|
||||
FeedbackLinksEnabled bool `json:"feedbackLinksEnabled"`
|
||||
ApplicationInsightsConnectionString string `json:"applicationInsightsConnectionString"`
|
||||
ApplicationInsightsEndpointUrl string `json:"applicationInsightsEndpointUrl"`
|
||||
@@ -204,6 +209,8 @@ type FrontendSettingsDTO struct {
|
||||
ExternalUserMngInfo string `json:"externalUserMngInfo"`
|
||||
ExternalUserMngLinkUrl string `json:"externalUserMngLinkUrl"`
|
||||
ExternalUserMngLinkName string `json:"externalUserMngLinkName"`
|
||||
ExternalUserMngAnalytics bool `json:"externalUserMngAnalytics"`
|
||||
ExternalUserMngAnalyticsParams string `json:"externalUserMngAnalyticsParams"`
|
||||
ViewersCanEdit bool `json:"viewersCanEdit"`
|
||||
AngularSupportEnabled bool `json:"angularSupportEnabled"`
|
||||
EditorsCanAdmin bool `json:"editorsCanAdmin"`
|
||||
@@ -212,6 +219,7 @@ type FrontendSettingsDTO struct {
|
||||
CSPReportOnlyEnabled bool `json:"cspReportOnlyEnabled"`
|
||||
EnableFrontendSandboxForPlugins []string `json:"enableFrontendSandboxForPlugins"`
|
||||
ExploreDefaultTimeOffset string `json:"exploreDefaultTimeOffset"`
|
||||
ExploreHideLogsDownload bool `json:"exploreHideLogsDownload"`
|
||||
|
||||
Auth FrontendSettingsAuthDTO `json:"auth"`
|
||||
|
||||
@@ -247,6 +255,8 @@ type FrontendSettingsDTO struct {
|
||||
|
||||
Azure FrontendSettingsAzureDTO `json:"azure"`
|
||||
|
||||
DefaultDatasourceManageAlertsUIToggle bool `json:"defaultDatasourceManageAlertsUiToggle"`
|
||||
|
||||
Caching FrontendSettingsCachingDTO `json:"caching"`
|
||||
RecordedQueries FrontendSettingsRecordedQueriesDTO `json:"recordedQueries"`
|
||||
Reporting FrontendSettingsReportingDTO `json:"reporting"`
|
||||
@@ -259,6 +269,7 @@ type FrontendSettingsDTO struct {
|
||||
TokenExpirationDayLimit int `json:"tokenExpirationDayLimit"`
|
||||
SharedWithMeFolderUID string `json:"sharedWithMeFolderUID"`
|
||||
RootFolderUID string `json:"rootFolderUID"`
|
||||
PasswordlessEnabled string `json:"passwordlessEnabled"`
|
||||
|
||||
GeomapDefaultBaseLayerConfig *map[string]any `json:"geomapDefaultBaseLayerConfig,omitempty"`
|
||||
GeomapDisableCustomBaseLayer bool `json:"geomapDisableCustomBaseLayer"`
|
||||
|
||||
@@ -2,7 +2,7 @@ package dtos
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs"
|
||||
"github.com/grafana/grafana/pkg/plugins/auth"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ type PluginSetting struct {
|
||||
Id string `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Pinned bool `json:"pinned"`
|
||||
AutoEnabled bool `json:"autoEnabled"`
|
||||
Module string `json:"module"`
|
||||
BaseUrl string `json:"baseUrl"`
|
||||
Info plugins.Info `json:"info"`
|
||||
@@ -51,7 +52,7 @@ type PluginListItem struct {
|
||||
SignatureOrg string `json:"signatureOrg"`
|
||||
AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"`
|
||||
AngularDetected bool `json:"angularDetected"`
|
||||
IAM *pfs.IAM `json:"iam,omitempty"`
|
||||
IAM *auth.IAM `json:"iam,omitempty"`
|
||||
}
|
||||
|
||||
type PluginList []PluginListItem
|
||||
|
||||
@@ -43,6 +43,7 @@ type ResetUserPasswordForm struct {
|
||||
|
||||
type UserLookupDTO struct {
|
||||
UserID int64 `json:"userId"`
|
||||
UID string `json:"uid"`
|
||||
Login string `json:"login"`
|
||||
AvatarURL string `json:"avatarUrl"`
|
||||
}
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ func NewFakePluginInstaller() *fakePluginInstaller {
|
||||
return &fakePluginInstaller{plugins: map[string]fakePlugin{}}
|
||||
}
|
||||
|
||||
func (pm *fakePluginInstaller) Add(_ context.Context, pluginID, version string, _ plugins.CompatOpts) error {
|
||||
func (pm *fakePluginInstaller) Add(_ context.Context, pluginID, version string, _ plugins.AddOpts) error {
|
||||
pm.plugins[pluginID] = fakePlugin{
|
||||
pluginID: pluginID,
|
||||
version: version,
|
||||
|
||||
+56
-375
@@ -5,25 +5,15 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
claims "github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana/pkg/api/apierrors"
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/infra/slugify"
|
||||
internalfolders "github.com/grafana/grafana/pkg/registry/apis/folders"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
||||
@@ -31,10 +21,9 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements/model"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/util/errhttp"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
@@ -45,38 +34,24 @@ func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authoriz
|
||||
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
|
||||
idScope := dashboards.ScopeFoldersProvider.GetResourceScope(accesscontrol.Parameter(":id"))
|
||||
uidScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.Parameter(":uid"))
|
||||
folderRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead)), routing.Wrap(hs.GetFolders))
|
||||
folderRoute.Get("/id/:id", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, idScope)), routing.Wrap(hs.GetFolderByID))
|
||||
|
||||
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
|
||||
folderUidRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderByUID))
|
||||
folderUidRoute.Put("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.UpdateFolder))
|
||||
folderUidRoute.Post("/move", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.MoveFolder))
|
||||
folderUidRoute.Delete("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, uidScope)), routing.Wrap(hs.DeleteFolder))
|
||||
folderUidRoute.Get("/counts", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderDescendantCounts))
|
||||
|
||||
folderUidRoute.Group("/permissions", func(folderPermissionRoute routing.RouteRegister) {
|
||||
folderPermissionRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsRead, uidScope)), routing.Wrap(hs.GetFolderPermissionList))
|
||||
folderPermissionRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, uidScope)), routing.Wrap(hs.UpdateFolderPermissions))
|
||||
})
|
||||
})
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesFolders) {
|
||||
// Use k8s client to implement legacy API
|
||||
handler := newFolderK8sHandler(hs)
|
||||
folderRoute.Post("/", handler.createFolder)
|
||||
} else {
|
||||
folderRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersCreate)), routing.Wrap(hs.CreateFolder))
|
||||
}
|
||||
// Only adding support for some routes with the k8s handler for now. Include the rest here.
|
||||
if false {
|
||||
handler := newFolderK8sHandler(hs)
|
||||
folderRoute.Get("/", handler.searchFolders)
|
||||
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
|
||||
folderUidRoute.Get("/", handler.getFolder)
|
||||
folderUidRoute.Delete("/", handler.deleteFolder)
|
||||
folderUidRoute.Put("/:uid", handler.updateFolder)
|
||||
})
|
||||
}
|
||||
|
||||
folderRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersCreate)), routing.Wrap(hs.CreateFolder))
|
||||
folderRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead)), routing.Wrap(hs.GetFolders))
|
||||
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
|
||||
folderUidRoute.Put("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.UpdateFolder))
|
||||
folderUidRoute.Delete("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, uidScope)), routing.Wrap(hs.DeleteFolder))
|
||||
folderUidRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderByUID))
|
||||
folderUidRoute.Get("/counts", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, uidScope)), routing.Wrap(hs.GetFolderDescendantCounts))
|
||||
folderUidRoute.Post("/move", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, uidScope)), routing.Wrap(hs.MoveFolder))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -119,10 +94,11 @@ func (hs *HTTPServer) GetFolders(c *contextmodel.ReqContext) response.Response {
|
||||
hits := make([]dtos.FolderSearchHit, 0)
|
||||
for _, f := range folders {
|
||||
hits = append(hits, dtos.FolderSearchHit{
|
||||
ID: f.ID, // nolint:staticcheck
|
||||
UID: f.UID,
|
||||
Title: f.Title,
|
||||
ParentUID: f.ParentUID,
|
||||
ID: f.ID, // nolint:staticcheck
|
||||
UID: f.UID,
|
||||
Title: f.Title,
|
||||
ParentUID: f.ParentUID,
|
||||
Repository: f.Repository,
|
||||
})
|
||||
metrics.MFolderIDsAPICount.WithLabelValues(metrics.GetFolders).Inc()
|
||||
}
|
||||
@@ -223,6 +199,10 @@ func (hs *HTTPServer) CreateFolder(c *contextmodel.ReqContext) response.Response
|
||||
return apierrors.ToFolderErrorResponse(err)
|
||||
}
|
||||
|
||||
if err := hs.setDefaultFolderPermissions(c.Req.Context(), cmd.OrgID, cmd.SignedInUser, folder); err != nil {
|
||||
hs.log.Error("Could not set the default folder permissions", "folder", folder.Title, "user", cmd.SignedInUser, "error", err)
|
||||
}
|
||||
|
||||
// Clear permission cache for the user who's created the folder, so that new permissions are fetched for their next call
|
||||
// Required for cases when caller wants to immediately interact with the newly created object
|
||||
hs.accesscontrolService.ClearUserPermissionCache(c.SignedInUser)
|
||||
@@ -236,6 +216,36 @@ func (hs *HTTPServer) CreateFolder(c *contextmodel.ReqContext) response.Response
|
||||
return response.JSON(http.StatusOK, folderDTO)
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) setDefaultFolderPermissions(ctx context.Context, orgID int64, user identity.Requester, folder *folder.Folder) error {
|
||||
if !hs.Cfg.RBAC.PermissionsOnCreation("folder") {
|
||||
return nil
|
||||
}
|
||||
|
||||
var permissions []accesscontrol.SetResourcePermissionCommand
|
||||
|
||||
if user.IsIdentityType(claims.TypeUser) {
|
||||
userID, err := user.GetInternalID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{
|
||||
UserID: userID, Permission: dashboardaccess.PERMISSION_ADMIN.String(),
|
||||
})
|
||||
}
|
||||
|
||||
isNested := folder.ParentUID != ""
|
||||
if !isNested || !hs.Features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
|
||||
permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{
|
||||
{BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()},
|
||||
{BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()},
|
||||
}...)
|
||||
}
|
||||
|
||||
_, err := hs.folderPermissionsService.SetPermissions(ctx, orgID, folder.UID, permissions...)
|
||||
return err
|
||||
}
|
||||
|
||||
// swagger:route POST /folders/{folder_uid}/move folders moveFolder
|
||||
//
|
||||
// Move folder.
|
||||
@@ -381,10 +391,10 @@ func (hs *HTTPServer) newToFolderDto(c *contextmodel.ReqContext, f *folder.Folde
|
||||
// Finding creator and last updater of the folder
|
||||
updater, creator := anonString, anonString
|
||||
if f.CreatedBy > 0 {
|
||||
creator = hs.getUserLogin(ctx, f.CreatedBy)
|
||||
creator = hs.getIdentityName(ctx, f.OrgID, f.CreatedBy)
|
||||
}
|
||||
if f.UpdatedBy > 0 {
|
||||
updater = hs.getUserLogin(ctx, f.UpdatedBy)
|
||||
updater = hs.getIdentityName(ctx, f.OrgID, f.UpdatedBy)
|
||||
}
|
||||
|
||||
acMetadata, _ := hs.getFolderACMetadata(c, f)
|
||||
@@ -402,6 +412,7 @@ func (hs *HTTPServer) newToFolderDto(c *contextmodel.ReqContext, f *folder.Folde
|
||||
return dtos.Folder{
|
||||
ID: f.ID, // nolint:staticcheck
|
||||
UID: f.UID,
|
||||
OrgID: f.OrgID,
|
||||
Title: f.Title,
|
||||
URL: f.URL,
|
||||
HasACL: f.HasACL,
|
||||
@@ -416,6 +427,7 @@ func (hs *HTTPServer) newToFolderDto(c *contextmodel.ReqContext, f *folder.Folde
|
||||
Version: f.Version,
|
||||
AccessControl: acMetadata,
|
||||
ParentUID: f.ParentUID,
|
||||
Repository: f.Repository,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -637,334 +649,3 @@ type GetFolderDescendantCountsResponse struct {
|
||||
// in: body
|
||||
Body folder.DescendantCounts `json:"body"`
|
||||
}
|
||||
|
||||
type folderK8sHandler struct {
|
||||
namespacer request.NamespaceMapper
|
||||
gvr schema.GroupVersionResource
|
||||
clientConfigProvider grafanaapiserver.DirectRestConfigProvider
|
||||
// #TODO check if it makes more sense to move this to FolderAPIBuilder
|
||||
accesscontrolService accesscontrol.Service
|
||||
userService user.Service
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------------------
|
||||
// Folder k8s wrapper functions
|
||||
//-----------------------------------------------------------------------------------------
|
||||
|
||||
func newFolderK8sHandler(hs *HTTPServer) *folderK8sHandler {
|
||||
return &folderK8sHandler{
|
||||
gvr: folderalpha1.FolderResourceInfo.GroupVersionResource(),
|
||||
namespacer: request.GetNamespaceMapper(hs.Cfg),
|
||||
clientConfigProvider: hs.clientConfigProvider,
|
||||
accesscontrolService: hs.accesscontrolService,
|
||||
userService: hs.userService,
|
||||
}
|
||||
}
|
||||
|
||||
func (fk8s *folderK8sHandler) searchFolders(c *contextmodel.ReqContext) {
|
||||
client, ok := fk8s.getClient(c)
|
||||
if !ok {
|
||||
return // error is already sent
|
||||
}
|
||||
out, err := client.List(c.Req.Context(), v1.ListOptions{})
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
query := strings.ToUpper(c.Query("query"))
|
||||
folders := []folder.Folder{}
|
||||
for _, item := range out.Items {
|
||||
p := internalfolders.UnstructuredToLegacyFolder(item, c.SignedInUser.GetOrgID())
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
if query != "" && !strings.Contains(strings.ToUpper(p.Title), query) {
|
||||
continue // query filter
|
||||
}
|
||||
folders = append(folders, *p)
|
||||
}
|
||||
c.JSON(http.StatusOK, folders)
|
||||
}
|
||||
|
||||
func (fk8s *folderK8sHandler) createFolder(c *contextmodel.ReqContext) {
|
||||
client, ok := fk8s.getClient(c)
|
||||
if !ok {
|
||||
return // error is already sent
|
||||
}
|
||||
cmd := folder.CreateFolderCommand{}
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
c.JsonApiErr(http.StatusBadRequest, "bad request data", err)
|
||||
return
|
||||
}
|
||||
obj, err := internalfolders.LegacyCreateCommandToUnstructured(cmd)
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
out, err := client.Create(c.Req.Context(), &obj, v1.CreateOptions{})
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
fk8s.accesscontrolService.ClearUserPermissionCache(c.SignedInUser)
|
||||
folderDTO, err := fk8s.newToFolderDto(c, *out, c.SignedInUser.GetOrgID())
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, folderDTO)
|
||||
}
|
||||
|
||||
func (fk8s *folderK8sHandler) getFolder(c *contextmodel.ReqContext) {
|
||||
client, ok := fk8s.getClient(c)
|
||||
if !ok {
|
||||
return // error is already sent
|
||||
}
|
||||
uid := web.Params(c.Req)[":uid"]
|
||||
out, err := client.Get(c.Req.Context(), uid, v1.GetOptions{})
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
folderDTO, err := fk8s.newToFolderDto(c, *out, c.SignedInUser.GetOrgID())
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, folderDTO)
|
||||
}
|
||||
|
||||
func (fk8s *folderK8sHandler) deleteFolder(c *contextmodel.ReqContext) {
|
||||
client, ok := fk8s.getClient(c)
|
||||
if !ok {
|
||||
return // error is already sent
|
||||
}
|
||||
uid := web.Params(c.Req)[":uid"]
|
||||
err := client.Delete(c.Req.Context(), uid, v1.DeleteOptions{})
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, "")
|
||||
}
|
||||
|
||||
func (fk8s *folderK8sHandler) updateFolder(c *contextmodel.ReqContext) {
|
||||
client, ok := fk8s.getClient(c)
|
||||
if !ok {
|
||||
return // error is already sent
|
||||
}
|
||||
|
||||
cmd := folder.UpdateFolderCommand{}
|
||||
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||
c.JsonApiErr(http.StatusBadRequest, "bad request data", err)
|
||||
return
|
||||
}
|
||||
cmd.OrgID = c.SignedInUser.GetOrgID()
|
||||
cmd.UID = web.Params(c.Req)[":uid"]
|
||||
cmd.SignedInUser = c.SignedInUser
|
||||
// #TODO add version?
|
||||
|
||||
obj, err := internalfolders.LegacyUpdateCommandToUnstructured(cmd)
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := client.Update(c.Req.Context(), &obj, v1.UpdateOptions{})
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
folderDTO, err := fk8s.newToFolderDto(c, *out, c.SignedInUser.GetOrgID())
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, folderDTO)
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------------------
|
||||
// Utility functions
|
||||
//-----------------------------------------------------------------------------------------
|
||||
|
||||
func (fk8s *folderK8sHandler) getClient(c *contextmodel.ReqContext) (dynamic.ResourceInterface, bool) {
|
||||
dyn, err := dynamic.NewForConfig(fk8s.clientConfigProvider.GetDirectRestConfig(c))
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "client", err)
|
||||
return nil, false
|
||||
}
|
||||
return dyn.Resource(fk8s.gvr).Namespace(fk8s.namespacer(c.OrgID)), true
|
||||
}
|
||||
|
||||
func (fk8s *folderK8sHandler) writeError(c *contextmodel.ReqContext, err error) {
|
||||
//nolint:errorlint
|
||||
statusError, ok := err.(*k8sErrors.StatusError)
|
||||
if ok {
|
||||
c.JsonApiErr(int(statusError.Status().Code), statusError.Status().Message, err)
|
||||
return
|
||||
}
|
||||
errhttp.Write(c.Req.Context(), err, c.Resp)
|
||||
}
|
||||
|
||||
func (fk8s *folderK8sHandler) newToFolderDto(c *contextmodel.ReqContext, item unstructured.Unstructured, orgID int64) (dtos.Folder, error) {
|
||||
// #TODO revisit how/where we get orgID
|
||||
ctx := c.Req.Context()
|
||||
|
||||
f := internalfolders.UnstructuredToLegacyFolder(item, orgID)
|
||||
|
||||
fDTO, err := internalfolders.UnstructuredToLegacyFolderDTO(item)
|
||||
if err != nil {
|
||||
return dtos.Folder{}, err
|
||||
}
|
||||
|
||||
// #TODO Is there a preexisting function we can use instead, something along the lines of UserIdentifier?
|
||||
toUID := func(rawIdentifier string) string {
|
||||
parts := strings.Split(rawIdentifier, ":")
|
||||
if len(parts) < 2 {
|
||||
return ""
|
||||
}
|
||||
return parts[1]
|
||||
}
|
||||
|
||||
toDTO := func(fold *folder.Folder, checkCanView bool) (dtos.Folder, error) {
|
||||
g, err := guardian.NewByFolder(c.Req.Context(), fold, c.SignedInUser.GetOrgID(), c.SignedInUser)
|
||||
if err != nil {
|
||||
return dtos.Folder{}, err
|
||||
}
|
||||
|
||||
canEdit, _ := g.CanEdit()
|
||||
canSave, _ := g.CanSave()
|
||||
canAdmin, _ := g.CanAdmin()
|
||||
canDelete, _ := g.CanDelete()
|
||||
|
||||
// Finding creator and last updater of the folder
|
||||
updater, creator := anonString, anonString
|
||||
// #TODO refactor the various conversions of the folder so that we either set created by in folder.Folder or
|
||||
// we convert from unstructured to folder DTO without an intermediate conversion to folder.Folder
|
||||
if len(fDTO.CreatedBy) > 0 {
|
||||
creator = fk8s.getUserLogin(ctx, toUID(fDTO.CreatedBy), orgID)
|
||||
}
|
||||
if len(fDTO.UpdatedBy) > 0 {
|
||||
updater = fk8s.getUserLogin(ctx, toUID(fDTO.UpdatedBy), orgID)
|
||||
}
|
||||
|
||||
acMetadata, _ := fk8s.getFolderACMetadata(c, fold)
|
||||
|
||||
if checkCanView {
|
||||
canView, _ := g.CanView()
|
||||
if !canView {
|
||||
return dtos.Folder{
|
||||
UID: REDACTED,
|
||||
Title: REDACTED,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
metrics.MFolderIDsAPICount.WithLabelValues(metrics.NewToFolderDTO).Inc()
|
||||
|
||||
fDTO.CanSave = canSave
|
||||
fDTO.CanEdit = canEdit
|
||||
fDTO.CanAdmin = canAdmin
|
||||
fDTO.CanDelete = canDelete
|
||||
fDTO.CreatedBy = creator
|
||||
fDTO.UpdatedBy = updater
|
||||
fDTO.AccessControl = acMetadata
|
||||
fDTO.OrgID = f.OrgID
|
||||
// #TODO version doesn't seem to be used--confirm or set it properly
|
||||
fDTO.Version = 1
|
||||
|
||||
return *fDTO, nil
|
||||
}
|
||||
|
||||
// no need to check view permission for the starting folder since it's already checked by the callers
|
||||
folderDTO, err := toDTO(f, false)
|
||||
if err != nil {
|
||||
return dtos.Folder{}, err
|
||||
}
|
||||
|
||||
if len(f.Fullpath) == 0 || len(f.FullpathUIDs) == 0 {
|
||||
return folderDTO, nil
|
||||
}
|
||||
|
||||
parentsFullPath, err := internalfolders.GetParentTitles(f.Fullpath)
|
||||
if err != nil {
|
||||
return dtos.Folder{}, err
|
||||
}
|
||||
parentsFullPathUIDs := strings.Split(f.FullpathUIDs, "/")
|
||||
|
||||
// The first part of the path is the newly created folder which we don't need to include
|
||||
// in the parents field
|
||||
if len(parentsFullPath) < 2 || len(parentsFullPathUIDs) < 2 {
|
||||
return folderDTO, nil
|
||||
}
|
||||
|
||||
parents := []dtos.Folder{}
|
||||
for i, v := range parentsFullPath[1:] {
|
||||
slug := slugify.Slugify(v)
|
||||
uid := parentsFullPathUIDs[1:][i]
|
||||
url := dashboards.GetFolderURL(uid, slug)
|
||||
|
||||
parents = append(parents, dtos.Folder{
|
||||
UID: uid,
|
||||
OrgID: c.SignedInUser.GetOrgID(),
|
||||
Title: v,
|
||||
URL: url,
|
||||
})
|
||||
}
|
||||
|
||||
folderDTO.Parents = parents
|
||||
|
||||
return folderDTO, nil
|
||||
}
|
||||
|
||||
func (fk8s *folderK8sHandler) getUserLogin(ctx context.Context, userUID string, orgID int64) string {
|
||||
ctx, span := tracer.Start(ctx, "api.getUserLogin")
|
||||
defer span.End()
|
||||
|
||||
query := user.GetUserByUIDQuery{
|
||||
UID: userUID,
|
||||
OrgID: orgID,
|
||||
}
|
||||
user, err := fk8s.userService.GetByUID(ctx, &query)
|
||||
if err != nil {
|
||||
return anonString
|
||||
}
|
||||
return user.Login
|
||||
}
|
||||
|
||||
func (fk8s *folderK8sHandler) getFolderACMetadata(c *contextmodel.ReqContext, f *folder.Folder) (accesscontrol.Metadata, error) {
|
||||
if !c.QueryBool("accesscontrol") {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if len(f.FullpathUIDs) == 0 {
|
||||
return map[string]bool{}, nil
|
||||
}
|
||||
|
||||
parentsFullPathUIDs := strings.Split(f.FullpathUIDs, "/")
|
||||
// The first part of the path is the newly created folder which we don't need to check here
|
||||
if len(parentsFullPathUIDs) < 2 {
|
||||
return map[string]bool{}, nil
|
||||
}
|
||||
|
||||
folderIDs := map[string]bool{f.UID: true}
|
||||
for _, uid := range parentsFullPathUIDs[1:] {
|
||||
folderIDs[uid] = true
|
||||
}
|
||||
|
||||
allMetadata := getMultiAccessControlMetadata(c, dashboards.ScopeFoldersPrefix, folderIDs)
|
||||
metadata := map[string]bool{}
|
||||
// Flatten metadata - if any parent has a permission, the child folder inherits it
|
||||
for _, md := range allMetadata {
|
||||
for action := range md {
|
||||
metadata[action] = true
|
||||
}
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/client"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
@@ -40,6 +40,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/org/orgimpl"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
"github.com/grafana/grafana/pkg/services/search/sort"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/star"
|
||||
"github.com/grafana/grafana/pkg/services/star/startest"
|
||||
@@ -51,6 +52,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"github.com/grafana/grafana/pkg/web/webtest"
|
||||
)
|
||||
@@ -451,39 +453,38 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
|
||||
|
||||
quotaSrv := quotatest.New(false, nil)
|
||||
|
||||
dashStore, err := database.ProvideDashboardStore(sc.db, sc.cfg, features, tagimpl.ProvideService(sc.db), quotaSrv)
|
||||
dashStore, err := database.ProvideDashboardStore(sc.db, sc.cfg, features, tagimpl.ProvideService(sc.db))
|
||||
require.NoError(b, err)
|
||||
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(sc.db)
|
||||
|
||||
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
|
||||
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
|
||||
cfg := setting.NewCfg()
|
||||
actionSets := resourcepermissions.NewActionSetService(features)
|
||||
fStore := folderimpl.ProvideStore(sc.db)
|
||||
folderServiceWithFlagOn := folderimpl.ProvideService(
|
||||
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
|
||||
nil, sc.db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService())
|
||||
acSvc := acimpl.ProvideOSSService(
|
||||
sc.cfg, acdb.ProvideService(sc.db), actionSets, localcache.ProvideService(),
|
||||
features, tracing.InitializeTracerForTest(), zanzana.NewNoopClient(), sc.db, permreg.ProvidePermissionRegistry(), nil,
|
||||
features, tracing.InitializeTracerForTest(), sc.db, permreg.ProvidePermissionRegistry(), nil,
|
||||
)
|
||||
fStore := folderimpl.ProvideStore(sc.db)
|
||||
folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions(
|
||||
cfg, features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, fStore, acSvc, sc.teamSvc, sc.userSvc, actionSets)
|
||||
cfg, features, routing.NewRouteRegister(), sc.db, ac, license, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc, actionSets)
|
||||
require.NoError(b, err)
|
||||
|
||||
folderServiceWithFlagOn := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore,
|
||||
folderStore, sc.db, features, cfg, folderPermissions, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
|
||||
dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions(
|
||||
cfg, features, routing.NewRouteRegister(), sc.db, ac, license, &dashboards.FakeDashboardStore{}, fStore, acSvc, sc.teamSvc, sc.userSvc, actionSets)
|
||||
require.NoError(b, err)
|
||||
|
||||
dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl(
|
||||
sc.cfg, dashStore, folderStore,
|
||||
features, folderPermissions, dashboardPermissions, ac,
|
||||
folderServiceWithFlagOn, fStore, nil,
|
||||
features, folderPermissions, ac,
|
||||
folderServiceWithFlagOn, fStore, nil, client.MockTestRestConfig{}, nil, quotaSrv, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
|
||||
)
|
||||
require.NoError(b, err)
|
||||
|
||||
_, err = ossaccesscontrol.ProvideDashboardPermissions(
|
||||
cfg, features, routing.NewRouteRegister(), sc.db, ac, license, dashboardSvc, folderServiceWithFlagOn, acSvc, sc.teamSvc, sc.userSvc, actionSets, dashboardSvc)
|
||||
require.NoError(b, err)
|
||||
|
||||
starSvc := startest.NewStarServiceFake()
|
||||
starSvc.ExpectedUserStars = &star.GetUserStarsResult{UserStars: make(map[int64]bool)}
|
||||
starSvc.ExpectedUserStars = &star.GetUserStarsResult{UserStars: make(map[string]bool)}
|
||||
|
||||
hs := &HTTPServer{
|
||||
CacheService: localcache.New(5*time.Minute, 10*time.Minute),
|
||||
@@ -491,13 +492,13 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
|
||||
SQLStore: sc.db,
|
||||
Features: features,
|
||||
QuotaService: quotaSrv,
|
||||
SearchService: search.ProvideService(sc.cfg, sc.db, starSvc, dashboardSvc),
|
||||
SearchService: search.ProvideService(sc.cfg, sc.db, starSvc, dashboardSvc, folderServiceWithFlagOn, features, sort.ProvideService()),
|
||||
folderService: folderServiceWithFlagOn,
|
||||
DashboardService: dashboardSvc,
|
||||
}
|
||||
|
||||
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
|
||||
guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService)
|
||||
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
|
||||
guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger())
|
||||
|
||||
m.Get("/api/folders", hs.GetFolders)
|
||||
m.Get("/api/search", hs.Search)
|
||||
|
||||
@@ -153,10 +153,12 @@ func (hs *HTTPServer) getFolderACL(ctx context.Context, user identity.Requester,
|
||||
FolderUID: folder.ParentUID,
|
||||
Created: p.Created,
|
||||
Updated: p.Updated,
|
||||
UserID: p.UserId,
|
||||
UserID: p.UserID,
|
||||
UserUID: p.UserUID,
|
||||
UserLogin: p.UserLogin,
|
||||
UserEmail: p.UserEmail,
|
||||
TeamID: p.TeamId,
|
||||
TeamID: p.TeamID,
|
||||
TeamUID: p.TeamUID,
|
||||
TeamEmail: p.TeamEmail,
|
||||
Team: p.Team,
|
||||
Role: role,
|
||||
|
||||
@@ -54,8 +54,8 @@ func TestHTTPServer_GetFolderPermissionList(t *testing.T) {
|
||||
|
||||
hs.folderPermissionsService = &actest.FakePermissionsService{
|
||||
ExpectedPermissions: []accesscontrol.ResourcePermission{
|
||||
{UserId: 1, UserLogin: "regular", IsManaged: true},
|
||||
{UserId: 2, UserLogin: "hidden", IsManaged: true},
|
||||
{UserID: 1, UserLogin: "regular", IsManaged: true},
|
||||
{UserID: 2, UserLogin: "hidden", IsManaged: true},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
+135
-86
@@ -15,6 +15,8 @@ import (
|
||||
clientrest "k8s.io/client-go/rest"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
@@ -27,6 +29,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/services/search/model"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web/webtest"
|
||||
)
|
||||
@@ -88,13 +91,6 @@ func TestFoldersCreateAPIEndpoint(t *testing.T) {
|
||||
expectedFolderSvcError: dashboards.ErrDashboardUidTooLong,
|
||||
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersCreate}},
|
||||
},
|
||||
{
|
||||
description: "folder creation fails given folder service error %s",
|
||||
input: folderWithoutParentInput,
|
||||
expectedCode: http.StatusConflict,
|
||||
expectedFolderSvcError: dashboards.ErrFolderSameNameExists,
|
||||
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersCreate}},
|
||||
},
|
||||
{
|
||||
description: "folder creation fails given folder service error %s",
|
||||
input: folderWithoutParentInput,
|
||||
@@ -203,12 +199,6 @@ func TestFoldersUpdateAPIEndpoint(t *testing.T) {
|
||||
expectedFolderSvcError: dashboards.ErrDashboardUidTooLong,
|
||||
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll}},
|
||||
},
|
||||
{
|
||||
description: "folder updating fails given folder service error %s",
|
||||
expectedCode: http.StatusConflict,
|
||||
expectedFolderSvcError: dashboards.ErrFolderSameNameExists,
|
||||
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll}},
|
||||
},
|
||||
{
|
||||
description: "folder updating fails given folder service error %s",
|
||||
expectedCode: http.StatusForbidden,
|
||||
@@ -539,92 +529,151 @@ func (m mockClientConfigProvider) GetDirectRestConfig(c *contextmodel.ReqContext
|
||||
|
||||
func (m mockClientConfigProvider) DirectlyServeHTTP(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
func TestHTTPServer_FolderMetadataK8s(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
//nolint:errcheck
|
||||
fmt.Fprintln(w,
|
||||
`{
|
||||
"kind": "Folder",
|
||||
"apiVersion": "folder.grafana.app/v0alpha1",
|
||||
"metadata": {
|
||||
"name": "ady4yobv315a8e",
|
||||
"namespace": "default",
|
||||
"uid": "28f306ee-ada1-40f4-8011-b2d1df462aad",
|
||||
"creationTimestamp": "2024-09-17T04:16:35Z",
|
||||
"annotations": {
|
||||
"grafana.app/createdBy": "user:fdxsqt7t5ryf4a",
|
||||
"grafana.app/originName": "SQL",
|
||||
"grafana.app/originPath": "3"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"title": "Example folder 226"
|
||||
}
|
||||
}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
// for now, test only the general folder
|
||||
func TestGetFolderLegacyAndUnifiedStorage(t *testing.T) {
|
||||
testuser := &user.User{ID: 99, UID: "fdxsqt7t5ryf4a", Login: "testuser"}
|
||||
|
||||
mockClientConfigProvider := mockClientConfigProvider{
|
||||
host: ts.URL,
|
||||
legacyFolder := *folder.RootFolder
|
||||
|
||||
expectedFolder := dtos.Folder{
|
||||
UID: legacyFolder.UID,
|
||||
OrgID: 0,
|
||||
Title: legacyFolder.Title,
|
||||
URL: legacyFolder.URL,
|
||||
HasACL: false,
|
||||
CanSave: false,
|
||||
CanEdit: true,
|
||||
CanAdmin: false,
|
||||
CanDelete: false,
|
||||
CreatedBy: "Anonymous",
|
||||
UpdatedBy: "Anonymous",
|
||||
}
|
||||
|
||||
setUpRBACGuardian(t)
|
||||
folderService := &foldertest.FakeService{}
|
||||
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders, featuremgmt.FlagKubernetesFolders)
|
||||
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
hs.Cfg = setting.NewCfg()
|
||||
hs.folderService = folderService
|
||||
hs.QuotaService = quotatest.New(false, nil)
|
||||
hs.SearchService = &mockSearchService{
|
||||
ExpectedResult: model.HitList{},
|
||||
mux := http.NewServeMux()
|
||||
folderApiServerMock := httptest.NewServer(mux)
|
||||
defer folderApiServerMock.Close()
|
||||
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
type testCase struct {
|
||||
description string
|
||||
folderUID string
|
||||
legacyFolder folder.Folder
|
||||
expectedFolder dtos.Folder
|
||||
expectedFolderServiceError error
|
||||
unifiedStorageEnabled bool
|
||||
unifiedStorageMode grafanarest.DualWriterMode
|
||||
expectedCode int
|
||||
}
|
||||
hs.Features = features
|
||||
hs.clientConfigProvider = mockClientConfigProvider
|
||||
})
|
||||
|
||||
t.Run("Should attach access control metadata to folder response", func(t *testing.T) {
|
||||
folderService.ExpectedFolder = &folder.Folder{UID: "ady4yobv315a8e"}
|
||||
tcs := []testCase{
|
||||
{
|
||||
description: "General folder - Legacy",
|
||||
expectedCode: http.StatusOK,
|
||||
legacyFolder: legacyFolder,
|
||||
folderUID: legacyFolder.UID,
|
||||
expectedFolder: expectedFolder,
|
||||
unifiedStorageEnabled: false,
|
||||
},
|
||||
{
|
||||
description: "General folder - Unified storage, mode 1",
|
||||
expectedCode: http.StatusOK,
|
||||
legacyFolder: legacyFolder,
|
||||
folderUID: legacyFolder.UID,
|
||||
expectedFolder: expectedFolder,
|
||||
unifiedStorageEnabled: true,
|
||||
unifiedStorageMode: grafanarest.Mode1,
|
||||
},
|
||||
{
|
||||
description: "General folder - Unified storage, mode 2",
|
||||
expectedCode: http.StatusOK,
|
||||
legacyFolder: legacyFolder,
|
||||
folderUID: legacyFolder.UID,
|
||||
expectedFolder: expectedFolder,
|
||||
unifiedStorageEnabled: true,
|
||||
unifiedStorageMode: grafanarest.Mode2,
|
||||
},
|
||||
{
|
||||
description: "General folder - Unified storage, mode 3",
|
||||
expectedCode: http.StatusOK,
|
||||
legacyFolder: legacyFolder,
|
||||
folderUID: legacyFolder.UID,
|
||||
expectedFolder: expectedFolder,
|
||||
unifiedStorageEnabled: true,
|
||||
unifiedStorageMode: grafanarest.Mode3,
|
||||
},
|
||||
{
|
||||
description: "General folder - Unified storage, mode 4",
|
||||
expectedCode: http.StatusOK,
|
||||
legacyFolder: legacyFolder,
|
||||
folderUID: legacyFolder.UID,
|
||||
expectedFolder: expectedFolder,
|
||||
unifiedStorageEnabled: true,
|
||||
unifiedStorageMode: grafanarest.Mode4,
|
||||
},
|
||||
}
|
||||
|
||||
req := server.NewGetRequest("/api/folders/ady4yobv315a8e?accesscontrol=true")
|
||||
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{
|
||||
1: accesscontrol.GroupScopesByActionContext(context.Background(), []accesscontrol.Permission{
|
||||
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll},
|
||||
{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("ady4yobv315a8e")},
|
||||
}),
|
||||
}})
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
setUpRBACGuardian(t)
|
||||
|
||||
res, err := server.Send(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
defer func() { require.NoError(t, res.Body.Close()) }()
|
||||
cfg := setting.NewCfg()
|
||||
cfg.UnifiedStorage = map[string]setting.UnifiedStorageConfig{
|
||||
folderv0alpha1.RESOURCEGROUP: {
|
||||
DualWriterMode: tc.unifiedStorageMode,
|
||||
},
|
||||
}
|
||||
|
||||
body := dtos.Folder{}
|
||||
require.NoError(t, json.NewDecoder(res.Body).Decode(&body))
|
||||
featuresArr := []any{featuremgmt.FlagNestedFolders}
|
||||
if tc.unifiedStorageEnabled {
|
||||
featuresArr = append(featuresArr, featuremgmt.FlagKubernetesClientDashboardsFolders)
|
||||
}
|
||||
|
||||
assert.True(t, body.AccessControl[dashboards.ActionFoldersRead])
|
||||
assert.True(t, body.AccessControl[dashboards.ActionFoldersWrite])
|
||||
})
|
||||
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
hs.Cfg = cfg
|
||||
hs.folderService = &foldertest.FakeService{
|
||||
ExpectedFolder: &tc.legacyFolder,
|
||||
ExpectedError: tc.expectedFolderServiceError,
|
||||
}
|
||||
hs.QuotaService = quotatest.New(false, nil)
|
||||
hs.SearchService = &mockSearchService{
|
||||
ExpectedResult: model.HitList{},
|
||||
}
|
||||
hs.userService = &usertest.FakeUserService{
|
||||
ExpectedUser: testuser,
|
||||
}
|
||||
hs.Features = featuremgmt.WithFeatures(
|
||||
featuresArr...,
|
||||
)
|
||||
hs.clientConfigProvider = mockClientConfigProvider{
|
||||
host: folderApiServerMock.URL,
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Should not attach access control metadata to folder response", func(t *testing.T) {
|
||||
folderService.ExpectedFolder = &folder.Folder{UID: "ady4yobv315a8e"}
|
||||
req := server.NewRequest(http.MethodGet, fmt.Sprintf("/api/folders/%s", tc.folderUID), nil)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{
|
||||
1: accesscontrol.GroupScopesByActionContext(context.Background(), []accesscontrol.Permission{
|
||||
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll},
|
||||
}),
|
||||
}})
|
||||
|
||||
req := server.NewGetRequest("/api/folders/ady4yobv315a8e")
|
||||
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{
|
||||
1: accesscontrol.GroupScopesByActionContext(context.Background(), []accesscontrol.Permission{
|
||||
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll},
|
||||
{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID("ady4yobv315a8e")},
|
||||
}),
|
||||
}})
|
||||
res, err := server.Send(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := server.Send(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
defer func() { require.NoError(t, res.Body.Close()) }()
|
||||
require.Equal(t, tc.expectedCode, res.StatusCode)
|
||||
defer func() { require.NoError(t, res.Body.Close()) }()
|
||||
|
||||
body := dtos.Folder{}
|
||||
require.NoError(t, json.NewDecoder(res.Body).Decode(&body))
|
||||
if tc.expectedCode == http.StatusOK {
|
||||
body := dtos.Folder{}
|
||||
require.NoError(t, json.NewDecoder(res.Body).Decode(&body))
|
||||
|
||||
assert.False(t, body.AccessControl[dashboards.ActionFoldersRead])
|
||||
assert.False(t, body.AccessControl[dashboards.ActionFoldersWrite])
|
||||
//nolint:staticcheck
|
||||
body.ID = 0
|
||||
body.Version = 0
|
||||
tc.expectedFolder.Version = 0
|
||||
require.Equal(t, tc.expectedFolder, body)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -157,7 +158,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
||||
}
|
||||
}
|
||||
|
||||
hideVersion := hs.Cfg.AnonymousHideVersion && !c.IsSignedIn
|
||||
hideVersion := hs.Cfg.Anonymous.HideVersion && !c.IsSignedIn
|
||||
version := setting.BuildVersion
|
||||
commit := setting.BuildCommit
|
||||
commitShort := getShortCommitHash(setting.BuildCommit, 10)
|
||||
@@ -213,6 +214,8 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
||||
RudderstackSdkUrl: hs.Cfg.RudderstackSDKURL,
|
||||
RudderstackConfigUrl: hs.Cfg.RudderstackConfigURL,
|
||||
RudderstackIntegrationsUrl: hs.Cfg.RudderstackIntegrationsURL,
|
||||
AnalyticsConsoleReporting: hs.Cfg.FrontendAnalyticsConsoleReporting,
|
||||
DashboardPerformanceMetrics: hs.Cfg.DashboardPerformanceMetrics,
|
||||
FeedbackLinksEnabled: hs.Cfg.FeedbackLinksEnabled,
|
||||
ApplicationInsightsConnectionString: hs.Cfg.ApplicationInsightsConnectionString,
|
||||
ApplicationInsightsEndpointUrl: hs.Cfg.ApplicationInsightsEndpointUrl,
|
||||
@@ -223,6 +226,8 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
||||
ExternalUserMngInfo: hs.Cfg.ExternalUserMngInfo,
|
||||
ExternalUserMngLinkUrl: hs.Cfg.ExternalUserMngLinkUrl,
|
||||
ExternalUserMngLinkName: hs.Cfg.ExternalUserMngLinkName,
|
||||
ExternalUserMngAnalytics: hs.Cfg.ExternalUserMngAnalytics,
|
||||
ExternalUserMngAnalyticsParams: hs.Cfg.ExternalUserMngAnalyticsParams,
|
||||
ViewersCanEdit: hs.Cfg.ViewersCanEdit,
|
||||
AngularSupportEnabled: hs.Cfg.AngularSupportEnabled,
|
||||
EditorsCanAdmin: hs.Cfg.EditorsCanAdmin,
|
||||
@@ -242,6 +247,9 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
||||
LocalFileSystemAvailable: hs.Cfg.LocalFileSystemAvailable,
|
||||
ReportingStaticContext: hs.Cfg.ReportingStaticContext,
|
||||
ExploreDefaultTimeOffset: hs.Cfg.ExploreDefaultTimeOffset,
|
||||
ExploreHideLogsDownload: hs.Cfg.ExploreHideLogsDownload,
|
||||
|
||||
DefaultDatasourceManageAlertsUIToggle: hs.Cfg.DefaultDatasourceManageAlertsUIToggle,
|
||||
PluginDependencies: pluginDependencyMap(c.Req.Context(), hs.pluginStore),
|
||||
|
||||
BuildInfo: dtos.FrontendSettingsBuildInfoDTO{
|
||||
@@ -266,8 +274,8 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
||||
},
|
||||
|
||||
FeatureToggles: featureToggles,
|
||||
AnonymousEnabled: hs.Cfg.AnonymousEnabled,
|
||||
AnonymousDeviceLimit: hs.Cfg.AnonymousDeviceLimit,
|
||||
AnonymousEnabled: hs.Cfg.Anonymous.Enabled,
|
||||
AnonymousDeviceLimit: hs.Cfg.Anonymous.DeviceLimit,
|
||||
RendererAvailable: hs.RenderService.IsAvailable(c.Req.Context()),
|
||||
RendererVersion: hs.RenderService.Version(),
|
||||
RendererDefaultImageWidth: hs.Cfg.RendererDefaultImageWidth,
|
||||
@@ -362,6 +370,24 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
||||
BasicAuthStrongPasswordPolicy: hs.Cfg.BasicAuthStrongPasswordPolicy,
|
||||
}
|
||||
|
||||
if hs.Cfg.PasswordlessMagicLinkAuth.Enabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagPasswordlessMagicLinkAuthentication) {
|
||||
hasEnabledProviders := hs.samlEnabled() || hs.authnService.IsClientEnabled(authn.ClientLDAP)
|
||||
|
||||
if !hasEnabledProviders {
|
||||
oauthInfos := hs.SocialService.GetOAuthInfoProviders()
|
||||
for _, provider := range oauthInfos {
|
||||
if provider.Enabled {
|
||||
hasEnabledProviders = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hasEnabledProviders {
|
||||
frontendSettings.Auth.PasswordlessEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
if hs.pluginsCDNService != nil && hs.pluginsCDNService.IsEnabled() {
|
||||
cdnBaseURL, err := hs.pluginsCDNService.BaseURL()
|
||||
if err != nil {
|
||||
@@ -720,6 +746,7 @@ func (hs *HTTPServer) pluginSettings(ctx context.Context, orgID int64) (map[stri
|
||||
OrgID: orgID,
|
||||
Enabled: plugin.AutoEnabled,
|
||||
Pinned: plugin.AutoEnabled,
|
||||
AutoEnabled: plugin.AutoEnabled,
|
||||
PluginVersion: plugin.Info.Version,
|
||||
}
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ func TestHTTPServer_GetFrontendSettings_hideVersionAnonymous(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
hs.Cfg.AnonymousHideVersion = test.hideVersion
|
||||
hs.Cfg.Anonymous.HideVersion = test.hideVersion
|
||||
expected := test.expected
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
@@ -23,7 +23,7 @@ var grafanaComProxyTransport = &http.Transport{
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
func ReverseProxyGnetReq(logger log.Logger, proxyPath string, version string, grafanaComAPIUrl string) *httputil.ReverseProxy {
|
||||
func ReverseProxyGnetReq(logger log.Logger, proxyPath, version, grafanaComAPIUrl, grafanaComAPIToken string) *httputil.ReverseProxy {
|
||||
url, _ := url.Parse(grafanaComAPIUrl)
|
||||
|
||||
director := func(req *http.Request) {
|
||||
@@ -40,6 +40,10 @@ func ReverseProxyGnetReq(logger log.Logger, proxyPath string, version string, gr
|
||||
|
||||
// send the current Grafana version for each request proxied to GCOM
|
||||
req.Header.Add("grafana-version", version)
|
||||
|
||||
if grafanaComAPIToken != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+grafanaComAPIToken)
|
||||
}
|
||||
}
|
||||
|
||||
return proxyutil.NewReverseProxy(logger, director)
|
||||
@@ -47,7 +51,7 @@ func ReverseProxyGnetReq(logger log.Logger, proxyPath string, version string, gr
|
||||
|
||||
func (hs *HTTPServer) ProxyGnetRequest(c *contextmodel.ReqContext) {
|
||||
proxyPath := web.Params(c.Req)["*"]
|
||||
proxy := ReverseProxyGnetReq(c.Logger, proxyPath, hs.Cfg.BuildVersion, hs.Cfg.GrafanaComAPIURL)
|
||||
proxy := ReverseProxyGnetReq(c.Logger, proxyPath, hs.Cfg.BuildVersion, hs.Cfg.GrafanaComAPIURL, hs.Cfg.GrafanaComSSOAPIToken)
|
||||
proxy.Transport = grafanaComProxyTransport
|
||||
proxy.ServeHTTP(c.Resp, c.Req)
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestHealthAPI_VersionEnterprise(t *testing.T) {
|
||||
|
||||
func TestHealthAPI_AnonymousHideVersion(t *testing.T) {
|
||||
m, hs := setupHealthAPITestEnvironment(t)
|
||||
hs.Cfg.AnonymousHideVersion = true
|
||||
hs.Cfg.Anonymous.HideVersion = true
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/health", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -80,7 +80,7 @@ func TestHealthAPI_DatabaseHealthy(t *testing.T) {
|
||||
const cacheKey = "db-healthy"
|
||||
|
||||
m, hs := setupHealthAPITestEnvironment(t)
|
||||
hs.Cfg.AnonymousHideVersion = true
|
||||
hs.Cfg.Anonymous.HideVersion = true
|
||||
|
||||
healthy, found := hs.CacheService.Get(cacheKey)
|
||||
require.False(t, found)
|
||||
@@ -107,7 +107,7 @@ func TestHealthAPI_DatabaseUnhealthy(t *testing.T) {
|
||||
const cacheKey = "db-healthy"
|
||||
|
||||
m, hs := setupHealthAPITestEnvironment(t)
|
||||
hs.Cfg.AnonymousHideVersion = true
|
||||
hs.Cfg.Anonymous.HideVersion = true
|
||||
hs.SQLStore.(*dbtest.FakeDB).ExpectedError = errors.New("bad")
|
||||
|
||||
healthy, found := hs.CacheService.Get(cacheKey)
|
||||
@@ -135,7 +135,7 @@ func TestHealthAPI_DatabaseHealthCached(t *testing.T) {
|
||||
const cacheKey = "db-healthy"
|
||||
|
||||
m, hs := setupHealthAPITestEnvironment(t)
|
||||
hs.Cfg.AnonymousHideVersion = true
|
||||
hs.Cfg.Anonymous.HideVersion = true
|
||||
|
||||
// Mock unhealthy database in cache.
|
||||
hs.CacheService.Set(cacheKey, false, 5*time.Minute)
|
||||
|
||||
+15
-17
@@ -25,6 +25,8 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
||||
"github.com/youmark/pkcs8"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/avatar"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
httpstatic "github.com/grafana/grafana/pkg/api/static"
|
||||
@@ -80,6 +82,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugininstaller"
|
||||
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
pref "github.com/grafana/grafana/pkg/services/preference"
|
||||
@@ -104,14 +107,12 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/tag"
|
||||
"github.com/grafana/grafana/pkg/services/team"
|
||||
tempUser "github.com/grafana/grafana/pkg/services/temp_user"
|
||||
"github.com/grafana/grafana/pkg/services/unifiedSearch"
|
||||
"github.com/grafana/grafana/pkg/services/updatechecker"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/services/validations"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"github.com/youmark/pkcs8"
|
||||
)
|
||||
|
||||
type HTTPServer struct {
|
||||
@@ -140,7 +141,7 @@ type HTTPServer struct {
|
||||
License licensing.Licensing
|
||||
AccessControl accesscontrol.AccessControl
|
||||
DataProxy *datasourceproxy.DataSourceProxyService
|
||||
PluginRequestValidator validations.PluginRequestValidator
|
||||
DataSourceRequestValidator validations.DataSourceRequestValidator
|
||||
pluginClient plugins.Client
|
||||
pluginStore pluginstore.Store
|
||||
pluginInstaller plugins.Installer
|
||||
@@ -149,6 +150,7 @@ type HTTPServer struct {
|
||||
pluginStaticRouteResolver plugins.StaticRouteResolver
|
||||
pluginErrorResolver plugins.ErrorResolver
|
||||
pluginAssets *pluginassets.Service
|
||||
pluginPreinstall plugininstaller.Preinstall
|
||||
SearchService search.Service
|
||||
ShortURLService shorturls.Service
|
||||
QueryHistoryService queryhistory.Service
|
||||
@@ -157,7 +159,6 @@ type HTTPServer struct {
|
||||
LivePushGateway *pushhttp.Gateway
|
||||
StorageService store.StorageService
|
||||
SearchV2HTTPService searchV2.SearchHTTPService
|
||||
UnifiedSearchHTTPService unifiedSearch.SearchHTTPService
|
||||
ContextHandler *contexthandler.ContextHandler
|
||||
LoggerMiddleware loggermw.Logger
|
||||
SQLStore db.DB
|
||||
@@ -206,6 +207,7 @@ type HTTPServer struct {
|
||||
tempUserService tempUser.Service
|
||||
loginAttemptService loginAttempt.Service
|
||||
orgService org.Service
|
||||
orgDeletionService org.DeletionService
|
||||
TeamService team.Service
|
||||
accesscontrolService accesscontrol.Service
|
||||
annotationsRepo annotations.Repository
|
||||
@@ -237,7 +239,7 @@ type ServerOptions struct {
|
||||
func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routing.RouteRegister, bus bus.Bus,
|
||||
renderService rendering.Service, licensing licensing.Licensing, hooksService *hooks.HooksService,
|
||||
cacheService *localcache.CacheService, sqlStore db.DB,
|
||||
pluginRequestValidator validations.PluginRequestValidator, pluginStaticRouteResolver plugins.StaticRouteResolver,
|
||||
dataSourceRequestValidator validations.DataSourceRequestValidator, pluginStaticRouteResolver plugins.StaticRouteResolver,
|
||||
pluginDashboardService plugindashboards.Service, pluginStore pluginstore.Store, pluginClient plugins.Client,
|
||||
pluginErrorResolver plugins.ErrorResolver, pluginInstaller plugins.Installer, settingsProvider setting.Provider,
|
||||
dataSourceCache datasources.CacheService, userTokenService auth.UserTokenService,
|
||||
@@ -255,7 +257,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
authInfoService login.AuthInfoService, storageService store.StorageService,
|
||||
notificationService notifications.Service, dashboardService dashboards.DashboardService,
|
||||
dashboardProvisioningService dashboards.DashboardProvisioningService, folderService folder.Service,
|
||||
folderStore folder.Store,
|
||||
dsGuardian guardian.DatasourceGuardianProvider,
|
||||
dashboardsnapshotsService dashboardsnapshots.Service, pluginSettings pluginSettings.Service,
|
||||
avatarCacheServer *avatar.AvatarCacheServer, preferenceService pref.Service,
|
||||
@@ -266,12 +267,12 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
secretsMigrator secrets.Migrator, secretsPluginManager plugins.SecretsPluginManager, secretsService secrets.Service,
|
||||
secretsPluginMigrator spm.SecretMigrationProvider, secretsStore secretsKV.SecretsKVStore,
|
||||
publicDashboardsApi *publicdashboardsApi.Api, userService user.Service, tempUserService tempUser.Service,
|
||||
loginAttemptService loginAttempt.Service, orgService org.Service, teamService team.Service,
|
||||
loginAttemptService loginAttempt.Service, orgService org.Service, orgDeletionService org.DeletionService, teamService team.Service,
|
||||
accesscontrolService accesscontrol.Service, navTreeService navtree.Service,
|
||||
annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, unifiedSearchHTTPService unifiedSearch.SearchHTTPService, oauthTokenService oauthtoken.OAuthTokenService,
|
||||
annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, oauthTokenService oauthtoken.OAuthTokenService,
|
||||
statsService stats.Service, authnService authn.Service, pluginsCDNService *pluginscdn.Service, promGatherer prometheus.Gatherer,
|
||||
starApi *starApi.API, promRegister prometheus.Registerer, clientConfigProvider grafanaapiserver.DirectRestConfigProvider, anonService anonymous.Service,
|
||||
userVerifier user.Verifier,
|
||||
userVerifier user.Verifier, pluginPreinstall plugininstaller.Preinstall,
|
||||
) (*HTTPServer, error) {
|
||||
web.Env = cfg.Env
|
||||
m := web.New()
|
||||
@@ -285,7 +286,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
HooksService: hooksService,
|
||||
CacheService: cacheService,
|
||||
SQLStore: sqlStore,
|
||||
PluginRequestValidator: pluginRequestValidator,
|
||||
DataSourceRequestValidator: dataSourceRequestValidator,
|
||||
pluginInstaller: pluginInstaller,
|
||||
pluginClient: pluginClient,
|
||||
pluginStore: pluginStore,
|
||||
@@ -296,6 +297,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
pluginFileStore: pluginFileStore,
|
||||
grafanaUpdateChecker: grafanaUpdateChecker,
|
||||
pluginsUpdateChecker: pluginsUpdateChecker,
|
||||
pluginPreinstall: pluginPreinstall,
|
||||
SettingsProvider: settingsProvider,
|
||||
DataSourceCache: dataSourceCache,
|
||||
AuthTokenService: userTokenService,
|
||||
@@ -310,7 +312,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
AccessControl: accessControl,
|
||||
DataProxy: dataSourceProxy,
|
||||
SearchV2HTTPService: searchv2HTTPService,
|
||||
UnifiedSearchHTTPService: unifiedSearchHTTPService,
|
||||
SearchService: searchService,
|
||||
Live: live,
|
||||
LivePushGateway: livePushGateway,
|
||||
@@ -359,6 +360,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
tempUserService: tempUserService,
|
||||
loginAttemptService: loginAttemptService,
|
||||
orgService: orgService,
|
||||
orgDeletionService: orgDeletionService,
|
||||
TeamService: teamService,
|
||||
navTreeService: navTreeService,
|
||||
accesscontrolService: accesscontrolService,
|
||||
@@ -383,7 +385,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
hs.registerRoutes()
|
||||
|
||||
// Register access control scope resolver for annotations
|
||||
hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo, features, dashboardService, folderStore))
|
||||
hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo, features, dashboardService, folderService))
|
||||
|
||||
if err := hs.declareFixedRoles(); err != nil {
|
||||
return nil, err
|
||||
@@ -718,7 +720,7 @@ func (hs *HTTPServer) apiHealthHandler(ctx *web.Context) {
|
||||
data := healthResponse{
|
||||
Database: "ok",
|
||||
}
|
||||
if !hs.Cfg.AnonymousHideVersion {
|
||||
if !hs.Cfg.Anonymous.HideVersion {
|
||||
data.Version = hs.Cfg.BuildVersion
|
||||
data.Commit = hs.Cfg.BuildCommit
|
||||
if hs.Cfg.EnterpriseBuildCommit != "NA" && hs.Cfg.EnterpriseBuildCommit != "" {
|
||||
@@ -794,13 +796,9 @@ func (hs *HTTPServer) getDefaultCiphers(tlsVersion uint16, protocol string) []ui
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
}
|
||||
}
|
||||
if protocol == "h2" {
|
||||
|
||||
+3
-2
@@ -8,7 +8,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
claims "github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/webassets"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
@@ -250,7 +250,8 @@ func (hs *HTTPServer) getThemeForIndexData(themePrefId string, themeURLParam str
|
||||
|
||||
if pref.IsValidThemeID(themePrefId) {
|
||||
theme := pref.GetThemeByID(themePrefId)
|
||||
if !theme.IsExtra || hs.Features.IsEnabledGlobally(featuremgmt.FlagExtraThemes) {
|
||||
// TODO refactor
|
||||
if !theme.IsExtra || hs.Features.IsEnabledGlobally(featuremgmt.FlagExtraThemes) || hs.Features.IsEnabledGlobally(featuremgmt.FlagGrafanaconThemes) {
|
||||
return theme
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
pref "github.com/grafana/grafana/pkg/services/preference"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
@@ -241,6 +242,26 @@ func (hs *HTTPServer) LoginPost(c *contextmodel.ReqContext) response.Response {
|
||||
return authn.HandleLoginResponse(c.Req, c.Resp, hs.Cfg, identity, hs.ValidateRedirectTo, hs.Features)
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) LoginPasswordless(c *contextmodel.ReqContext) response.Response {
|
||||
identity, err := hs.authnService.Login(c.Req.Context(), authn.ClientPasswordless, &authn.Request{HTTPRequest: c.Req})
|
||||
if err != nil {
|
||||
tokenErr := &auth.CreateTokenErr{}
|
||||
if errors.As(err, &tokenErr) {
|
||||
return response.Error(tokenErr.StatusCode, tokenErr.ExternalErr, tokenErr.InternalErr)
|
||||
}
|
||||
return response.Err(err)
|
||||
}
|
||||
return authn.HandleLoginResponse(c.Req, c.Resp, hs.Cfg, identity, hs.ValidateRedirectTo, hs.Features)
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) StartPasswordless(c *contextmodel.ReqContext) {
|
||||
redirect, err := hs.authnService.RedirectURL(c.Req.Context(), authn.ClientPasswordless, &authn.Request{HTTPRequest: c.Req})
|
||||
if err != nil {
|
||||
c.Redirect(hs.redirectURLWithErrorCookie(c, err))
|
||||
}
|
||||
c.JSON(http.StatusOK, redirect)
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) loginUserWithUser(user *user.User, c *contextmodel.ReqContext) error {
|
||||
if user == nil {
|
||||
return errors.New("could not login user")
|
||||
@@ -382,6 +403,22 @@ func (hs *HTTPServer) samlAutoLoginEnabled() bool {
|
||||
return hs.samlEnabled() && config.IsAutoLoginEnabled()
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) samlSkipOrgRoleSyncEnabled() bool {
|
||||
config, ok := hs.authnService.GetClientConfig(authn.ClientSAML)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return hs.samlEnabled() && config.IsSkipOrgRoleSyncEnabled()
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) samlAllowAssignGrafanaAdminEnabled() bool {
|
||||
config, ok := hs.authnService.GetClientConfig(authn.ClientSAML)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return hs.samlEnabled() && config.IsAllowAssignGrafanaAdminEnabled()
|
||||
}
|
||||
|
||||
func getLoginExternalError(err error) string {
|
||||
var createTokenErr *auth.CreateTokenErr
|
||||
if errors.As(err, &createTokenErr) {
|
||||
@@ -411,3 +448,76 @@ func getFirstPublicErrorMessage(err *errutil.Error) string {
|
||||
|
||||
return errPublic.Message
|
||||
}
|
||||
|
||||
// isExternalySynced is used to tell if the user roles are externally synced
|
||||
// true means that the org role sync is handled by Grafana
|
||||
// Note: currently the users authinfo is overridden each time the user logs in
|
||||
// https://github.com/grafana/grafana/blob/4181acec72f76df7ad02badce13769bae4a1f840/pkg/services/login/authinfoservice/database/database.go#L61
|
||||
// this means that if the user has multiple auth providers and one of them is set to sync org roles
|
||||
// then isExternallySynced will be true for this one provider and false for the others
|
||||
func (hs *HTTPServer) isExternallySynced(cfg *setting.Cfg, authModule string) bool {
|
||||
// provider enabled in config
|
||||
if !hs.isProviderEnabled(cfg, authModule) {
|
||||
return false
|
||||
}
|
||||
// first check SAML, LDAP and JWT
|
||||
switch authModule {
|
||||
case loginservice.SAMLAuthModule:
|
||||
return !hs.samlSkipOrgRoleSyncEnabled()
|
||||
case loginservice.LDAPAuthModule:
|
||||
return !cfg.LDAPSkipOrgRoleSync
|
||||
case loginservice.JWTModule:
|
||||
return !cfg.JWTAuth.SkipOrgRoleSync
|
||||
}
|
||||
switch authModule {
|
||||
case loginservice.GoogleAuthModule, loginservice.OktaAuthModule, loginservice.AzureADAuthModule, loginservice.GitLabAuthModule, loginservice.GithubAuthModule, loginservice.GrafanaComAuthModule, loginservice.GenericOAuthModule:
|
||||
config, ok := hs.authnService.GetClientConfig(oauthModuleToAuthnClient(authModule))
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return !config.IsSkipOrgRoleSyncEnabled()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isGrafanaAdminExternallySynced returns true if Grafana server admin role is being managed by an external auth provider, and false otherwise.
|
||||
// Grafana admin role sync is available for JWT, OAuth providers and LDAP.
|
||||
// For JWT and OAuth providers there is an additional config option `allow_assign_grafana_admin` that has to be enabled for Grafana Admin role to be synced.
|
||||
func (hs *HTTPServer) isGrafanaAdminExternallySynced(cfg *setting.Cfg, authModule string) bool {
|
||||
if !hs.isExternallySynced(cfg, authModule) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch authModule {
|
||||
case loginservice.JWTModule:
|
||||
return cfg.JWTAuth.AllowAssignGrafanaAdmin
|
||||
case loginservice.SAMLAuthModule:
|
||||
return hs.samlAllowAssignGrafanaAdminEnabled()
|
||||
case loginservice.LDAPAuthModule:
|
||||
return true
|
||||
default:
|
||||
config, ok := hs.authnService.GetClientConfig(oauthModuleToAuthnClient(authModule))
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return config.IsAllowAssignGrafanaAdminEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) isProviderEnabled(cfg *setting.Cfg, authModule string) bool {
|
||||
switch authModule {
|
||||
case loginservice.SAMLAuthModule:
|
||||
return hs.authnService.IsClientEnabled(authn.ClientSAML)
|
||||
case loginservice.LDAPAuthModule:
|
||||
return cfg.LDAPAuthEnabled
|
||||
case loginservice.JWTModule:
|
||||
return cfg.JWTAuth.Enabled
|
||||
case loginservice.GoogleAuthModule, loginservice.OktaAuthModule, loginservice.AzureADAuthModule, loginservice.GitLabAuthModule, loginservice.GithubAuthModule, loginservice.GrafanaComAuthModule, loginservice.GenericOAuthModule:
|
||||
return hs.authnService.IsClientEnabled(oauthModuleToAuthnClient(authModule))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func oauthModuleToAuthnClient(authModule string) string {
|
||||
return authn.ClientWithPrefix(strings.TrimPrefix(authModule, "oauth_"))
|
||||
}
|
||||
|
||||
+168
-1
@@ -12,10 +12,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
claims "github.com/grafana/authlib/types"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
@@ -688,6 +689,172 @@ func TestLogoutSaml(t *testing.T) {
|
||||
require.Equal(t, 302, sc.resp.Code)
|
||||
}
|
||||
|
||||
func TestIsExternallySynced(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
cfg *setting.Cfg
|
||||
provider string
|
||||
enabledAuthnClients []string
|
||||
authnClientConfig authn.SSOClientConfig
|
||||
expected bool
|
||||
}{
|
||||
// Same for all of the OAuth providers
|
||||
{
|
||||
name: "AzureAD external user should return that it is externally synced",
|
||||
cfg: &setting.Cfg{},
|
||||
provider: loginservice.AzureADAuthModule,
|
||||
enabledAuthnClients: []string{authn.ClientWithPrefix("azuread")},
|
||||
authnClientConfig: &authntest.FakeSSOClientConfig{
|
||||
ExpectedIsSkipOrgRoleSyncEnabled: false,
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "AzureAD external user should return that it is not externally synced when org role sync is set",
|
||||
cfg: &setting.Cfg{},
|
||||
provider: loginservice.AzureADAuthModule,
|
||||
enabledAuthnClients: []string{authn.ClientWithPrefix(strings.TrimPrefix(loginservice.AzureADAuthModule, "oauth_"))},
|
||||
authnClientConfig: &authntest.FakeSSOClientConfig{
|
||||
ExpectedIsSkipOrgRoleSyncEnabled: true,
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "AzureAD external user should return that it is not externally synced when the provider is not enabled",
|
||||
cfg: &setting.Cfg{},
|
||||
enabledAuthnClients: []string{},
|
||||
authnClientConfig: &authntest.FakeSSOClientConfig{
|
||||
ExpectedIsSkipOrgRoleSyncEnabled: false,
|
||||
},
|
||||
provider: loginservice.AzureADAuthModule,
|
||||
expected: false,
|
||||
},
|
||||
// saml
|
||||
{
|
||||
name: "SAML synced user should return that it is not externally synced when the provider is disabled",
|
||||
cfg: &setting.Cfg{},
|
||||
provider: loginservice.SAMLAuthModule,
|
||||
enabledAuthnClients: []string{},
|
||||
authnClientConfig: &authntest.FakeSSOClientConfig{
|
||||
ExpectedIsSkipOrgRoleSyncEnabled: false,
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "SAML synced user should return that it is externally synced",
|
||||
cfg: &setting.Cfg{},
|
||||
provider: loginservice.SAMLAuthModule,
|
||||
enabledAuthnClients: []string{authn.ClientSAML},
|
||||
authnClientConfig: &authntest.FakeSSOClientConfig{
|
||||
ExpectedIsSkipOrgRoleSyncEnabled: false,
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "SAML synced user should return that it is not externally synced when org role sync is set",
|
||||
cfg: &setting.Cfg{},
|
||||
provider: loginservice.SAMLAuthModule,
|
||||
enabledAuthnClients: []string{authn.ClientSAML},
|
||||
authnClientConfig: &authntest.FakeSSOClientConfig{
|
||||
ExpectedIsSkipOrgRoleSyncEnabled: true,
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
// ldap
|
||||
{
|
||||
name: "LDAP synced user should return that it is externally synced",
|
||||
cfg: &setting.Cfg{LDAPAuthEnabled: true, LDAPSkipOrgRoleSync: false},
|
||||
provider: loginservice.LDAPAuthModule,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "LDAP synced user should return that it is not externally synced when org role sync is set",
|
||||
cfg: &setting.Cfg{LDAPAuthEnabled: true, LDAPSkipOrgRoleSync: true},
|
||||
provider: loginservice.LDAPAuthModule,
|
||||
expected: false,
|
||||
},
|
||||
// jwt
|
||||
{
|
||||
name: "JWT synced user should return that it is externally synced",
|
||||
cfg: &setting.Cfg{JWTAuth: setting.AuthJWTSettings{Enabled: true, SkipOrgRoleSync: false}},
|
||||
provider: loginservice.JWTModule,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "JWT synced user should return that it is not externally synced when org role sync is set",
|
||||
cfg: &setting.Cfg{JWTAuth: setting.AuthJWTSettings{Enabled: true, SkipOrgRoleSync: true}},
|
||||
provider: loginservice.JWTModule,
|
||||
expected: false,
|
||||
},
|
||||
// IsProvider test
|
||||
{
|
||||
name: "If no provider enabled should return false",
|
||||
cfg: &setting.Cfg{JWTAuth: setting.AuthJWTSettings{Enabled: false, SkipOrgRoleSync: true}},
|
||||
provider: loginservice.JWTModule,
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
hs := &HTTPServer{
|
||||
authnService: &authntest.FakeService{
|
||||
ExpectedClientConfig: tc.authnClientConfig,
|
||||
EnabledClients: tc.enabledAuthnClients,
|
||||
},
|
||||
}
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.expected, hs.isExternallySynced(tc.cfg, tc.provider))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsProviderEnabled(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
provider string
|
||||
enabledAuthnClients []string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Github should return true if enabled",
|
||||
provider: loginservice.GithubAuthModule,
|
||||
enabledAuthnClients: []string{authn.ClientWithPrefix(strings.TrimPrefix(loginservice.GithubAuthModule, "oauth_"))},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Github should return false if not enabled",
|
||||
provider: loginservice.GithubAuthModule,
|
||||
enabledAuthnClients: []string{},
|
||||
expected: false,
|
||||
},
|
||||
// saml
|
||||
{
|
||||
name: "SAML should return true if enabled",
|
||||
provider: loginservice.SAMLAuthModule,
|
||||
enabledAuthnClients: []string{authn.ClientSAML},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "SAML should return false if not enabled",
|
||||
provider: loginservice.SAMLAuthModule,
|
||||
enabledAuthnClients: []string{},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
hs := &HTTPServer{
|
||||
authnService: &authntest.FakeService{
|
||||
EnabledClients: tc.enabledAuthnClients,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, tc.expected, hs.isProviderEnabled(setting.NewCfg(), tc.provider))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockSocialService struct {
|
||||
oAuthInfo *social.OAuthInfo
|
||||
oAuthInfos map[string]*social.OAuthInfo
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
claims "github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
@@ -297,7 +297,7 @@ func (hs *HTTPServer) DeleteOrgByID(c *contextmodel.ReqContext) response.Respons
|
||||
return response.Error(http.StatusBadRequest, "Can not delete org for current user", nil)
|
||||
}
|
||||
|
||||
if err := hs.orgService.Delete(c.Req.Context(), &org.DeleteOrgCommand{ID: orgID}); err != nil {
|
||||
if err := hs.orgDeletionService.Delete(c.Req.Context(), &org.DeleteOrgCommand{ID: orgID}); err != nil {
|
||||
if errors.Is(err, org.ErrOrgNotFound) {
|
||||
return response.Error(http.StatusNotFound, "Failed to delete organization. ID not found", nil)
|
||||
}
|
||||
|
||||
+16
-2
@@ -129,7 +129,7 @@ func (hs *HTTPServer) AddOrgInvite(c *contextmodel.ReqContext) response.Response
|
||||
"OrgName": c.SignedInUser.GetOrgName(),
|
||||
"Email": c.SignedInUser.GetEmail(),
|
||||
"LinkUrl": setting.ToAbsUrl("invite/" + cmd.Code),
|
||||
"InvitedBy": c.SignedInUser.GetDisplayName(),
|
||||
"InvitedBy": c.SignedInUser.GetName(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ func (hs *HTTPServer) inviteExistingUserToOrg(c *contextmodel.ReqContext, user *
|
||||
Data: map[string]any{
|
||||
"Name": user.NameOrFallback(),
|
||||
"OrgName": c.SignedInUser.GetOrgName(),
|
||||
"InvitedBy": c.SignedInUser.GetDisplayName(),
|
||||
"InvitedBy": c.SignedInUser.GetName(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -195,6 +195,20 @@ func (hs *HTTPServer) inviteExistingUserToOrg(c *contextmodel.ReqContext, user *
|
||||
// 404: notFoundError
|
||||
// 500: internalServerError
|
||||
func (hs *HTTPServer) RevokeInvite(c *contextmodel.ReqContext) response.Response {
|
||||
query := tempuser.GetTempUserByCodeQuery{Code: web.Params(c.Req)[":code"]}
|
||||
queryResult, err := hs.tempUserService.GetTempUserByCode(c.Req.Context(), &query)
|
||||
if err != nil {
|
||||
if errors.Is(err, tempuser.ErrTempUserNotFound) {
|
||||
return response.Error(http.StatusNotFound, "Invite not found", nil)
|
||||
}
|
||||
return response.Error(http.StatusInternalServerError, "Failed to get invite", err)
|
||||
}
|
||||
|
||||
canRevoke := c.SignedInUser.GetOrgID() == queryResult.OrgID || c.SignedInUser.GetIsGrafanaAdmin()
|
||||
if !canRevoke {
|
||||
return response.Error(http.StatusForbidden, "Permission denied: not permitted to revoke invite", nil)
|
||||
}
|
||||
|
||||
if ok, rsp := hs.updateTempUserStatus(c.Req.Context(), web.Params(c.Req)[":code"], tempuser.TmpUserRevoked); !ok {
|
||||
return rsp
|
||||
}
|
||||
|
||||
+3
-1
@@ -6,10 +6,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
claims "github.com/grafana/authlib/types"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
@@ -228,6 +229,7 @@ func TestAPIEndpoint_DeleteOrgs(t *testing.T) {
|
||||
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
hs.Cfg = setting.NewCfg()
|
||||
hs.orgService = &orgtest.FakeOrgService{ExpectedOrg: &org.Org{}}
|
||||
hs.orgDeletionService = &orgtest.FakeOrgDeletionService{}
|
||||
hs.userService = &usertest.FakeUserService{ExpectedSignedInUser: &user.SignedInUser{OrgID: 1}}
|
||||
hs.accesscontrolService = actest.FakeService{ExpectedPermissions: tt.permission}
|
||||
hs.authnService = &authntest.FakeService{}
|
||||
|
||||
@@ -161,6 +161,7 @@ func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *contextmodel.ReqContext)
|
||||
|
||||
for _, u := range orgUsersResult.OrgUsers {
|
||||
result = append(result, &dtos.UserLookupDTO{
|
||||
UID: u.UID,
|
||||
UserID: u.UserID,
|
||||
Login: u.Login,
|
||||
AvatarURL: u.AvatarURL,
|
||||
@@ -338,9 +339,8 @@ func (hs *HTTPServer) searchOrgUsersHelper(c *contextmodel.ReqContext, query *or
|
||||
for i := range filteredUsers {
|
||||
filteredUsers[i].AccessControl = accessControlMetadata[fmt.Sprint(filteredUsers[i].UserID)]
|
||||
if module, ok := modules[filteredUsers[i].UserID]; ok {
|
||||
oauthInfo := hs.SocialService.GetOAuthInfoProvider(module)
|
||||
filteredUsers[i].AuthLabels = []string{login.GetAuthProviderLabel(module)}
|
||||
filteredUsers[i].IsExternallySynced = login.IsExternallySynced(hs.Cfg, module, oauthInfo)
|
||||
filteredUsers[i].IsExternallySynced = hs.isExternallySynced(hs.Cfg, module)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,8 +427,7 @@ func (hs *HTTPServer) updateOrgUserHelper(c *contextmodel.ReqContext, cmd org.Up
|
||||
}
|
||||
}
|
||||
if authInfo != nil && authInfo.AuthModule != "" {
|
||||
oauthInfo := hs.SocialService.GetOAuthInfoProvider(authInfo.AuthModule)
|
||||
if login.IsExternallySynced(hs.Cfg, authInfo.AuthModule, oauthInfo) {
|
||||
if hs.isExternallySynced(hs.Cfg, authInfo.AuthModule) {
|
||||
return response.Err(org.ErrCannotChangeRoleForExternallySyncedUser.Errorf("Cannot change role for externally synced user"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/authn/authntest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -258,9 +260,17 @@ func TestOrgUsersAPIEndpoint_updateOrgRole(t *testing.T) {
|
||||
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
hs.Cfg = setting.NewCfg()
|
||||
hs.Cfg.LDAPAuthEnabled = tt.AuthEnabled
|
||||
if tt.AuthModule == login.LDAPAuthModule {
|
||||
switch tt.AuthModule {
|
||||
case login.LDAPAuthModule:
|
||||
hs.Cfg.LDAPAuthEnabled = tt.AuthEnabled
|
||||
hs.Cfg.LDAPSkipOrgRoleSync = tt.SkipOrgRoleSync
|
||||
case login.GrafanaComAuthModule:
|
||||
hs.authnService = &authntest.FakeService{
|
||||
EnabledClients: []string{authn.ClientWithPrefix("grafana_com")},
|
||||
ExpectedClientConfig: &authntest.FakeSSOClientConfig{
|
||||
ExpectedIsSkipOrgRoleSyncEnabled: tt.SkipOrgRoleSync,
|
||||
},
|
||||
}
|
||||
}
|
||||
// AuthModule empty means basic auth
|
||||
|
||||
|
||||
+1
-2
@@ -39,8 +39,7 @@ func (hs *HTTPServer) SendResetPasswordEmail(c *contextmodel.ReqContext) respons
|
||||
|
||||
getAuthQuery := login.GetAuthInfoQuery{UserId: usr.ID}
|
||||
if authInfo, err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery); err == nil {
|
||||
oauthInfo := hs.SocialService.GetOAuthInfoProvider(authInfo.AuthModule)
|
||||
if login.IsProviderEnabled(hs.Cfg, authInfo.AuthModule, oauthInfo) {
|
||||
if hs.isProviderEnabled(hs.Cfg, authInfo.AuthModule) {
|
||||
c.Logger.Info("Requested password reset for external user", nil)
|
||||
return response.Error(http.StatusOK, "Email sent", nil)
|
||||
}
|
||||
|
||||
+2
-2
@@ -9,12 +9,12 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
"github.com/grafana/grafana/apps/playlist/apis/playlist/v0alpha1"
|
||||
"github.com/grafana/grafana/apps/playlist/pkg/apis/playlist/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
internalplaylist "github.com/grafana/grafana/pkg/registry/apis/playlist"
|
||||
internalplaylist "github.com/grafana/grafana/pkg/registry/apps/playlist"
|
||||
grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
|
||||
@@ -62,12 +62,7 @@ func (hs *HTTPServer) callPluginResourceWithDataSource(c *contextmodel.ReqContex
|
||||
return
|
||||
}
|
||||
|
||||
var dsURL string
|
||||
if pCtx.DataSourceInstanceSettings != nil {
|
||||
dsURL = pCtx.DataSourceInstanceSettings.URL
|
||||
}
|
||||
|
||||
err = hs.PluginRequestValidator.Validate(dsURL, c.Req)
|
||||
err = hs.DataSourceRequestValidator.Validate(ds, c.Req)
|
||||
if err != nil {
|
||||
c.JsonApiErr(http.StatusForbidden, "Access denied", err)
|
||||
return
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestCallResource(t *testing.T) {
|
||||
cfg.Azure = &azsettings.AzureSettings{}
|
||||
|
||||
coreRegistry := coreplugin.ProvideCoreRegistry(tracing.InitializeTracerForTest(), nil, &cloudwatch.CloudWatchService{}, nil, nil, nil, nil,
|
||||
nil, nil, nil, nil, testdatasource.ProvideService(), nil, nil, nil, nil, nil, nil)
|
||||
nil, nil, nil, nil, testdatasource.ProvideService(), nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
|
||||
testCtx := pluginsintegration.CreateIntegrationTestCtx(t, cfg, coreRegistry)
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
|
||||
}
|
||||
|
||||
ctxLogger := logger.FromContext(ctx)
|
||||
|
||||
if len(route.URL) > 0 {
|
||||
interpolatedURL, err := interpolateString(route.URL, data)
|
||||
if err != nil {
|
||||
@@ -49,7 +48,14 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
|
||||
req.URL.Scheme = routeURL.Scheme
|
||||
req.URL.Host = routeURL.Host
|
||||
req.Host = routeURL.Host
|
||||
req.URL.Path = util.JoinURLFragments(routeURL.Path, proxyPath)
|
||||
req.URL.RawPath = util.JoinURLFragments(routeURL.Path, proxyPath)
|
||||
unescapedPath, err := url.PathUnescape(req.URL.RawPath)
|
||||
if err != nil {
|
||||
ctxLogger.Error("Failed to unescape raw path", "rawPath", req.URL.RawPath, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
req.URL.Path = unescapedPath
|
||||
}
|
||||
|
||||
if err := addQueryString(req, route, data); err != nil {
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
glog "github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -57,7 +58,8 @@ type httpClient interface {
|
||||
func NewDataSourceProxy(ds *datasources.DataSource, pluginRoutes []*plugins.Route, ctx *contextmodel.ReqContext,
|
||||
proxyPath string, cfg *setting.Cfg, clientProvider httpclient.Provider,
|
||||
oAuthTokenService oauthtoken.OAuthTokenService, dsService datasources.DataSourceService,
|
||||
tracer tracing.Tracer, features featuremgmt.FeatureToggles) (*DataSourceProxy, error) {
|
||||
tracer tracing.Tracer, features featuremgmt.FeatureToggles,
|
||||
) (*DataSourceProxy, error) {
|
||||
targetURL, err := datasource.ValidateURL(ds.Type, ds.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -261,7 +263,8 @@ func (proxy *DataSourceProxy) director(req *http.Request) {
|
||||
}
|
||||
|
||||
if proxy.oAuthTokenService.IsOAuthPassThruEnabled(proxy.ds) {
|
||||
if token := proxy.oAuthTokenService.GetCurrentOAuthToken(req.Context(), proxy.ctx.SignedInUser); token != nil {
|
||||
reqCtx := contexthandler.FromContext(req.Context())
|
||||
if token := proxy.oAuthTokenService.GetCurrentOAuthToken(req.Context(), proxy.ctx.SignedInUser, reqCtx.UserToken); token != nil {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("%s %s", token.Type(), token.AccessToken))
|
||||
|
||||
idToken, ok := token.Extra("id_token").(string)
|
||||
@@ -372,7 +375,7 @@ func (proxy *DataSourceProxy) logRequest() {
|
||||
panelPluginId := proxy.ctx.Req.Header.Get("X-Panel-Plugin-Id")
|
||||
|
||||
uri, err := util.SanitizeURI(proxy.ctx.Req.RequestURI)
|
||||
if err == nil {
|
||||
if err != nil {
|
||||
proxy.ctx.Logger.Error("Could not sanitize RequestURI", "error", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
claims "github.com/grafana/authlib/types"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/datasource"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@@ -32,7 +33,8 @@ import (
|
||||
pluginfakes "github.com/grafana/grafana/pkg/plugins/manager/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service"
|
||||
@@ -117,6 +119,10 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
Path: "api/rbac-restricted",
|
||||
ReqAction: "test-app.settings:read",
|
||||
},
|
||||
{
|
||||
Path: "encodedPath",
|
||||
URL: "http://encoded.com",
|
||||
},
|
||||
}
|
||||
|
||||
ds := &datasources.DataSource{
|
||||
@@ -233,6 +239,16 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
assert.Equal(t, "https://example.com/api/v1/some-route/", req.URL.String())
|
||||
})
|
||||
|
||||
t.Run("When matching proxy path is already encoded", func(t *testing.T) {
|
||||
ctx, req := setUp()
|
||||
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/our%20devices")
|
||||
require.NoError(t, err)
|
||||
proxy.matchedRoute = routes[9]
|
||||
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, proxy.matchedRoute, dsInfo, proxy.cfg)
|
||||
|
||||
assert.Equal(t, "http://encoded.com/our%20devices", req.URL.String())
|
||||
})
|
||||
|
||||
t.Run("Validating request", func(t *testing.T) {
|
||||
t.Run("plugin route with valid role", func(t *testing.T) {
|
||||
ctx, _ := setUp()
|
||||
@@ -557,7 +573,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
var routes []*plugins.Route
|
||||
proxy, err := setupDSProxyTest(t, ctx, ds, routes, "/path/to/folder/", func(proxy *DataSourceProxy) {
|
||||
proxy.oAuthTokenService = &oauthtokentest.MockOauthTokenService{
|
||||
GetCurrentOauthTokenFunc: func(_ context.Context, _ identity.Requester) *oauth2.Token {
|
||||
GetCurrentOauthTokenFunc: func(_ context.Context, _ identity.Requester, _ *auth.UserToken) *oauth2.Token {
|
||||
return (&oauth2.Token{
|
||||
AccessToken: "testtoken",
|
||||
RefreshToken: "testrefreshtoken",
|
||||
@@ -573,6 +589,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
|
||||
req = req.WithContext(context.WithValue(req.Context(), ctxkey.Key{}, &contextmodel.ReqContext{UserToken: nil}))
|
||||
require.NoError(t, err)
|
||||
|
||||
proxy.director(req)
|
||||
@@ -900,7 +917,7 @@ func getDatasourceProxiedRequest(t *testing.T, ctx *contextmodel.ReqContext, cfg
|
||||
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
|
||||
features := featuremgmt.WithFeatures()
|
||||
quotaService := quotatest.New(false, nil)
|
||||
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()),
|
||||
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features),
|
||||
&actest.FakePermissionsService{}, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{},
|
||||
plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()))
|
||||
require.NoError(t, err)
|
||||
@@ -1022,7 +1039,7 @@ func runDatasourceAuthTest(t *testing.T, secretsService secrets.Service, secrets
|
||||
var routes []*plugins.Route
|
||||
features := featuremgmt.WithFeatures()
|
||||
quotaService := quotatest.New(false, nil)
|
||||
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()),
|
||||
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features),
|
||||
&actest.FakePermissionsService{}, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{},
|
||||
plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()))
|
||||
require.NoError(t, err)
|
||||
@@ -1078,7 +1095,7 @@ func setupDSProxyTest(t *testing.T, ctx *contextmodel.ReqContext, ds *datasource
|
||||
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
secretsStore := secretskvs.NewSQLSecretsKVStore(dbtest.NewFakeDB(), secretsService, log.NewNopLogger())
|
||||
features := featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall)
|
||||
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()),
|
||||
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, features, acimpl.ProvideAccessControl(features),
|
||||
&actest.FakePermissionsService{}, quotatest.New(false, nil), &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{},
|
||||
plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider()))
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
claims "github.com/grafana/authlib/types"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
@@ -267,7 +267,7 @@ func TestPluginProxy(t *testing.T) {
|
||||
SecureJSONData: map[string][]byte{},
|
||||
}
|
||||
cfg := &setting.Cfg{}
|
||||
proxy, err := NewPluginProxy(ps, routes, ctx, "", cfg, secretsService, tracing.InitializeTracerForTest(), &http.Transport{}, acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), featuremgmt.WithFeatures())
|
||||
proxy, err := NewPluginProxy(ps, routes, ctx, "", cfg, secretsService, tracing.InitializeTracerForTest(), &http.Transport{}, acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
proxy.HandleRequest()
|
||||
|
||||
@@ -423,7 +423,7 @@ func TestPluginProxyRoutes(t *testing.T) {
|
||||
SecureJSONData: map[string][]byte{},
|
||||
}
|
||||
cfg := &setting.Cfg{}
|
||||
proxy, err := NewPluginProxy(ps, testRoutes, ctx, tc.proxyPath, cfg, secretsService, tracing.InitializeTracerForTest(), &http.Transport{}, acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), featuremgmt.WithFeatures(tc.withFeatures...))
|
||||
proxy, err := NewPluginProxy(ps, testRoutes, ctx, tc.proxyPath, cfg, secretsService, tracing.InitializeTracerForTest(), &http.Transport{}, acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), featuremgmt.WithFeatures(tc.withFeatures...))
|
||||
require.NoError(t, err)
|
||||
proxy.HandleRequest()
|
||||
|
||||
@@ -557,7 +557,7 @@ func TestPluginProxyRoutesAccessControl(t *testing.T) {
|
||||
SecureJSONData: map[string][]byte{},
|
||||
}
|
||||
cfg := &setting.Cfg{}
|
||||
proxy, err := NewPluginProxy(ps, testRoutes, ctx, tc.proxyPath, cfg, secretsService, tracing.InitializeTracerForTest(), &http.Transport{}, acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall))
|
||||
proxy, err := NewPluginProxy(ps, testRoutes, ctx, tc.proxyPath, cfg, secretsService, tracing.InitializeTracerForTest(), &http.Transport{}, acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), featuremgmt.WithFeatures(featuremgmt.FlagAccessControlOnCall))
|
||||
require.NoError(t, err)
|
||||
proxy.HandleRequest()
|
||||
|
||||
@@ -588,7 +588,7 @@ func getPluginProxiedRequest(t *testing.T, ps *pluginsettings.DTO, secretsServic
|
||||
ReqRole: org.RoleEditor,
|
||||
}
|
||||
}
|
||||
proxy, err := NewPluginProxy(ps, []*plugins.Route{}, ctx, "", cfg, secretsService, tracing.InitializeTracerForTest(), &http.Transport{}, acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), featuremgmt.WithFeatures())
|
||||
proxy, err := NewPluginProxy(ps, []*plugins.Route{}, ctx, "", cfg, secretsService, tracing.InitializeTracerForTest(), &http.Transport{}, acimpl.ProvideAccessControl(featuremgmt.WithFeatures()), featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "/api/plugin-proxy/grafana-simple-app/api/v4/alerts", nil)
|
||||
|
||||
+19
-18
@@ -13,6 +13,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins/auth"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
@@ -21,7 +22,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs"
|
||||
"github.com/grafana/grafana/pkg/plugins/repo"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
@@ -74,7 +74,7 @@ func (hs *HTTPServer) GetPluginList(c *contextmodel.ReqContext) response.Respons
|
||||
|
||||
// Filter plugins
|
||||
pluginDefinitions := hs.pluginStore.Plugins(c.Req.Context())
|
||||
filteredPluginDefinitions := []pluginstore.Plugin{}
|
||||
filteredPluginDefinitions := make([]pluginstore.Plugin, 0)
|
||||
filteredPluginIDs := map[string]bool{}
|
||||
for _, pluginDef := range pluginDefinitions {
|
||||
// filter out app sub plugins
|
||||
@@ -216,6 +216,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *contextmodel.ReqContext) response.
|
||||
if plugin.IsApp() {
|
||||
dto.Enabled = plugin.AutoEnabled
|
||||
dto.Pinned = plugin.AutoEnabled
|
||||
dto.AutoEnabled = plugin.AutoEnabled
|
||||
}
|
||||
|
||||
ps, err := hs.PluginSettings.GetPluginSettingByPluginID(c.Req.Context(), &pluginsettings.GetByPluginIDArgs{
|
||||
@@ -254,9 +255,13 @@ func (hs *HTTPServer) UpdatePluginSetting(c *contextmodel.ReqContext) response.R
|
||||
}
|
||||
pluginID := web.Params(c.Req)[":pluginId"]
|
||||
|
||||
if _, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID); !exists {
|
||||
p, exists := hs.pluginStore.Plugin(c.Req.Context(), pluginID)
|
||||
if !exists {
|
||||
return response.Error(http.StatusNotFound, "Plugin not installed", nil)
|
||||
}
|
||||
if p.AutoEnabled && !cmd.Enabled {
|
||||
return response.Error(http.StatusBadRequest, "Cannot disable auto-enabled plugin", nil)
|
||||
}
|
||||
|
||||
cmd.OrgId = c.SignedInUser.GetOrgID()
|
||||
cmd.PluginId = pluginID
|
||||
@@ -461,13 +466,11 @@ func (hs *HTTPServer) InstallPlugin(c *contextmodel.ReqContext) response.Respons
|
||||
|
||||
hs.log.Info("Plugin install/update requested", "pluginId", pluginID, "user", c.Login)
|
||||
|
||||
for _, preinstalled := range hs.Cfg.PreinstallPlugins {
|
||||
if preinstalled.ID == pluginID && preinstalled.Version != "" {
|
||||
return response.Error(http.StatusConflict, "Cannot update a pinned pre-installed plugin", nil)
|
||||
}
|
||||
for hs.pluginPreinstall.IsPinned(pluginID) {
|
||||
return response.Error(http.StatusConflict, "Cannot update a pinned pre-installed plugin", nil)
|
||||
}
|
||||
|
||||
compatOpts := plugins.NewCompatOpts(hs.Cfg.BuildVersion, runtime.GOOS, runtime.GOARCH)
|
||||
compatOpts := plugins.NewAddOpts(hs.Cfg.BuildVersion, runtime.GOOS, runtime.GOARCH, "")
|
||||
ctx := repo.WithRequestOrigin(c.Req.Context(), "api")
|
||||
err := hs.pluginInstaller.Add(ctx, pluginID, dto.Version, compatOpts)
|
||||
if err != nil {
|
||||
@@ -506,10 +509,8 @@ func (hs *HTTPServer) UninstallPlugin(c *contextmodel.ReqContext) response.Respo
|
||||
return response.Error(http.StatusNotFound, "Plugin not installed", nil)
|
||||
}
|
||||
|
||||
for _, preinstalled := range hs.Cfg.PreinstallPlugins {
|
||||
if preinstalled.ID == pluginID {
|
||||
return response.Error(http.StatusConflict, "Cannot uninstall a pre-installed plugin", nil)
|
||||
}
|
||||
for hs.pluginPreinstall.IsPreinstalled(pluginID) {
|
||||
return response.Error(http.StatusConflict, "Cannot uninstall a pre-installed plugin", nil)
|
||||
}
|
||||
|
||||
err := hs.pluginInstaller.Remove(c.Req.Context(), pluginID, plugin.Info.Version)
|
||||
@@ -578,14 +579,14 @@ func (hs *HTTPServer) hasPluginRequestedPermissions(c *contextmodel.ReqContext,
|
||||
}
|
||||
|
||||
// evalAllPermissions generates an evaluator with all permissions from the input slice
|
||||
func evalAllPermissions(ps []pfs.Permission) ac.Evaluator {
|
||||
res := []ac.Evaluator{}
|
||||
for _, p := range ps {
|
||||
if p.Scope != nil {
|
||||
res = append(res, ac.EvalPermission(p.Action, *p.Scope))
|
||||
func evalAllPermissions(ps []auth.Permission) ac.Evaluator {
|
||||
res := make([]ac.Evaluator, len(ps))
|
||||
for i, p := range ps {
|
||||
if p.Scope != "" {
|
||||
res[i] = ac.EvalPermission(p.Action, p.Scope)
|
||||
continue
|
||||
}
|
||||
res = append(res, ac.EvalPermission(p.Action))
|
||||
res[i] = ac.EvalPermission(p.Action)
|
||||
}
|
||||
return ac.EvalAll(res...)
|
||||
}
|
||||
|
||||
+49
-10
@@ -12,12 +12,14 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana/pkg/plugins/auth"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/log/logtest"
|
||||
@@ -29,14 +31,12 @@ import (
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature"
|
||||
"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs"
|
||||
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/authn/authntest"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
@@ -45,6 +45,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugininstaller"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
"github.com/grafana/grafana/pkg/services/updatechecker"
|
||||
@@ -72,7 +73,7 @@ func Test_PluginsInstallAndUninstall(t *testing.T) {
|
||||
preInstalledPlugin bool
|
||||
}
|
||||
tcs := []testCase{
|
||||
{expectedCode: http.StatusNotFound, permissionOrg: globalOrg, permissions: canInstall, pluginAdminEnabled: true, pluginAdminExternalManageEnabled: true},
|
||||
{expectedCode: http.StatusOK, permissionOrg: globalOrg, permissions: canInstall, pluginAdminEnabled: true, pluginAdminExternalManageEnabled: true},
|
||||
{expectedCode: http.StatusNotFound, permissionOrg: globalOrg, permissions: canInstall, pluginAdminEnabled: false, pluginAdminExternalManageEnabled: true},
|
||||
{expectedCode: http.StatusNotFound, permissionOrg: globalOrg, permissions: canInstall, pluginAdminEnabled: false, pluginAdminExternalManageEnabled: false},
|
||||
{expectedCode: http.StatusForbidden, permissionOrg: globalOrg, permissions: cannotInstall, pluginAdminEnabled: true, pluginAdminExternalManageEnabled: false},
|
||||
@@ -111,6 +112,7 @@ func Test_PluginsInstallAndUninstall(t *testing.T) {
|
||||
},
|
||||
})
|
||||
hs.managedPluginsService = managedplugins.NewNoop()
|
||||
hs.pluginPreinstall = plugininstaller.ProvidePreinstall(hs.Cfg)
|
||||
|
||||
expectedIdentity := &authn.Identity{
|
||||
OrgID: tc.permissionOrg,
|
||||
@@ -682,14 +684,11 @@ func createPlugin(jd plugins.JSONData, class plugins.Class, files plugins.FS) *p
|
||||
}
|
||||
|
||||
func TestHTTPServer_hasPluginRequestedPermissions(t *testing.T) {
|
||||
newStr := func(s string) *string {
|
||||
return &s
|
||||
}
|
||||
pluginReg := pluginstore.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: "grafana-test-app",
|
||||
IAM: &pfs.IAM{
|
||||
Permissions: []pfs.Permission{{Action: ac.ActionUsersRead, Scope: newStr(ac.ScopeUsersAll)}, {Action: ac.ActionUsersCreate}},
|
||||
IAM: &auth.IAM{
|
||||
Permissions: []auth.Permission{{Action: ac.ActionUsersRead, Scope: ac.ScopeUsersAll}, {Action: ac.ActionUsersCreate}},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -762,7 +761,7 @@ func TestHTTPServer_hasPluginRequestedPermissions(t *testing.T) {
|
||||
}
|
||||
hs.log = logger
|
||||
hs.accesscontrolService = actest.FakeService{}
|
||||
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
|
||||
hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
|
||||
|
||||
expectedIdentity := &authn.Identity{
|
||||
OrgID: tt.orgID,
|
||||
@@ -871,3 +870,43 @@ func Test_PluginsSettings(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_UpdatePluginSetting(t *testing.T) {
|
||||
pID := "test-app"
|
||||
p1 := createPlugin(plugins.JSONData{
|
||||
ID: pID, Type: "app", Name: pID,
|
||||
Info: plugins.Info{
|
||||
Version: "1.0.0",
|
||||
},
|
||||
AutoEnabled: true,
|
||||
}, plugins.ClassExternal, plugins.NewFakeFS(),
|
||||
)
|
||||
pluginRegistry := &fakes.FakePluginRegistry{
|
||||
Store: map[string]*plugins.Plugin{
|
||||
p1.ID: p1,
|
||||
},
|
||||
}
|
||||
|
||||
pluginSettings := pluginsettings.FakePluginSettings{Plugins: map[string]*pluginsettings.DTO{
|
||||
pID: {ID: 0, OrgID: 1, PluginID: pID, PluginVersion: "1.0.0", Enabled: true},
|
||||
}}
|
||||
|
||||
t.Run("should return an error when trying to disable an auto-enabled plugin", func(t *testing.T) {
|
||||
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
hs.Cfg = setting.NewCfg()
|
||||
hs.PluginSettings = &pluginSettings
|
||||
hs.pluginStore = pluginstore.New(pluginRegistry, &fakes.FakeLoader{})
|
||||
hs.pluginFileStore = filestore.ProvideService(pluginRegistry)
|
||||
hs.managedPluginsService = managedplugins.NewNoop()
|
||||
hs.log = log.NewNopLogger()
|
||||
})
|
||||
|
||||
input := strings.NewReader(`{"enabled": false}`)
|
||||
endpoint := fmt.Sprintf("/api/plugins/%s/settings", pID)
|
||||
req := webtest.RequestWithSignedInUser(server.NewPostRequest(endpoint, input), userWithPermissions(1, []ac.Permission{{Action: pluginaccesscontrol.ActionWrite, Scope: "plugins:id:test-app"}}))
|
||||
res, err := server.SendJSON(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusBadRequest, res.StatusCode)
|
||||
require.NoError(t, res.Body.Close())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,12 +46,16 @@ func (hs *HTTPServer) redirectFromShortURL(c *contextmodel.ReqContext) {
|
||||
|
||||
shortURL, err := hs.ShortURLService.GetShortURLByUID(c.Req.Context(), c.SignedInUser, shortURLUID)
|
||||
if err != nil {
|
||||
// If we didn't get the URL for whatever reason, we redirect to the
|
||||
// main page, otherwise we get into an endless loops of redirects, as
|
||||
// we would try to redirect again.
|
||||
if shorturls.ErrShortURLNotFound.Is(err) {
|
||||
hs.log.Debug("Not redirecting short URL since not found")
|
||||
hs.log.Debug("Not redirecting short URL since not found", "uid", shortURLUID)
|
||||
c.Redirect(hs.Cfg.AppURL, 308)
|
||||
return
|
||||
}
|
||||
|
||||
hs.log.Error("Short URL redirection error", "err", err)
|
||||
c.Redirect(hs.Cfg.AppURL, 307)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+5
-6
@@ -8,7 +8,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
claims "github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
@@ -36,7 +36,7 @@ func (hs *HTTPServer) GetSignedInUser(c *contextmodel.ReqContext) response.Respo
|
||||
IsGrafanaAdmin: c.SignedInUser.GetIsGrafanaAdmin(),
|
||||
OrgID: c.SignedInUser.GetOrgID(),
|
||||
UID: c.SignedInUser.GetID(),
|
||||
Name: c.SignedInUser.NameOrFallback(),
|
||||
Name: c.SignedInUser.GetName(),
|
||||
Email: c.SignedInUser.GetEmail(),
|
||||
Login: c.SignedInUser.GetLogin(),
|
||||
})
|
||||
@@ -86,9 +86,8 @@ func (hs *HTTPServer) getUserUserProfile(c *contextmodel.ReqContext, userID int6
|
||||
userProfile.AuthLabels = append(userProfile.AuthLabels, authLabel)
|
||||
userProfile.IsExternal = true
|
||||
|
||||
oauthInfo := hs.SocialService.GetOAuthInfoProvider(authInfo.AuthModule)
|
||||
userProfile.IsExternallySynced = login.IsExternallySynced(hs.Cfg, authInfo.AuthModule, oauthInfo)
|
||||
userProfile.IsGrafanaAdminExternallySynced = login.IsGrafanaAdminExternallySynced(hs.Cfg, oauthInfo, authInfo.AuthModule)
|
||||
userProfile.IsExternallySynced = hs.isExternallySynced(hs.Cfg, authInfo.AuthModule)
|
||||
userProfile.IsGrafanaAdminExternallySynced = hs.isGrafanaAdminExternallySynced(hs.Cfg, authInfo.AuthModule)
|
||||
}
|
||||
|
||||
userProfile.AccessControl = getAccessControlMetadata(c, "global.users:id:", strconv.FormatInt(userID, 10))
|
||||
@@ -281,7 +280,7 @@ func (hs *HTTPServer) StartEmailVerificaton(c *contextmodel.ReqContext) response
|
||||
return response.Error(http.StatusBadRequest, "Only users can verify their email", nil)
|
||||
}
|
||||
|
||||
if c.SignedInUser.IsEmailVerified() {
|
||||
if c.SignedInUser.GetEmailVerified() {
|
||||
// email is already verified so we don't need to trigger the flow.
|
||||
return response.Respond(http.StatusNotModified, nil)
|
||||
}
|
||||
|
||||
+61
-7
@@ -14,6 +14,9 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/authn/authntest"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
@@ -28,7 +31,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/auth/idtest"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/login"
|
||||
@@ -61,7 +63,7 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
|
||||
hs := &HTTPServer{
|
||||
Cfg: settings,
|
||||
SQLStore: sqlStore,
|
||||
AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()),
|
||||
AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures()),
|
||||
}
|
||||
|
||||
mockResult := user.SearchUserQueryResult{
|
||||
@@ -243,6 +245,7 @@ func Test_GetUserByID(t *testing.T) {
|
||||
authEnabled bool
|
||||
skipOrgRoleSync bool
|
||||
expectedIsGrafanaAdminSynced bool
|
||||
expectedIsExternallySynced bool
|
||||
}{
|
||||
{
|
||||
name: "Should return IsGrafanaAdminExternallySynced = false for an externally synced OAuth user if Grafana Admin role is not synced",
|
||||
@@ -251,6 +254,7 @@ func Test_GetUserByID(t *testing.T) {
|
||||
allowAssignGrafanaAdmin: false,
|
||||
skipOrgRoleSync: false,
|
||||
expectedIsGrafanaAdminSynced: false,
|
||||
expectedIsExternallySynced: true,
|
||||
},
|
||||
{
|
||||
name: "Should return IsGrafanaAdminExternallySynced = false for an externally synced OAuth user if OAuth provider is not enabled",
|
||||
@@ -259,6 +263,7 @@ func Test_GetUserByID(t *testing.T) {
|
||||
allowAssignGrafanaAdmin: true,
|
||||
skipOrgRoleSync: false,
|
||||
expectedIsGrafanaAdminSynced: false,
|
||||
expectedIsExternallySynced: false,
|
||||
},
|
||||
{
|
||||
name: "Should return IsGrafanaAdminExternallySynced = false for an externally synced OAuth user if org roles are not being synced",
|
||||
@@ -267,6 +272,7 @@ func Test_GetUserByID(t *testing.T) {
|
||||
allowAssignGrafanaAdmin: true,
|
||||
skipOrgRoleSync: true,
|
||||
expectedIsGrafanaAdminSynced: false,
|
||||
expectedIsExternallySynced: false,
|
||||
},
|
||||
{
|
||||
name: "Should return IsGrafanaAdminExternallySynced = true for an externally synced OAuth user",
|
||||
@@ -275,6 +281,7 @@ func Test_GetUserByID(t *testing.T) {
|
||||
allowAssignGrafanaAdmin: true,
|
||||
skipOrgRoleSync: false,
|
||||
expectedIsGrafanaAdminSynced: true,
|
||||
expectedIsExternallySynced: true,
|
||||
},
|
||||
{
|
||||
name: "Should return IsGrafanaAdminExternallySynced = false for an externally synced JWT user if Grafana Admin role is not synced",
|
||||
@@ -283,6 +290,7 @@ func Test_GetUserByID(t *testing.T) {
|
||||
allowAssignGrafanaAdmin: false,
|
||||
skipOrgRoleSync: false,
|
||||
expectedIsGrafanaAdminSynced: false,
|
||||
expectedIsExternallySynced: true,
|
||||
},
|
||||
{
|
||||
name: "Should return IsGrafanaAdminExternallySynced = false for an externally synced JWT user if JWT provider is not enabled",
|
||||
@@ -291,6 +299,7 @@ func Test_GetUserByID(t *testing.T) {
|
||||
allowAssignGrafanaAdmin: true,
|
||||
skipOrgRoleSync: false,
|
||||
expectedIsGrafanaAdminSynced: false,
|
||||
expectedIsExternallySynced: false,
|
||||
},
|
||||
{
|
||||
name: "Should return IsGrafanaAdminExternallySynced = false for an externally synced JWT user if org roles are not being synced",
|
||||
@@ -299,6 +308,7 @@ func Test_GetUserByID(t *testing.T) {
|
||||
allowAssignGrafanaAdmin: true,
|
||||
skipOrgRoleSync: true,
|
||||
expectedIsGrafanaAdminSynced: false,
|
||||
expectedIsExternallySynced: false,
|
||||
},
|
||||
{
|
||||
name: "Should return IsGrafanaAdminExternallySynced = true for an externally synced JWT user",
|
||||
@@ -307,6 +317,31 @@ func Test_GetUserByID(t *testing.T) {
|
||||
allowAssignGrafanaAdmin: true,
|
||||
skipOrgRoleSync: false,
|
||||
expectedIsGrafanaAdminSynced: true,
|
||||
expectedIsExternallySynced: true,
|
||||
},
|
||||
{
|
||||
name: "Should return IsExternallySynced = true for an externally synced SAML user",
|
||||
authModule: login.SAMLAuthModule,
|
||||
authEnabled: true,
|
||||
skipOrgRoleSync: false,
|
||||
expectedIsGrafanaAdminSynced: false,
|
||||
expectedIsExternallySynced: true,
|
||||
},
|
||||
{
|
||||
name: "Should return IsExternallySynced = false for an externally synced SAML user if SAML provider is not enabled",
|
||||
authModule: login.SAMLAuthModule,
|
||||
authEnabled: false,
|
||||
skipOrgRoleSync: false,
|
||||
expectedIsGrafanaAdminSynced: false,
|
||||
expectedIsExternallySynced: false,
|
||||
},
|
||||
{
|
||||
name: "Should return IsExternallySynced = false for an externally synced SAML user if if org roles are not being synced",
|
||||
authModule: login.SAMLAuthModule,
|
||||
authEnabled: true,
|
||||
skipOrgRoleSync: true,
|
||||
expectedIsGrafanaAdminSynced: false,
|
||||
expectedIsExternallySynced: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
@@ -315,15 +350,28 @@ func Test_GetUserByID(t *testing.T) {
|
||||
authInfoService := &authinfotest.FakeService{ExpectedUserAuth: userAuth}
|
||||
socialService := &socialtest.FakeSocialService{}
|
||||
userService := &usertest.FakeUserService{ExpectedUserProfileDTO: &user.UserProfileDTO{}}
|
||||
authnService := &authntest.FakeService{
|
||||
ExpectedClientConfig: &authntest.FakeSSOClientConfig{
|
||||
ExpectedIsSkipOrgRoleSyncEnabled: tc.skipOrgRoleSync,
|
||||
ExpectedIsAllowAssignGrafanaAdminEnabled: tc.allowAssignGrafanaAdmin,
|
||||
},
|
||||
EnabledClients: []string{},
|
||||
}
|
||||
cfg := setting.NewCfg()
|
||||
|
||||
switch tc.authModule {
|
||||
case login.GenericOAuthModule:
|
||||
socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{AllowAssignGrafanaAdmin: tc.allowAssignGrafanaAdmin, Enabled: tc.authEnabled, SkipOrgRoleSync: tc.skipOrgRoleSync}
|
||||
if tc.authEnabled {
|
||||
authnService.EnabledClients = []string{authn.ClientWithPrefix("generic_oauth")}
|
||||
}
|
||||
case login.JWTModule:
|
||||
cfg.JWTAuth.Enabled = tc.authEnabled
|
||||
cfg.JWTAuth.SkipOrgRoleSync = tc.skipOrgRoleSync
|
||||
cfg.JWTAuth.AllowAssignGrafanaAdmin = tc.allowAssignGrafanaAdmin
|
||||
case login.SAMLAuthModule:
|
||||
if tc.authEnabled {
|
||||
authnService.EnabledClients = []string{authn.ClientSAML}
|
||||
}
|
||||
}
|
||||
|
||||
hs := &HTTPServer{
|
||||
@@ -331,6 +379,7 @@ func Test_GetUserByID(t *testing.T) {
|
||||
authInfoService: authInfoService,
|
||||
SocialService: socialService,
|
||||
userService: userService,
|
||||
authnService: authnService,
|
||||
}
|
||||
|
||||
sc := setupScenarioContext(t, "/api/users/1")
|
||||
@@ -348,13 +397,13 @@ func Test_GetUserByID(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.expectedIsGrafanaAdminSynced, resp.IsGrafanaAdminExternallySynced)
|
||||
assert.Equal(t, tc.expectedIsExternallySynced, resp.IsExternallySynced)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPServer_UpdateUser(t *testing.T) {
|
||||
settings := setting.NewCfg()
|
||||
settings.SAMLAuthEnabled = true
|
||||
sqlStore := db.InitTestDB(t)
|
||||
|
||||
hs := &HTTPServer{
|
||||
@@ -362,6 +411,9 @@ func TestHTTPServer_UpdateUser(t *testing.T) {
|
||||
SQLStore: sqlStore,
|
||||
AccessControl: acmock.New(),
|
||||
SocialService: &socialtest.FakeSocialService{ExpectedAuthInfoProvider: &social.OAuthInfo{Enabled: true}},
|
||||
authnService: &authntest.FakeService{
|
||||
EnabledClients: []string{authn.ClientSAML},
|
||||
},
|
||||
}
|
||||
|
||||
updateUserCommand := user.UpdateUserCommand{
|
||||
@@ -411,7 +463,7 @@ func setupUpdateEmailTests(t *testing.T, cfg *setting.Cfg) (*user.User, *HTTPSer
|
||||
require.NoError(t, err)
|
||||
|
||||
nsMock := notifications.MockNotificationService()
|
||||
verifier := userimpl.ProvideVerifier(cfg, userSvc, tempUserService, nsMock, &idtest.MockService{})
|
||||
verifier := userimpl.ProvideVerifier(cfg, userSvc, tempUserService, nsMock, &idtest.FakeService{})
|
||||
|
||||
hs := &HTTPServer{
|
||||
Cfg: cfg,
|
||||
@@ -636,7 +688,7 @@ func TestUser_UpdateEmail(t *testing.T) {
|
||||
hs.tempUserService = tempUserSvc
|
||||
hs.NotificationService = nsMock
|
||||
hs.SecretsService = fakes.NewFakeSecretsService()
|
||||
hs.userVerifier = userimpl.ProvideVerifier(settings, userSvc, tempUserSvc, nsMock, &idtest.MockService{})
|
||||
hs.userVerifier = userimpl.ProvideVerifier(settings, userSvc, tempUserSvc, nsMock, &idtest.FakeService{})
|
||||
// User is internal
|
||||
hs.authInfoService = &authinfotest.FakeService{ExpectedError: user.ErrUserNotFound}
|
||||
})
|
||||
@@ -1102,13 +1154,15 @@ func updateUserScenario(t *testing.T, ctx updateUserContext, hs *HTTPServer) {
|
||||
func TestHTTPServer_UpdateSignedInUser(t *testing.T) {
|
||||
settings := setting.NewCfg()
|
||||
sqlStore := db.InitTestDB(t)
|
||||
settings.SAMLAuthEnabled = true
|
||||
|
||||
hs := &HTTPServer{
|
||||
Cfg: settings,
|
||||
SQLStore: sqlStore,
|
||||
AccessControl: acmock.New(),
|
||||
SocialService: &socialtest.FakeSocialService{},
|
||||
authnService: &authntest.FakeService{
|
||||
EnabledClients: []string{authn.ClientSAML},
|
||||
},
|
||||
}
|
||||
|
||||
updateUserCommand := user.UpdateUserCommand{
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/ua-parser/uap-go/uaparser"
|
||||
|
||||
"github.com/grafana/authlib/claims"
|
||||
claims "github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/infra/network"
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ func (hs *HTTPServer) isExternalUser(ctx context.Context, userID int64) (bool, e
|
||||
return true, err
|
||||
}
|
||||
|
||||
return login.IsProviderEnabled(hs.Cfg, info.AuthModule, hs.SocialService.GetOAuthInfoProvider(info.AuthModule)), nil
|
||||
return hs.isProviderEnabled(hs.Cfg, info.AuthModule), nil
|
||||
}
|
||||
|
||||
func ValidateAndNormalizeEmail(email string) (string, error) {
|
||||
|
||||
Reference in New Issue
Block a user