Commit ac6170a7 by Torkel Ödegaard

Merge branch 'master' into explore-styling-fixes

parents 1283a329 a69ab2fb
......@@ -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
}
......
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)
})
})
})
}
......
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;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment