Commit ac6170a7 by Torkel Ödegaard

Merge branch 'master' into explore-styling-fixes

parents 1283a329 a69ab2fb
...@@ -3,6 +3,9 @@ ...@@ -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 @@ ...@@ -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 \ ...@@ -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 ...@@ -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 ...@@ -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
} }
......
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 ( ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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))
} }
} }
} }
......
...@@ -7,7 +7,8 @@ import ( ...@@ -7,7 +7,8 @@ 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 { ...@@ -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)
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 { ...@@ -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) { ...@@ -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 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 { ...@@ -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'); ...@@ -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> { ...@@ -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 { ...@@ -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 ...@@ -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> { ...@@ -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> { ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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;
......
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