diff --git a/CHANGELOG.md b/CHANGELOG.md index 2654361c4e8..ab5263eba03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ### Minor * **Elasticsearch**: Add support for offset in date histogram aggregation [#12653](https://github.com/grafana/grafana/issues/12653), thx [@mattiarossi](https://github.com/mattiarossi) +* **Auth**: Prevent password reset when login form is disabled or either LDAP or Auth Proxy is enabled [#14246](https://github.com/grafana/grafana/issues/14246), thx [@SilverFire](https://github.com/SilverFire) +* **Dataproxy**: Override incoming Authorization header [#13815](https://github.com/grafana/grafana/issues/13815), thx [@kornholi](https://github.com/kornholi) +* **Admin**: Fix prevent removing last grafana admin permissions [#11067](https://github.com/grafana/grafana/issues/11067), thx [@danielbh](https://github.com/danielbh) # 5.4.0 (2018-12-03) diff --git a/package.json b/package.json index 68b7647fb58..f5e6e22a8a2 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Grafana Labs" }, "name": "grafana", - "version": "5.4.0-pre1", + "version": "5.5.0-pre1", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" diff --git a/packaging/docker/build-enterprise.sh b/packaging/docker/build-enterprise.sh index e10e0d691e8..10c24784d5c 100755 --- a/packaging/docker/build-enterprise.sh +++ b/packaging/docker/build-enterprise.sh @@ -18,3 +18,8 @@ docker build \ . docker push "${_docker_repo}:${_grafana_tag}" + +if echo "$_raw_grafana_tag" | grep -q "^v" && echo "$_raw_grafana_tag" | grep -qv "beta"; then + docker tag "${_docker_repo}:${_grafana_tag}" "${_docker_repo}:latest" + docker push "${_docker_repo}:latest" +fi diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index dc3d390dda9..efc760d2b51 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -76,6 +76,7 @@ func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordF c.JsonOK("User password updated") } +// PUT /api/admin/users/:id/permissions func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermissionsForm) { userID := c.ParamsInt64(":id") @@ -85,6 +86,11 @@ func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermis } if err := bus.Dispatch(&cmd); err != nil { + if err == m.ErrLastGrafanaAdmin { + c.JsonApiErr(400, m.ErrLastGrafanaAdmin.Error(), nil) + return + } + c.JsonApiErr(500, "Failed to update user permissions", err) return } diff --git a/pkg/api/admin_users_test.go b/pkg/api/admin_users_test.go new file mode 100644 index 00000000000..0b94a64b3fb --- /dev/null +++ b/pkg/api/admin_users_test.go @@ -0,0 +1,50 @@ +package api + +import ( + "testing" + + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestAdminApiEndpoint(t *testing.T) { + role := m.ROLE_ADMIN + Convey("Given a server admin attempts to remove themself as an admin", t, func() { + + updateCmd := dtos.AdminUpdateUserPermissionsForm{ + IsGrafanaAdmin: false, + } + + bus.AddHandler("test", func(cmd *m.UpdateUserPermissionsCommand) error { + return m.ErrLastGrafanaAdmin + }) + + putAdminScenario("When calling PUT on", "/api/admin/users/1/permissions", "/api/admin/users/:id/permissions", role, updateCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 400) + }) + }) +} + +func putAdminScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc) { + Convey(desc+" "+url, func() { + defer bus.ClearBusHandlers() + + sc := setupScenarioContext(url) + sc.defaultHandler = Wrap(func(c *m.ReqContext) { + sc.context = c + sc.context.UserId = TestUserID + sc.context.OrgId = TestOrgID + sc.context.OrgRole = role + + AdminUpdateUserPermissions(c, cmd) + }) + + sc.m.Put(routePattern, sc.defaultHandler) + + fn(sc) + }) +} diff --git a/pkg/api/password.go b/pkg/api/password.go index 7dd901c898e..4776c6a3064 100644 --- a/pkg/api/password.go +++ b/pkg/api/password.go @@ -4,10 +4,18 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) func SendResetPasswordEmail(c *m.ReqContext, form dtos.SendResetPasswordEmailForm) Response { + if setting.LdapEnabled || setting.AuthProxyEnabled { + return Error(401, "Not allowed to reset password when LDAP or Auth Proxy is enabled", nil) + } + if setting.DisableLoginForm { + return Error(401, "Not allowed to reset password when login form is disabled", nil) + } + userQuery := m.GetUserByLoginQuery{LoginOrEmail: form.UserOrEmail} if err := bus.Dispatch(&userQuery); err != nil { diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index edf43085c7c..5c5776eec07 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -51,7 +51,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route if token, err := tokenProvider.getAccessToken(data); err != nil { logger.Error("Failed to get access token", "error", err) } else { - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) } } @@ -60,7 +60,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil { logger.Error("Failed to get access token", "error", err) } else { - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) } } @@ -73,7 +73,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route if err != nil { logger.Error("Failed to get default access token from meta data server", "error", err) } else { - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) } } } diff --git a/pkg/models/user.go b/pkg/models/user.go index e3c7b556d35..69031e40338 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -7,7 +7,8 @@ import ( // Typed errors var ( - ErrUserNotFound = errors.New("User not found") + ErrUserNotFound = errors.New("User not found") + ErrLastGrafanaAdmin = errors.New("Cannot remove last grafana admin") ) type Password string diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 99a77ecabc3..a3ccb93b30c 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -504,8 +504,18 @@ func UpdateUserPermissions(cmd *m.UpdateUserPermissionsCommand) error { user.IsAdmin = cmd.IsGrafanaAdmin sess.UseBool("is_admin") + _, err := sess.ID(user.Id).Update(&user) - return err + if err != nil { + return err + } + + // validate that after update there is at least one server admin + if err := validateOneAdminLeft(sess); err != nil { + return err + } + + return nil }) } @@ -522,3 +532,17 @@ func SetUserHelpFlag(cmd *m.SetUserHelpFlagCommand) error { return err }) } + +func validateOneAdminLeft(sess *DBSession) error { + // validate that there is an admin user left + count, err := sess.Where("is_admin=?", true).Count(&m.User{}) + if err != nil { + return err + } + + if count == 0 { + return m.ErrLastGrafanaAdmin + } + + return nil +} diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go index b26dd235772..627f2ab1ca5 100644 --- a/pkg/services/sqlstore/user_test.go +++ b/pkg/services/sqlstore/user_test.go @@ -155,6 +155,32 @@ func TestUserDataAccess(t *testing.T) { }) }) }) + + Convey("Given one grafana admin user", func() { + var err error + createUserCmd := &m.CreateUserCommand{ + Email: fmt.Sprint("admin", "@test.com"), + Name: fmt.Sprint("admin"), + Login: fmt.Sprint("admin"), + IsAdmin: true, + } + err = CreateUser(context.Background(), createUserCmd) + So(err, ShouldBeNil) + + Convey("Cannot make themselves a non-admin", func() { + updateUserPermsCmd := m.UpdateUserPermissionsCommand{IsGrafanaAdmin: false, UserId: 1} + updatePermsError := UpdateUserPermissions(&updateUserPermsCmd) + + So(updatePermsError, ShouldEqual, m.ErrLastGrafanaAdmin) + + query := m.GetUserByIdQuery{Id: createUserCmd.Result.Id} + getUserError := GetUserById(&query) + + So(getUserError, ShouldBeNil) + + So(query.Result.IsAdmin, ShouldEqual, true) + }) + }) }) } diff --git a/public/app/core/controllers/reset_password_ctrl.ts b/public/app/core/controllers/reset_password_ctrl.ts index 933655399e8..9ad6864ece2 100644 --- a/public/app/core/controllers/reset_password_ctrl.ts +++ b/public/app/core/controllers/reset_password_ctrl.ts @@ -1,4 +1,5 @@ import coreModule from '../core_module'; +import config from 'app/core/config'; export class ResetPasswordCtrl { /** @ngInject */ @@ -6,6 +7,9 @@ export class ResetPasswordCtrl { contextSrv.sidemenu = false; $scope.formModel = {}; $scope.mode = 'send'; + $scope.ldapEnabled = config.ldapEnabled; + $scope.authProxyEnabled = config.authProxyEnabled; + $scope.disableLoginForm = config.disableLoginForm; const params = $location.search(); if (params.code) { diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index 9caa5bf1a54..e0b98cb803c 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -590,8 +590,8 @@ kbn.valueFormats.flowcms = kbn.formatBuilders.fixedUnit('cms'); kbn.valueFormats.flowcfs = kbn.formatBuilders.fixedUnit('cfs'); kbn.valueFormats.flowcfm = kbn.formatBuilders.fixedUnit('cfm'); kbn.valueFormats.litreh = kbn.formatBuilders.fixedUnit('l/h'); -kbn.valueFormats.flowlpm = kbn.formatBuilders.decimalSIPrefix('l/min'); -kbn.valueFormats.flowmlpm = kbn.formatBuilders.decimalSIPrefix('mL/min', -1); +kbn.valueFormats.flowlpm = kbn.formatBuilders.fixedUnit('l/min'); +kbn.valueFormats.flowmlpm = kbn.formatBuilders.fixedUnit('mL/min'); // Angle kbn.valueFormats.degree = kbn.formatBuilders.fixedUnit('°'); diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 3e35a68bf84..71e23640bec 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -351,6 +351,7 @@ export class Explore extends React.PureComponent { }; onClickClear = () => { + this.onStopScanning(); this.modifiedQueries = ensureQueries(); this.setState( prevState => ({ diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 6447ec895ed..79207f28074 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -91,7 +91,7 @@ interface RowProps { function Row({ onClickLabel, row, showLabels, showLocalTime, showUtc }: RowProps) { const needsHighlighter = row.searchWords && row.searchWords.length > 0; return ( -
+ <>
{row.duplicates > 0 && (
@@ -128,7 +128,7 @@ function Row({ onClickLabel, row, showLabels, showLocalTime, showUtc }: RowProps row.entry )}
-
+ ); } @@ -270,6 +270,22 @@ export default class Logs extends PureComponent { } } + // Grid options + const cssColumnSizes = ['3px']; // Log-level indicator line + if (showUtc) { + cssColumnSizes.push('minmax(100px, max-content)'); + } + if (showLocalTime) { + cssColumnSizes.push('minmax(100px, max-content)'); + } + if (showLabels) { + cssColumnSizes.push('fit-content(20%)'); + } + cssColumnSizes.push('1fr'); + const logEntriesStyle = { + gridTemplateColumns: cssColumnSizes.join(' '), + }; + const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...'; return ( @@ -329,7 +345,7 @@ export default class Logs extends PureComponent {
-
+
{hasData && !deferLogs && firstRows.map(row => ( diff --git a/public/app/partials/login.html b/public/app/partials/login.html index a2bc8173766..f4237e7b1ec 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -22,7 +22,7 @@ -