mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into explore-styling-fixes
This commit is contained in:
commit
ac6170a7cc
@ -3,6 +3,9 @@
|
|||||||
### Minor
|
### Minor
|
||||||
|
|
||||||
* **Elasticsearch**: Add support for offset in date histogram aggregation [#12653](https://github.com/grafana/grafana/issues/12653), thx [@mattiarossi](https://github.com/mattiarossi)
|
* **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)
|
# 5.4.0 (2018-12-03)
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"company": "Grafana Labs"
|
"company": "Grafana Labs"
|
||||||
},
|
},
|
||||||
"name": "grafana",
|
"name": "grafana",
|
||||||
"version": "5.4.0-pre1",
|
"version": "5.5.0-pre1",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "http://github.com/grafana/grafana.git"
|
"url": "http://github.com/grafana/grafana.git"
|
||||||
|
@ -18,3 +18,8 @@ docker build \
|
|||||||
.
|
.
|
||||||
|
|
||||||
docker push "${_docker_repo}:${_grafana_tag}"
|
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
|
||||||
|
@ -76,6 +76,7 @@ func AdminUpdateUserPassword(c *m.ReqContext, form dtos.AdminUpdateUserPasswordF
|
|||||||
c.JsonOK("User password updated")
|
c.JsonOK("User password updated")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PUT /api/admin/users/:id/permissions
|
||||||
func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermissionsForm) {
|
func AdminUpdateUserPermissions(c *m.ReqContext, form dtos.AdminUpdateUserPermissionsForm) {
|
||||||
userID := c.ParamsInt64(":id")
|
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 := 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)
|
c.JsonApiErr(500, "Failed to update user permissions", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
50
pkg/api/admin_users_test.go
Normal file
50
pkg/api/admin_users_test.go
Normal file
@ -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)
|
||||||
|
})
|
||||||
|
}
|
@ -4,10 +4,18 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SendResetPasswordEmail(c *m.ReqContext, form dtos.SendResetPasswordEmailForm) Response {
|
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}
|
userQuery := m.GetUserByLoginQuery{LoginOrEmail: form.UserOrEmail}
|
||||||
|
|
||||||
if err := bus.Dispatch(&userQuery); err != nil {
|
if err := bus.Dispatch(&userQuery); err != nil {
|
||||||
|
@ -51,7 +51,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
|
|||||||
if token, err := tokenProvider.getAccessToken(data); err != nil {
|
if token, err := tokenProvider.getAccessToken(data); err != nil {
|
||||||
logger.Error("Failed to get access token", "error", err)
|
logger.Error("Failed to get access token", "error", err)
|
||||||
} else {
|
} 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 {
|
if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil {
|
||||||
logger.Error("Failed to get access token", "error", err)
|
logger.Error("Failed to get access token", "error", err)
|
||||||
} else {
|
} 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 {
|
if err != nil {
|
||||||
logger.Error("Failed to get default access token from meta data server", "error", err)
|
logger.Error("Failed to get default access token from meta data server", "error", err)
|
||||||
} else {
|
} else {
|
||||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
// Typed errors
|
// Typed errors
|
||||||
var (
|
var (
|
||||||
ErrUserNotFound = errors.New("User not found")
|
ErrUserNotFound = errors.New("User not found")
|
||||||
|
ErrLastGrafanaAdmin = errors.New("Cannot remove last grafana admin")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Password string
|
type Password string
|
||||||
|
@ -504,8 +504,18 @@ func UpdateUserPermissions(cmd *m.UpdateUserPermissionsCommand) error {
|
|||||||
|
|
||||||
user.IsAdmin = cmd.IsGrafanaAdmin
|
user.IsAdmin = cmd.IsGrafanaAdmin
|
||||||
sess.UseBool("is_admin")
|
sess.UseBool("is_admin")
|
||||||
|
|
||||||
_, err := sess.ID(user.Id).Update(&user)
|
_, err := sess.ID(user.Id).Update(&user)
|
||||||
|
if err != nil {
|
||||||
return err
|
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
|
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
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import coreModule from '../core_module';
|
import coreModule from '../core_module';
|
||||||
|
import config from 'app/core/config';
|
||||||
|
|
||||||
export class ResetPasswordCtrl {
|
export class ResetPasswordCtrl {
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
@ -6,6 +7,9 @@ export class ResetPasswordCtrl {
|
|||||||
contextSrv.sidemenu = false;
|
contextSrv.sidemenu = false;
|
||||||
$scope.formModel = {};
|
$scope.formModel = {};
|
||||||
$scope.mode = 'send';
|
$scope.mode = 'send';
|
||||||
|
$scope.ldapEnabled = config.ldapEnabled;
|
||||||
|
$scope.authProxyEnabled = config.authProxyEnabled;
|
||||||
|
$scope.disableLoginForm = config.disableLoginForm;
|
||||||
|
|
||||||
const params = $location.search();
|
const params = $location.search();
|
||||||
if (params.code) {
|
if (params.code) {
|
||||||
|
@ -590,8 +590,8 @@ kbn.valueFormats.flowcms = kbn.formatBuilders.fixedUnit('cms');
|
|||||||
kbn.valueFormats.flowcfs = kbn.formatBuilders.fixedUnit('cfs');
|
kbn.valueFormats.flowcfs = kbn.formatBuilders.fixedUnit('cfs');
|
||||||
kbn.valueFormats.flowcfm = kbn.formatBuilders.fixedUnit('cfm');
|
kbn.valueFormats.flowcfm = kbn.formatBuilders.fixedUnit('cfm');
|
||||||
kbn.valueFormats.litreh = kbn.formatBuilders.fixedUnit('l/h');
|
kbn.valueFormats.litreh = kbn.formatBuilders.fixedUnit('l/h');
|
||||||
kbn.valueFormats.flowlpm = kbn.formatBuilders.decimalSIPrefix('l/min');
|
kbn.valueFormats.flowlpm = kbn.formatBuilders.fixedUnit('l/min');
|
||||||
kbn.valueFormats.flowmlpm = kbn.formatBuilders.decimalSIPrefix('mL/min', -1);
|
kbn.valueFormats.flowmlpm = kbn.formatBuilders.fixedUnit('mL/min');
|
||||||
|
|
||||||
// Angle
|
// Angle
|
||||||
kbn.valueFormats.degree = kbn.formatBuilders.fixedUnit('°');
|
kbn.valueFormats.degree = kbn.formatBuilders.fixedUnit('°');
|
||||||
|
@ -351,6 +351,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onClickClear = () => {
|
onClickClear = () => {
|
||||||
|
this.onStopScanning();
|
||||||
this.modifiedQueries = ensureQueries();
|
this.modifiedQueries = ensureQueries();
|
||||||
this.setState(
|
this.setState(
|
||||||
prevState => ({
|
prevState => ({
|
||||||
|
@ -91,7 +91,7 @@ interface RowProps {
|
|||||||
function Row({ onClickLabel, row, showLabels, showLocalTime, showUtc }: RowProps) {
|
function Row({ onClickLabel, row, showLabels, showLocalTime, showUtc }: RowProps) {
|
||||||
const needsHighlighter = row.searchWords && row.searchWords.length > 0;
|
const needsHighlighter = row.searchWords && row.searchWords.length > 0;
|
||||||
return (
|
return (
|
||||||
<div className="logs-row">
|
<>
|
||||||
<div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''}>
|
<div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''}>
|
||||||
{row.duplicates > 0 && (
|
{row.duplicates > 0 && (
|
||||||
<div className="logs-row-level__duplicates" title={`${row.duplicates} duplicates`}>
|
<div className="logs-row-level__duplicates" title={`${row.duplicates} duplicates`}>
|
||||||
@ -128,7 +128,7 @@ function Row({ onClickLabel, row, showLabels, showLocalTime, showUtc }: RowProps
|
|||||||
row.entry
|
row.entry
|
||||||
)}
|
)}
|
||||||
</div>
|
</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...';
|
const scanText = scanRange ? `Scanning ${rangeUtil.describeTimeRange(scanRange)}` : 'Scanning...';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -329,7 +345,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="logs-entries">
|
<div className="logs-entries" style={logEntriesStyle}>
|
||||||
{hasData &&
|
{hasData &&
|
||||||
!deferLogs &&
|
!deferLogs &&
|
||||||
firstRows.map(row => (
|
firstRows.map(row => (
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
<button type="submit" class="btn btn-large p-x-2 btn-inverse btn-loading" ng-if="loggingIn">
|
<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>
|
Logging In<span>.</span><span>.</span><span>.</span>
|
||||||
</button>
|
</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">
|
<a href="user/password/send-reset-email">
|
||||||
Forgot your password?
|
Forgot your password?
|
||||||
</a>
|
</a>
|
||||||
|
@ -3,7 +3,14 @@
|
|||||||
<div class="page-container page-body">
|
<div class="page-container page-body">
|
||||||
<div class="signup">
|
<div class="signup">
|
||||||
<h3 class="p-b-1">Reset password</h3>
|
<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">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-7">User</span>
|
<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">
|
<input type="text" name="username" class="gf-form-input max-width-14" required ng-model='formModel.userOrEmail' placeholder="email or username">
|
||||||
|
@ -294,31 +294,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logs-entries {
|
.logs-entries {
|
||||||
|
display: grid;
|
||||||
|
grid-column-gap: 1rem;
|
||||||
|
grid-row-gap: 0.1rem;
|
||||||
font-family: $font-family-monospace;
|
font-family: $font-family-monospace;
|
||||||
font-size: 12px;
|
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 {
|
.logs-row-match-highlight {
|
||||||
// Undoing mark styling
|
// Undoing mark styling
|
||||||
background: inherit;
|
background: inherit;
|
||||||
|
Loading…
Reference in New Issue
Block a user