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
|
||||
|
||||
* **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)
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
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/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 {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 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) {
|
||||
|
@ -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('°');
|
||||
|
@ -351,6 +351,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
};
|
||||
|
||||
onClickClear = () => {
|
||||
this.onStopScanning();
|
||||
this.modifiedQueries = ensureQueries();
|
||||
this.setState(
|
||||
prevState => ({
|
||||
|
@ -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 => (
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user