Просмотр исходного кода

Merge branch 'master' into explore-styling-fixes

Torkel Ödegaard 7 лет назад
Родитель
Сommit
ac6170a7cc

+ 3 - 0
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)
 

+ 1 - 1
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"

+ 5 - 0
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

+ 6 - 0
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
 	}

+ 50 - 0
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)
+	})
+}

+ 8 - 0
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 {

+ 3 - 3
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))
 			}
 		}
 	}

+ 2 - 1
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

+ 25 - 1
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
+}

+ 26 - 0
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)
+			})
+		})
 	})
 }
 

+ 4 - 0
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) {

+ 2 - 2
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('°');

+ 1 - 0
public/app/features/explore/Explore.tsx

@@ -351,6 +351,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
   };
 
   onClickClear = () => {
+    this.onStopScanning();
     this.modifiedQueries = ensureQueries();
     this.setState(
       prevState => ({

+ 19 - 3
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 (
-    <div className="logs-row">
+    <>
       <div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''}>
         {row.duplicates > 0 && (
           <div className="logs-row-level__duplicates" title={`${row.duplicates} duplicates`}>
@@ -128,7 +128,7 @@ function Row({ onClickLabel, row, showLabels, showLocalTime, showUtc }: RowProps
           row.entry
         )}
       </div>
-    </div>
+    </>
   );
 }
 
@@ -270,6 +270,22 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
       }
     }
 
+    // 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<LogsProps, LogsState> {
           </div>
         </div>
 
-        <div className="logs-entries">
+        <div className="logs-entries" style={logEntriesStyle}>
           {hasData &&
             !deferLogs &&
             firstRows.map(row => (

+ 1 - 1
public/app/partials/login.html

@@ -22,7 +22,7 @@
             <button type="submit" class="btn btn-large p-x-2 btn-inverse btn-loading" ng-if="loggingIn">
               Logging In<span>.</span><span>.</span><span>.</span>
             </button>
-            <div class="small login-button-forgot-password">
+            <div class="small login-button-forgot-password" ng-hide="ldapEnabled || authProxyEnabled">
               <a href="user/password/send-reset-email">
                 Forgot your password?
               </a>

+ 8 - 1
public/app/partials/reset_password.html

@@ -3,7 +3,14 @@
 <div class="page-container page-body">
 	<div class="signup">
 		<h3 class="p-b-1">Reset password</h3>
-		<form name="sendResetForm" class="login-form gf-form-group" ng-show="mode === 'send'">
+
+		<div ng-if="ldapEnabled || authProxyEnabled">
+			You cannot reset password when LDAP or Auth Proxy authentication is enabled.
+		</div>
+		<div ng-if="disableLoginForm">
+			You cannot reset password when login form is disabled.
+		</div>
+		<form name="sendResetForm" class="login-form gf-form-group" ng-show="mode === 'send'" ng-hide="ldapEnabled || authProxyEnabled || disableLoginForm">
 			<div class="gf-form">
 					<span class="gf-form-label width-7">User</span>
 					<input type="text" name="username" class="gf-form-input max-width-14" required ng-model='formModel.userOrEmail' placeholder="email or username">

+ 3 - 21
public/sass/pages/_explore.scss

@@ -294,31 +294,13 @@
     }
 
     .logs-entries {
+      display: grid;
+      grid-column-gap: 1rem;
+      grid-row-gap: 0.1rem;
       font-family: $font-family-monospace;
       font-size: 12px;
     }
 
-    .logs-row {
-      display: flex;
-      flex-direction: row;
-
-      > div + div {
-        margin-left: 0.5rem;
-      }
-    }
-
-    .logs-row-level {
-      width: 3px;
-    }
-
-    .logs-row-labels {
-      flex: 0 0 25%;
-    }
-
-    .logs-row-message {
-      flex: 1;
-    }
-
     .logs-row-match-highlight {
       // Undoing mark styling
       background: inherit;