From b980cedfd5653e2dabcb7aa68864d3ce343ee71e Mon Sep 17 00:00:00 2001 From: ilyastoli <52413756+ilyastoli@users.noreply.github.com> Date: Wed, 15 Apr 2020 10:35:21 +0300 Subject: [PATCH 01/11] CloudWatch: Added AWS Chatbot metrics and dimensions (#23516) Adding AWS Chatbot metics according to https://docs.aws.amazon.com/chatbot/latest/adminguide/monitoring-cloudwatch.html --- pkg/tsdb/cloudwatch/metric_find_query.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index 31d37434ca8..35a8163ff75 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -49,6 +49,7 @@ func init() { "AWS/Athena": {"DataScannedInBytes", "EngineExecutionTime", "QueryPlanningTime", "QueryQueueTime", "QueryState", "QueryType", "ServiceProcessingTime", "TotalExecutionTime", "WorkGroup"}, "AWS/AutoScaling": {"GroupDesiredCapacity", "GroupInServiceInstances", "GroupMaxSize", "GroupMinSize", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"}, "AWS/Billing": {"EstimatedCharges"}, + "AWS/Chatbot": {"EventsThrottled", "EventsProcessed", "MessageDeliverySuccess", "MessageDeliveryFailure", "UnsupportedEvents"}, "AWS/CloudFront": {"4xxErrorRate", "5xxErrorRate", "BytesDownloaded", "BytesUploaded", "Requests", "TotalErrorRate"}, "AWS/CloudHSM": {"HsmKeysSessionOccupied", "HsmKeysTokenOccupied", "HsmSessionCount", "HsmSslCtxsOccupied", "HsmTemperature", "HsmUnhealthy", "HsmUsersAvailable", "HsmUsersMax", "InterfaceEth2OctetsInput", "InterfaceEth2OctetsOutput"}, "AWS/CloudSearch": {"IndexUtilization", "Partitions", "SearchableDocuments", "SuccessfulRequests"}, @@ -135,6 +136,7 @@ func init() { "AWS/ApplicationELB": {"AvailabilityZone", "LoadBalancer", "TargetGroup"}, "AWS/AutoScaling": {"AutoScalingGroupName"}, "AWS/Billing": {"Currency", "LinkedAccount", "ServiceName"}, + "AWS/Chatbot": {"ConfigurationName"}, "AWS/CloudFront": {"DistributionId", "Region"}, "AWS/CloudHSM": {"ClusterId", "HsmId", "Region"}, "AWS/CloudSearch": {"ClientId", "DomainName"}, From fa6a43c6e592a1b077ffb63143cb3cf090fe5a15 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Wed, 15 Apr 2020 10:11:25 +0200 Subject: [PATCH 02/11] Docs: Select (#23398) * Start Select docs * Writ emore docs * Apply suggestions from code review Co-Authored-By: Alex Khomenko Co-authored-by: Alex Khomenko --- .../src/components/Select/Select.mdx | 133 ++++++++++++++++++ .../src/components/Select/Select.story.tsx | 7 + .../src/components/Select/mockOptions.tsx | 3 +- .../grafana-ui/src/components/Select/types.ts | 6 + 4 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 packages/grafana-ui/src/components/Select/Select.mdx diff --git a/packages/grafana-ui/src/components/Select/Select.mdx b/packages/grafana-ui/src/components/Select/Select.mdx new file mode 100644 index 00000000000..5ca57e225c5 --- /dev/null +++ b/packages/grafana-ui/src/components/Select/Select.mdx @@ -0,0 +1,133 @@ +import { Props, Preview } from "@storybook/addon-docs/blocks"; +import { Select, AsyncSelect, MultiSelect, AsyncMultiSelect } from "./Select"; +import { generateOptions } from "./mockOptions"; + +# Select variants + +Select is an input with the ability to search and create new values. It should be used when you have a list of options. If the data has a tree structure, consider using `Cascader` instead. +Select has some features: + +- Search a list of values +- Select multiple values +- Select from async data +- Create custom values that aren't in the list + +## Select + +Select is the base for every component on this page. The approaches mentioned here are also applicable to `AsyncSelect`, `MultiSelect`, `AsyncMultiSelect`. + +### Options format + +There are four properties for each option: + +- `label` - Text that is visible in the menu. +- `value` - Could be anything, but is usually a string. Used to identify what is **actually** selected. +- `description` - Longer description that describes the choice. Use this sparingly. +- `imgUrl` - URL to an image. Use this when an image or icon provides more context for the option. + +```jsx +const options = [ + { label: "Basic option", value: 0 }, + { label: "Option with description", value: 1, description: "this is a description" }, + { + label: "Option with description and image", + value: 2, + description: "This is a very elaborate description, describing all the wonders in the world.", + imgUrl: "https://placekitten.com/40/40", + }, +]; +``` + +### Creatable option + +Creatable option is used when you want to be able to add a custom value to the list of options. `allowCustomValue` needs to be true and you must handle the value creation with `onCreateOption`. + +```jsx +import { Select } from "@grafana/ui"; + +const SelectComponent = () => { + const [value, setValue] = useState>(); + + return ( + + + + + + ); + } +} diff --git a/packages/grafana-ui/src/components/Forms/Switch.mdx b/packages/grafana-ui/src/components/Forms/Switch.mdx deleted file mode 100644 index 1e1936a26cc..00000000000 --- a/packages/grafana-ui/src/components/Forms/Switch.mdx +++ /dev/null @@ -1,25 +0,0 @@ -import { Meta, Story, Preview, Props } from "@storybook/addon-docs/blocks"; -import { Switch } from "./Switch"; - - - -# Switch - -### When to use - -`Switch` is a representation of an on-off state – like a light switch. So you can use `Switch` to toggle binary states. - -Switches trigger changes immediately. If your component should trigger a change only after sending a form, it's better to use either `RadioButtonGroup` or `Checkbox` instead. Furthermore, switches cannot be grouped – each `Switch` triggers an independent state. If you want multiple mutually exclusive choices, the `RadioButtonGroup` is the better option. To offer multiple choices within the same group or context which are not mutually exclusive, use `Checkbox` instead. - - -### Usage - -```jsx -import { Switch } from '@grafana/ui'; - - -``` - -### Props - - diff --git a/packages/grafana-ui/src/components/Forms/Switch.tsx b/packages/grafana-ui/src/components/Forms/Switch.tsx deleted file mode 100644 index 0848257a939..00000000000 --- a/packages/grafana-ui/src/components/Forms/Switch.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React, { HTMLProps } from 'react'; -import { css, cx } from 'emotion'; -import uniqueId from 'lodash/uniqueId'; -import { GrafanaTheme } from '@grafana/data'; -import { stylesFactory, useTheme } from '../../themes'; -import { getFocusCss } from './commonStyles'; - -export interface SwitchProps extends Omit, 'value'> { - value?: boolean; -} - -export const getSwitchStyles = stylesFactory((theme: GrafanaTheme) => { - return { - switch: css` - width: 32px; - height: 16px; - position: relative; - - input { - opacity: 0; - left: -100vw; - z-index: -1000; - position: absolute; - - &:disabled + label { - background: ${theme.colors.formSwitchBgDisabled}; - cursor: not-allowed; - } - - &:checked + label { - background: ${theme.colors.formSwitchBgActive}; - - &:hover { - background: ${theme.colors.formSwitchBgActiveHover}; - } - - &::after { - transform: translate3d(18px, -50%, 0); - } - } - - &:focus + label { - ${getFocusCss(theme)}; - } - } - - label { - width: 100%; - height: 100%; - cursor: pointer; - border: none; - border-radius: 50px; - background: ${theme.colors.formSwitchBg}; - transition: all 0.3s ease; - - &:hover { - background: ${theme.colors.formSwitchBgHover}; - } - - &::after { - position: absolute; - display: block; - content: ''; - width: 12px; - height: 12px; - border-radius: 6px; - background: ${theme.colors.formSwitchDot}; - top: 50%; - transform: translate3d(2px, -50%, 0); - transition: transform 0.2s cubic-bezier(0.19, 1, 0.22, 1); - } - } - } - `, - }; -}); - -export const Switch = React.forwardRef( - ({ value, checked, disabled = false, onChange, ...inputProps }, ref) => { - const theme = useTheme(); - const styles = getSwitchStyles(theme); - const switchId = uniqueId('switch-'); - - return ( -
- { - onChange?.(event); - }} - id={switchId} - {...inputProps} - ref={ref} - /> -
- ); - } -); diff --git a/packages/grafana-ui/src/components/Forms/getFormStyles.ts b/packages/grafana-ui/src/components/Forms/getFormStyles.ts index c46c3b003fd..11c329902bd 100644 --- a/packages/grafana-ui/src/components/Forms/getFormStyles.ts +++ b/packages/grafana-ui/src/components/Forms/getFormStyles.ts @@ -6,7 +6,7 @@ import { getFieldValidationMessageStyles } from './FieldValidationMessage'; import { getButtonStyles, ButtonVariant } from '../Button'; import { ComponentSize } from '../../types/size'; import { getInputStyles } from '../Input/Input'; -import { getSwitchStyles } from './Switch'; +import { getSwitchStyles } from '../Switch/Switch'; import { getCheckboxStyles } from './Checkbox'; export const getFormStyles = stylesFactory( diff --git a/packages/grafana-ui/src/components/Switch/Switch.mdx b/packages/grafana-ui/src/components/Switch/Switch.mdx index f347718b212..1e1936a26cc 100644 --- a/packages/grafana-ui/src/components/Switch/Switch.mdx +++ b/packages/grafana-ui/src/components/Switch/Switch.mdx @@ -1,3 +1,25 @@ +import { Meta, Story, Preview, Props } from "@storybook/addon-docs/blocks"; +import { Switch } from "./Switch"; + + + # Switch -A basic docs for Switch component +### When to use + +`Switch` is a representation of an on-off state – like a light switch. So you can use `Switch` to toggle binary states. + +Switches trigger changes immediately. If your component should trigger a change only after sending a form, it's better to use either `RadioButtonGroup` or `Checkbox` instead. Furthermore, switches cannot be grouped – each `Switch` triggers an independent state. If you want multiple mutually exclusive choices, the `RadioButtonGroup` is the better option. To offer multiple choices within the same group or context which are not mutually exclusive, use `Checkbox` instead. + + +### Usage + +```jsx +import { Switch } from '@grafana/ui'; + + +``` + +### Props + + diff --git a/packages/grafana-ui/src/components/Forms/Switch.story.tsx b/packages/grafana-ui/src/components/Switch/Switch.story.tsx similarity index 100% rename from packages/grafana-ui/src/components/Forms/Switch.story.tsx rename to packages/grafana-ui/src/components/Switch/Switch.story.tsx diff --git a/packages/grafana-ui/src/components/Switch/Switch.tsx b/packages/grafana-ui/src/components/Switch/Switch.tsx index 6dc7a3ecee4..8db64260d3a 100644 --- a/packages/grafana-ui/src/components/Switch/Switch.tsx +++ b/packages/grafana-ui/src/components/Switch/Switch.tsx @@ -1,72 +1,101 @@ -import React, { PureComponent } from 'react'; +import React, { HTMLProps } from 'react'; +import { css, cx } from 'emotion'; import uniqueId from 'lodash/uniqueId'; -import { Tooltip } from '../Tooltip/Tooltip'; -import { Icon } from '../Icon/Icon'; -import * as PopperJS from 'popper.js'; +import { GrafanaTheme } from '@grafana/data'; +import { stylesFactory, useTheme } from '../../themes'; +import { getFocusCss } from '../Forms/commonStyles'; -export interface Props { - label: string; - checked: boolean; - className?: string; - labelClass?: string; - switchClass?: string; - tooltip?: string; - tooltipPlacement?: PopperJS.Placement; - transparent?: boolean; - onChange: (event?: React.SyntheticEvent) => void; +export interface SwitchProps extends Omit, 'value'> { + value?: boolean; } -export interface State { - id: string; -} +export const getSwitchStyles = stylesFactory((theme: GrafanaTheme) => { + return { + switch: css` + width: 32px; + height: 16px; + position: relative; -export class Switch extends PureComponent { - state = { - id: uniqueId(), + input { + opacity: 0; + left: -100vw; + z-index: -1000; + position: absolute; + + &:disabled + label { + background: ${theme.colors.formSwitchBgDisabled}; + cursor: not-allowed; + } + + &:checked + label { + background: ${theme.colors.formSwitchBgActive}; + + &:hover { + background: ${theme.colors.formSwitchBgActiveHover}; + } + + &::after { + transform: translate3d(18px, -50%, 0); + } + } + + &:focus + label { + ${getFocusCss(theme)}; + } + } + + label { + width: 100%; + height: 100%; + cursor: pointer; + border: none; + border-radius: 50px; + background: ${theme.colors.formSwitchBg}; + transition: all 0.3s ease; + + &:hover { + background: ${theme.colors.formSwitchBgHover}; + } + + &::after { + position: absolute; + display: block; + content: ''; + width: 12px; + height: 12px; + border-radius: 6px; + background: ${theme.colors.formSwitchDot}; + top: 50%; + transform: translate3d(2px, -50%, 0); + transition: transform 0.2s cubic-bezier(0.19, 1, 0.22, 1); + } + } + } + `, }; +}); - internalOnChange = (event: React.FormEvent) => { - event.stopPropagation(); - this.props.onChange(event); - }; - - render() { - const { - labelClass = '', - switchClass = '', - label, - checked, - transparent, - className, - tooltip, - tooltipPlacement, - } = this.props; - - const labelId = this.state.id; - const labelClassName = `gf-form-label ${labelClass} ${transparent ? 'gf-form-label--transparent' : ''} pointer`; - const switchClassName = `gf-form-switch ${switchClass} ${transparent ? 'gf-form-switch--transparent' : ''}`; +export const Switch = React.forwardRef( + ({ value, checked, disabled = false, onChange, ...inputProps }, ref) => { + const theme = useTheme(); + const styles = getSwitchStyles(theme); + const switchId = uniqueId('switch-'); return ( -
- +
+ { + onChange?.(event); + }} + id={switchId} + {...inputProps} + ref={ref} + /> +
); } -} +); diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 46b334b8a63..ba1579a9d75 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -143,8 +143,10 @@ export { HorizontalGroup, VerticalGroup, Container } from './Layout/Layout'; export { RadioButtonGroup } from './Forms/RadioButtonGroup/RadioButtonGroup'; export { Input } from './Input/Input'; -export { Switch } from './Forms/Switch'; + +export { Switch } from './Switch/Switch'; export { Checkbox } from './Forms/Checkbox'; + export { TextArea } from './TextArea/TextArea'; // Legacy forms @@ -158,7 +160,7 @@ import { ButtonSelect } from './Forms/Legacy/Select/ButtonSelect'; //Input import { Input, LegacyInputStatus } from './Forms/Legacy/Input/Input'; -import { Switch } from './Switch/Switch'; +import { Switch } from './Forms/Legacy/Switch/Switch'; const LegacyForms = { Select, diff --git a/packages/grafana-ui/src/utils/standardEditors.tsx b/packages/grafana-ui/src/utils/standardEditors.tsx index 470ba283ea0..3bdcea7b38e 100644 --- a/packages/grafana-ui/src/utils/standardEditors.tsx +++ b/packages/grafana-ui/src/utils/standardEditors.tsx @@ -19,8 +19,9 @@ import { valueMappingsOverrideProcessor, ThresholdsMode, } from '@grafana/data'; + +import { Switch } from '../components/Switch/Switch'; import { NumberValueEditor, RadioButtonGroup, StringValueEditor, Select } from '../components'; -import { Switch } from '../components/Forms/Switch'; import { ValueMappingsValueEditor } from '../components/OptionsUI/mappings'; import { ThresholdsValueEditor } from '../components/OptionsUI/thresholds'; import { UnitValueEditor } from '../components/OptionsUI/units'; From d721dd13cdf416117418d181f94da4267f0b0bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20Fugulin?= Date: Wed, 15 Apr 2020 05:11:45 -0400 Subject: [PATCH 07/11] Allow API to assign new user to a specific organization (#21775) * Allow API to assign new user to a specific organization * Add defer block to test * Add API tests and return 400 instead of 500 for bad orgId * Minor test improvements --- docs/sources/http_api/admin.md | 5 +- pkg/api/admin_users.go | 6 ++ pkg/api/admin_users_test.go | 103 +++++++++++++++++++++++++++++ pkg/api/dtos/user.go | 1 + pkg/models/user.go | 1 + pkg/services/sqlstore/org.go | 12 ++++ pkg/services/sqlstore/user.go | 8 +++ pkg/services/sqlstore/user_test.go | 52 +++++++++++++++ 8 files changed, 187 insertions(+), 1 deletion(-) diff --git a/docs/sources/http_api/admin.md b/docs/sources/http_api/admin.md index 125a8e38bb9..77d56f66cf7 100644 --- a/docs/sources/http_api/admin.md +++ b/docs/sources/http_api/admin.md @@ -224,10 +224,13 @@ Content-Type: application/json "name":"User", "email":"user@graf.com", "login":"user", - "password":"userpassword" + "password":"userpassword", + "OrgId": 1 } ``` +Note that `OrgId` is an optional parameter that can be used to assign a new user to a different organization when [auto_assign_org](https://grafana.com/docs/grafana/latest/installation/configuration/#auto-assign-org) is set to `true`. + **Example Response**: ```http diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index dff36c19084..4fd43ef7125 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -14,6 +14,7 @@ func AdminCreateUser(c *models.ReqContext, form dtos.AdminCreateUserForm) { Email: form.Email, Password: form.Password, Name: form.Name, + OrgId: form.OrgId, } if len(cmd.Login) == 0 { @@ -30,6 +31,11 @@ func AdminCreateUser(c *models.ReqContext, form dtos.AdminCreateUserForm) { } if err := bus.Dispatch(&cmd); err != nil { + if err == models.ErrOrgNotFound { + c.JsonApiErr(400, models.ErrOrgNotFound.Error(), nil) + return + } + c.JsonApiErr(500, "failed to create user", err) return } diff --git a/pkg/api/admin_users_test.go b/pkg/api/admin_users_test.go index bed74a2c688..c7b32f65976 100644 --- a/pkg/api/admin_users_test.go +++ b/pkg/api/admin_users_test.go @@ -12,6 +12,12 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +const ( + TestLogin = "test@example.com" + TestPassword = "password" + nonExistingOrgID = 1000 +) + func TestAdminApiEndpoint(t *testing.T) { role := models.ROLE_ADMIN Convey("Given a server admin attempts to remove themself as an admin", t, func() { @@ -175,6 +181,85 @@ func TestAdminApiEndpoint(t *testing.T) { So(userId, ShouldEqual, 42) }) }) + + Convey("When a server admin attempts to create a user", t, func() { + var userLogin string + var orgId int64 + + bus.AddHandler("test", func(cmd *models.CreateUserCommand) error { + userLogin = cmd.Login + orgId = cmd.OrgId + + if orgId == nonExistingOrgID { + return models.ErrOrgNotFound + } + + cmd.Result = models.User{Id: TestUserID} + return nil + }) + + Convey("Without an organization", func() { + createCmd := dtos.AdminCreateUserForm{ + Login: TestLogin, + Password: TestPassword, + } + + adminCreateUserScenario("Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 200) + + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + So(err, ShouldBeNil) + So(respJSON.Get("id").MustInt64(), ShouldEqual, TestUserID) + So(respJSON.Get("message").MustString(), ShouldEqual, "User created") + + // test that userLogin and orgId were transmitted correctly to the handler + So(userLogin, ShouldEqual, TestLogin) + So(orgId, ShouldEqual, 0) + }) + }) + + Convey("With an organization", func() { + createCmd := dtos.AdminCreateUserForm{ + Login: TestLogin, + Password: TestPassword, + OrgId: TestOrgID, + } + + adminCreateUserScenario("Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 200) + + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + So(err, ShouldBeNil) + So(respJSON.Get("id").MustInt64(), ShouldEqual, TestUserID) + So(respJSON.Get("message").MustString(), ShouldEqual, "User created") + + So(userLogin, ShouldEqual, TestLogin) + So(orgId, ShouldEqual, TestOrgID) + }) + }) + + Convey("With a nonexistent organization", func() { + createCmd := dtos.AdminCreateUserForm{ + Login: TestLogin, + Password: TestPassword, + OrgId: nonExistingOrgID, + } + + adminCreateUserScenario("Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 400) + + respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) + So(err, ShouldBeNil) + So(respJSON.Get("message").MustString(), ShouldEqual, "Organization not found") + + So(userLogin, ShouldEqual, TestLogin) + So(orgId, ShouldEqual, 1000) + }) + }) + }) } func putAdminScenario(desc string, url string, routePattern string, role models.RoleType, cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc) { @@ -324,3 +409,21 @@ func adminDeleteUserScenario(desc string, url string, routePattern string, fn sc fn(sc) }) } + +func adminCreateUserScenario(desc string, url string, routePattern string, cmd dtos.AdminCreateUserForm, fn scenarioFunc) { + Convey(desc+" "+url, func() { + defer bus.ClearBusHandlers() + + sc := setupScenarioContext(url) + sc.defaultHandler = Wrap(func(c *models.ReqContext) { + sc.context = c + sc.context.UserId = TestUserID + + AdminCreateUser(c, cmd) + }) + + sc.m.Post(routePattern, sc.defaultHandler) + + fn(sc) + }) +} diff --git a/pkg/api/dtos/user.go b/pkg/api/dtos/user.go index d6a58a98d71..3800ace02fc 100644 --- a/pkg/api/dtos/user.go +++ b/pkg/api/dtos/user.go @@ -18,6 +18,7 @@ type AdminCreateUserForm struct { Login string `json:"login"` Name string `json:"name"` Password string `json:"password" binding:"Required"` + OrgId int64 `json:"orgId"` } type AdminUpdateUserForm struct { diff --git a/pkg/models/user.go b/pkg/models/user.go index 3cf9a96334e..7ecdba21137 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -58,6 +58,7 @@ type CreateUserCommand struct { Login string Name string Company string + OrgId int64 OrgName string Password string EmailVerified bool diff --git a/pkg/services/sqlstore/org.go b/pkg/services/sqlstore/org.go index 06e4350303b..4f46d10077e 100644 --- a/pkg/services/sqlstore/org.go +++ b/pkg/services/sqlstore/org.go @@ -220,6 +220,18 @@ func DeleteOrg(cmd *models.DeleteOrgCommand) error { }) } +func verifyExistingOrg(sess *DBSession, orgId int64) error { + var org models.Org + has, err := sess.Where("id=?", orgId).Get(&org) + if err != nil { + return err + } + if !has { + return models.ErrOrgNotFound + } + return nil +} + func getOrCreateOrg(sess *DBSession, orgName string) (int64, error) { var org models.Org diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index b4f78a5a6f0..a1294f041ce 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -41,6 +41,14 @@ func getOrgIdForNewUser(sess *DBSession, cmd *models.CreateUserCommand) (int64, return -1, nil } + if setting.AutoAssignOrg && cmd.OrgId != 0 { + err := verifyExistingOrg(sess, cmd.OrgId) + if err != nil { + return -1, err + } + return cmd.OrgId, nil + } + orgName := cmd.OrgName if len(orgName) == 0 { orgName = util.StringsFallback2(cmd.Email, cmd.Login) diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go index 4651ddbbb60..514fdbb6818 100644 --- a/pkg/services/sqlstore/user_test.go +++ b/pkg/services/sqlstore/user_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/setting" + . "github.com/smartystreets/goconvey/convey" "github.com/grafana/grafana/pkg/models" @@ -63,6 +65,56 @@ func TestUserDataAccess(t *testing.T) { }) }) + Convey("Given an organization", func() { + autoAssignOrg := setting.AutoAssignOrg + setting.AutoAssignOrg = true + defer func() { + setting.AutoAssignOrg = autoAssignOrg + }() + + orgCmd := &models.CreateOrgCommand{Name: "Some Test Org"} + err := CreateOrg(orgCmd) + So(err, ShouldBeNil) + + Convey("Creates user assigned to other organization", func() { + cmd := &models.CreateUserCommand{ + Email: "usertest@test.com", + Name: "user name", + Login: "user_test_login", + OrgId: orgCmd.Result.Id, + } + + err := CreateUser(context.Background(), cmd) + So(err, ShouldBeNil) + + Convey("Loading a user", func() { + query := models.GetUserByIdQuery{Id: cmd.Result.Id} + err := GetUserById(&query) + So(err, ShouldBeNil) + + So(query.Result.Email, ShouldEqual, "usertest@test.com") + So(query.Result.Password, ShouldEqual, "") + So(query.Result.Rands, ShouldHaveLength, 10) + So(query.Result.Salt, ShouldHaveLength, 10) + So(query.Result.IsDisabled, ShouldBeFalse) + So(query.Result.OrgId, ShouldEqual, orgCmd.Result.Id) + }) + }) + + Convey("Don't create user assigned to unknown organization", func() { + const nonExistingOrgID = 10000 + cmd := &models.CreateUserCommand{ + Email: "usertest@test.com", + Name: "user name", + Login: "user_test_login", + OrgId: nonExistingOrgID, + } + + err := CreateUser(context.Background(), cmd) + So(err, ShouldEqual, models.ErrOrgNotFound) + }) + }) + Convey("Given 5 users", func() { users := createFiveTestUsers(func(i int) *models.CreateUserCommand { return &models.CreateUserCommand{ From 2c36137457faf9260ae389b19b74515a19e7a728 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 15 Apr 2020 11:57:44 +0200 Subject: [PATCH 08/11] Fix instantiation of plugin settings (#23583) Forgot to instantiate plugin settings in #23451 --- pkg/setting/setting.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 93358ca2e27..235aa6dd02d 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -988,6 +988,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { pluginsSection := iniFile.Section("plugins") cfg.PluginsEnableAlpha = pluginsSection.Key("enable_alpha").MustBool(false) cfg.PluginsAppsSkipVerifyTLS = pluginsSection.Key("app_tls_skip_verify_insecure").MustBool(false) + cfg.PluginSettings = extractPluginSettings(iniFile.Sections()) // Read and populate feature toggles list featureTogglesSection := iniFile.Section("feature_toggles") From f997f85eb7963fdd01cc5e5ab623c8593fb6160b Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 15 Apr 2020 12:07:49 +0200 Subject: [PATCH 09/11] CI: scan master and release images oss/enterprise (#23475) --- .circleci/config.yml | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d46d5354089..3dfc9e5e4f0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -864,7 +864,7 @@ jobs: command: "./scripts/ci-job-succeeded.sh" when: on_success - scan-docker-master: + scan-docker-images: docker: - image: circleci/buildpack-deps:stretch steps: @@ -887,11 +887,29 @@ jobs: name: Clear trivy cache command: trivy --clear-cache - run: - name: Scan the latest grafana master alpine image with trivy + name: Scan grafana/grafana:master command: trivy --exit-code 1 grafana/grafana:master - run: - name: Scan the latest grafana master ubuntu image with trivy + name: Scan grafana/grafana:master-ubuntu command: trivy --exit-code 1 grafana/grafana:master-ubuntu + - run: + name: Scan grafana/grafana-enterprise:master + command: trivy --exit-code 1 grafana/grafana-enterprise:master + - run: + name: Scan grafana/grafana-enterprise:master-ubuntu + command: trivy --exit-code 1 grafana/grafana-enterprise:master-ubuntu + - run: + name: Scan grafana/grafana:latest + command: trivy --exit-code 1 grafana/grafana:latest + - run: + name: Scan grafana/grafana:latest-ubuntu + command: trivy --exit-code 1 grafana/grafana:latest-ubuntu + - run: + name: Scan grafana/grafana-enterprise:latest + command: trivy --exit-code 1 grafana/grafana-enterprise:latest + - run: + name: Scan grafana/grafana-enterprise:latest-ubuntu + command: trivy --exit-code 1 grafana/grafana-enterprise:latest-ubuntu - save_cache: key: vulnerability-db paths: @@ -1227,4 +1245,4 @@ workflows: cron: "0 0 * * *" filters: *filter-only-master jobs: - - scan-docker-master + - scan-docker-images From c796f70a3b0a78447146792e5a493903c14d8157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Toulet?= <35176601+AgnesToulet@users.noreply.github.com> Date: Wed, 15 Apr 2020 12:32:22 +0200 Subject: [PATCH 10/11] Toolkit: Update node_modules path to fix Windows error (#23582) --- packages/grafana-toolkit/bin/grafana-toolkit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-toolkit/bin/grafana-toolkit.js b/packages/grafana-toolkit/bin/grafana-toolkit.js index 2cb78b0c5a1..8bc6003d734 100755 --- a/packages/grafana-toolkit/bin/grafana-toolkit.js +++ b/packages/grafana-toolkit/bin/grafana-toolkit.js @@ -15,7 +15,7 @@ const isLinkedMode = () => { } try { - return fs.lstatSync(`${pwd}/node_modules/@grafana/toolkit`.replace('~', process.env.HOME)).isSymbolicLink(); + return fs.lstatSync(`${__dirname}/../../../node_modules/@grafana/toolkit`).isSymbolicLink(); } catch { return false; } From f48d444a145684025e199845892523ecb886e995 Mon Sep 17 00:00:00 2001 From: Marcus Olsson Date: Wed, 15 Apr 2020 15:19:12 +0200 Subject: [PATCH 11/11] Remove guides (#23589) --- docs/sources/installation/behind_proxy.md | 166 ---------------------- docs/sources/menu.yaml | 9 -- docs/sources/tutorials/_index.md | 3 - docs/sources/tutorials/hubot_howto.md | 139 ------------------ docs/sources/tutorials/iis.md | 89 ------------ 5 files changed, 406 deletions(-) delete mode 100644 docs/sources/installation/behind_proxy.md delete mode 100755 docs/sources/tutorials/hubot_howto.md delete mode 100644 docs/sources/tutorials/iis.md diff --git a/docs/sources/installation/behind_proxy.md b/docs/sources/installation/behind_proxy.md deleted file mode 100644 index 4f0a80bfe8d..00000000000 --- a/docs/sources/installation/behind_proxy.md +++ /dev/null @@ -1,166 +0,0 @@ -+++ -title = "Running Grafana behind a reverse proxy" -description = "Guide for running Grafana behind a reverse proxy" -keywords = ["grafana", "nginx", "documentation", "haproxy", "reverse"] -type = "docs" -[menu.docs] -name = "Running Grafana behind a reverse proxy" -parent = "tutorials" -weight = 1 -+++ - - -# Running Grafana behind a reverse proxy - -It should be straight forward to get Grafana up and running behind a reverse proxy. But here are some things that you might run into. - -Links and redirects will not be rendered correctly unless you set the server.domain setting. -```bash -[server] -domain = foo.bar -``` - -To use sub *path* ex `http://foo.bar/grafana` make sure to include `/grafana` in the end of root_url. -Otherwise Grafana will not behave correctly. See example below. - -## Examples -Here are some example configurations for running Grafana behind a reverse proxy. - -### Grafana configuration (ex http://foo.bar) - -```bash -[server] -domain = foo.bar -``` - -### Nginx configuration - -Nginx is a high performance load balancer, web server and reverse proxy: https://www.nginx.com/ - -#### Nginx configuration with HTTP and Reverse Proxy enabled -```nginx -server { - listen 80; - root /usr/share/nginx/html; - index index.html index.htm; - - location / { - proxy_pass http://localhost:3000/; - } -} -``` - -### Grafana configuration with hosting HTTPS in Nginx (ex https://foo.bar) - -```bash -[server] -domain = foo.bar -root_url = https://foo.bar -``` - -#### Nginx configuration with HTTPS, Reverse Proxy, HTTP to HTTPS redirect and URL re-writes enabled - -Instead of http://foo.bar:3000/?orgId=1, this configuration will redirect all HTTP requests to HTTPS and re-write the URL so that port 3000 isn't visible and will result in https://foo.bar/?orgId=1 - -```nginx -server { - listen 80; - server_name foo.bar; - return 301 https://foo.bar$request_uri; -} - -server { - listen 443 ssl http2; - server_name foo.bar; - root /usr/share/nginx/html; - index index.html index.htm; - ssl_certificate /etc/nginx/certs/foo_bar.crt; - ssl_certificate_key /etc/nginx/certs/foo_bar_decrypted.key; - ssl_protocols TLSv1.2; - ssl_ciphers HIGH:!aNULL:!MD5; - - location / { - rewrite /(.*) /$1 break; - proxy_pass http://localhost:3000/; - proxy_redirect off; - proxy_set_header Host $host; - } -} -``` - -### Examples with **sub path** (ex http://foo.bar/grafana) - -#### Grafana configuration with sub path -```bash -[server] -domain = foo.bar -root_url = %(protocol)s://%(domain)s/grafana/ -``` - -#### Nginx configuration with sub path -```nginx -server { - listen 80; - root /usr/share/nginx/www; - index index.html index.htm; - - location /grafana/ { - proxy_pass http://localhost:3000/; - } -} -``` - -#### HAProxy configuration with sub path -```bash -frontend http-in - bind *:80 - use_backend grafana_backend if { path /grafana } or { path_beg /grafana/ } - -backend grafana_backend - # Requires haproxy >= 1.6 - http-request set-path %[path,regsub(^/grafana/?,/)] - - # Works for haproxy < 1.6 - # reqrep ^([^\ ]*\ /)grafana[/]?(.*) \1\2 - - server grafana localhost:3000 -``` - -### IIS URL Rewrite Rule (Windows) with Subpath - -IIS requires that the URL Rewrite module is installed. - -Given: - -- subpath `grafana` -- Grafana installed on `http://localhost:3000` -- server config: - - ```bash - [server] - domain = localhost:8080 - root_url = %(protocol)s://%(domain)s/grafana/ - ``` - -Create an Inbound Rule for the parent website (localhost:8080 in this example) in IIS Manager with the following settings: - -- pattern: `grafana(/)?(.*)` -- check the `Ignore case` checkbox -- rewrite URL set to `http://localhost:3000/{R:2}` -- check the `Append query string` checkbox -- check the `Stop processing of subsequent rules` checkbox - -This is the rewrite rule that is generated in the `web.config`: - -```xml - - - - - - - - -``` - -See the [tutorial on IIS URL Rewrites](http://docs.grafana.org/tutorials/iis/) for more in-depth instructions. diff --git a/docs/sources/menu.yaml b/docs/sources/menu.yaml index 1dd52ace031..ee83f2bd085 100644 --- a/docs/sources/menu.yaml +++ b/docs/sources/menu.yaml @@ -267,15 +267,6 @@ link: /enterprise/license-expiration/ - name: Export dashboard as PDF link: /enterprise/export-pdf/ -- name: Guides - link: /tutorials/ - children: - - name: Run Grafana behind a reverse proxy - link: /installation/behind_proxy/ - - name: Run Grafana with IIS Reverse Proxy on Windows - link: /tutorials/iis/ - - name: Integrate Hubot and Grafana - link: /tutorials/hubot_howto/ - name: Plugins link: /plugins/ children: diff --git a/docs/sources/tutorials/_index.md b/docs/sources/tutorials/_index.md index 66ff8c14531..8911fc4e15d 100755 --- a/docs/sources/tutorials/_index.md +++ b/docs/sources/tutorials/_index.md @@ -12,10 +12,7 @@ This section of the docs contains a series for tutorials and stack setup guides. ## Articles -- [Running Grafana behind a reverse proxy]({{< relref "../installation/behind_proxy.md" >}}) - [API Tutorial: How To Create API Tokens And Dashboards For A Specific Organization]({{< relref "api_org_token_howto.md" >}}) -- [How to Use IIS with URL Rewrite as a Reverse Proxy for Grafana on Windows]({{< relref "iis.md" >}}) -- [How to integrate Hubot with Grafana]({{< relref "hubot_howto.md" >}}) - [How to setup Grafana for high availability]({{< relref "ha_setup.md" >}}) ## External links diff --git a/docs/sources/tutorials/hubot_howto.md b/docs/sources/tutorials/hubot_howto.md deleted file mode 100755 index 162f9748f5b..00000000000 --- a/docs/sources/tutorials/hubot_howto.md +++ /dev/null @@ -1,139 +0,0 @@ -+++ -title = "How to integrate Hubot and Grafana" -type = "docs" -keywords = ["grafana", "tutorials", "hubot", "slack", "hipchat", "setup", "install", "config"] -[menu.docs] -parent = "tutorials" -weight = 10 -+++ - -# How to integrate Hubot with Grafana - -Grafana 2.0 shipped with a great feature that enables it to render any graph or panel to a PNG image. -No matter what data source you are using, the PNG image of the Graph will look the same -as it does in your browser. - -This guide will show you how to install and configure the [Hubot-Grafana](https://github.com/stephenyeargin/hubot-grafana) -plugin. This plugin allows you to tell hubot to render any dashboard or graph right from a channel in -Slack, Hipchat or Basecamp. The bot will respond with an image of the graph and a link that will -take you to the graph. - -> *Amazon S3 Required*: The hubot-grafana script will upload the rendered graphs to Amazon S3. This -> is so Hipchat and Slack can show them reliably (they require the image to be publicly available). - -
- -
- -## What is Hubot? - -[Hubot](https://hubot.github.com/) is an universal and extensible chat bot that can be used with many chat -services and has a huge library of third party plugins that allow you to automate anything from your -chat rooms. - -## Install Hubot - -Hubot is very easy to install and host. If you do not already have a bot up and running please -read the official [Getting Started With Hubot](https://hubot.github.com/docs/) guide. - -## Install Hubot-Grafana script - -In your Hubot project repo install the Grafana plugin using `npm`: -```bash -npm install hubot-grafana --save -``` -Edit the file external-scripts.json, and add hubot-grafana to the list of plugins. - -```json -[ -"hubot-pugme", -"hubot-shipit", -"hubot-grafana" -] -``` - -## Configure - -The `hubot-grafana` plugin requires a number of environment variables to be set in order to work properly. - -```bash -export HUBOT_GRAFANA_HOST=https://play.grafana.org -export HUBOT_GRAFANA_API_KEY=abcd01234deadbeef01234 -export HUBOT_GRAFANA_S3_BUCKET=mybucket -export HUBOT_GRAFANA_S3_ACCESS_KEY_ID=ABCDEF123456XYZ -export HUBOT_GRAFANA_S3_SECRET_ACCESS_KEY=aBcD01234dEaDbEef01234 -export HUBOT_GRAFANA_S3_PREFIX=graphs -export HUBOT_GRAFANA_S3_REGION=us-standard -``` - -### Grafana server side rendering - -The hubot plugin will take advantage of the Grafana server side rendering feature that can -render any panel on the server using phantomjs. Grafana ships with a phantomjs binary (Linux only). - -To verify that this feature works try the `Direct link to rendered image` link in the panel share dialog. -If you do not get an image when opening this link verify that the required font packages are installed for phantomjs to work. - -### Grafana API Key - -{{< docs-imagebox img="/img/docs/v2/orgdropdown_api_keys.png" max-width="150px" class="docs-image--right">}} - -You need to set the environment variable `HUBOT_GRAFANA_API_KEY` to a Grafana API Key. -You can add these from the API Keys page which you find in the Organization dropdown. - -### Amazon S3 - -The `S3` options are optional but for the images to work properly in services like Slack and Hipchat they need -to publicly available. By specifying the `S3` options the hubot-grafana script will publish the rendered -panel to `S3` and it will use that URL when it posts to Slack or Hipchat. - -## Hubot commands - -- `hubot graf list` - - Lists the available dashboards -- `hubot graf db graphite-carbon-metrics` - - Graph all panels in the dashboard -- `hubot graf db graphite-carbon-metrics:3` - - Graph only panel with id 3 of a particular dashboard -- `hubot graf db graphite-carbon-metrics:cpu` - - Graph only the panels containing "cpu" (case insensitive) in the title -- `hubot graf db graphite-carbon-metrics now-12hr` - - Get a dashboard with a window of 12 hours ago to now -- `hubot graf db graphite-carbon-metrics now-24hr now-12hr` - - Get a dashboard with a window of 24 hours ago to 12 hours ago -- `hubot graf db graphite-carbon-metrics:3 now-8d now-1d` - - Get only the third panel of a particular dashboard with a window of 8 days ago to yesterday -- `hubot graf db graphite-carbon-metrics host=carbon-a` - - Get a templated dashboard with the `$host` parameter set to `carbon-a` - -## Aliases - -Some of the hubot commands above can lengthy and you might have to remember the dashboard slug (url id). -If you have a few favorite graphs you want to be able check up on often (let's say from your mobile) you -can create hubot command aliases with the hubot script `hubot-alias`. - -Install it: - -```bash -npm i --save hubot-alias -``` - -Now add `hubot-alias` to the list of plugins in `external-scripts.json` and restart hubot. - -Now you can add an alias like this: - -- `hubot alias graf-lb=graf db loadbalancers:2 now-20m` - -
- Using the alias:
- -
- -## Summary - -Grafana is going to ship with integrated Slack and Hipchat features some day but you do -not have to wait for that. Grafana 2 shipped with a very clever server side rendering feature -that can render any panel to a png using phantomjs. The hubot plugin for Grafana is something -you can install and use today! - - diff --git a/docs/sources/tutorials/iis.md b/docs/sources/tutorials/iis.md deleted file mode 100644 index 6a2b6f7368b..00000000000 --- a/docs/sources/tutorials/iis.md +++ /dev/null @@ -1,89 +0,0 @@ -+++ -title = "Grafana with IIS Reverse Proxy on Windows" -type = "docs" -keywords = ["grafana", "tutorials", "proxy", "IIS", "windows"] -[menu.docs] -parent = "tutorials" -weight = 10 -+++ - -# How to Use IIS with URL Rewrite as a Reverse Proxy for Grafana on Windows - -If you want Grafana to be a subpath or subfolder under a website in IIS then the URL Rewrite module for ISS can be used to support this. - -Example: - -- Parent site: http://localhost:8080 -- Grafana: http://localhost:3000 - -Grafana as a subpath: http://localhost:8080/grafana - -## Setup - -If you have not already done it, then a requirement is to install URL Rewrite module for IIS. - -Download and install the URL Rewrite module for IIS: https://www.iis.net/downloads/microsoft/url-rewrite - -## Grafana Config - -The Grafana config can be set by creating a file named `custom.ini` in the `conf` subdirectory of your Grafana installation. See the [installation instructions](http://docs.grafana.org/installation/windows/#configure) for more details. - -Given that the subpath should be `grafana` and the parent site is `localhost:8080` then add this to the `custom.ini` config file: - - ```bash -[server] -domain = localhost:8080 -root_url = %(protocol)s://%(domain)s/grafana/ -``` - -Restart the Grafana server after changing the config file. - -## IIS Config - -1. Open the IIS Manager and click on the parent website -2. In the admin console for this website, double click on the URL Rewrite option: - {{< docs-imagebox img="/img/docs/tutorials/IIS_admin_console.png" max-width= "800px" >}} - -3. Click on the `Add Rule(s)...` action -4. Choose the Blank Rule template for an Inbound Rule - {{< docs-imagebox img="/img/docs/tutorials/IIS_add_inbound_rule.png" max-width= "800px" >}} - -5. Create an Inbound Rule for the parent website (localhost:8080 in this example) with the following settings: - - pattern: `grafana(/)?(.*)` - - check the `Ignore case` checkbox - - rewrite URL set to `http://localhost:3000/{R:2}` - - check the `Append query string` checkbox - - check the `Stop processing of subsequent rules` checkbox - - {{< docs-imagebox img="/img/docs/tutorials/IIS_url_rewrite.png" max-width= "800px" >}} - -Finally, navigate to `http://localhost:8080/grafana` (replace `http://localhost:8080` with your parent domain) and you should come to the Grafana login page. - -## Troubleshooting - -### 404 error - -When navigating to the Grafana URL (`http://localhost:8080/grafana` in the example above) and a `HTTP Error 404.0 - Not Found` error is returned then either: - -- the pattern for the Inbound Rule is incorrect. Edit the rule, click on the `Test pattern...` button, test the part of the URL after `http://localhost:8080/` and make sure it matches. For `grafana/login` the test should return 3 capture groups: {R:0}: `grafana` {R:1}: `/` and {R:2}: `login`. -- The `root_url` setting in the Grafana config file does not match the parent URL with subpath. - -### Grafana Website only shows text with no images or css - -{{< docs-imagebox img="/img/docs/tutorials/IIS_proxy_error.png" max-width= "800px" >}} - -1. The `root_url` setting in the Grafana config file does not match the parent URL with subpath. This could happen if the root_url is commented out by mistake (`;` is used for commenting out a line in .ini files): - - `; root_url = %(protocol)s://%(domain)s/grafana/` - -2. or if the subpath in the `root_url` setting does not match the subpath used in the pattern in the Inbound Rule in IIS: - - `root_url = %(protocol)s://%(domain)s/grafana/` - - pattern in Inbound Rule: `wrongsubpath(/)?(.*)` - -3. or if the Rewrite URL in the Inbound Rule is incorrect. - - The Rewrite URL should not include the subpath. - - The Rewrite URL should contain the capture group from the pattern matching that returns the part of the URL after the subpath. The pattern used above returns 3 capture groups and the third one {R:2} returns the part of the URL after `http://localhost:8080/grafana/`.