diff --git a/conf/defaults.ini b/conf/defaults.ini index afa742fc4fc9d..c550e5bfdee33 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -365,6 +365,7 @@ allow_sign_up = true # LDAP backround sync (Enterprise only) sync_cron = @hourly +active_sync_enabled = false #################################### SMTP / Emailing ##################### [smtp] diff --git a/devenv/dev-dashboards/datasource-influxdb/influxdb-templated.json b/devenv/dev-dashboards/datasource-influxdb/influxdb-templated.json new file mode 100644 index 0000000000000..8a73979bb935b --- /dev/null +++ b/devenv/dev-dashboards/datasource-influxdb/influxdb-templated.json @@ -0,0 +1,323 @@ +{ + "annotations": { + "enable": false, + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "iteration": 1556259111212, + "links": [], + "panels": [ + { + "aliasColors": {}, + "annotate": { + "enable": false + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-influxdb", + "editable": true, + "error": false, + "fill": 2, + "grid": {}, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "interval": "$summarize", + "legend": { + "alignAsTable": true, + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "resolution": 100, + "scale": 1, + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "$tag_hostname", + "dsType": "influxdb", + "groupBy": [ + { + "params": ["auto"], + "type": "time" + }, + { + "params": ["hostname"], + "type": "tag" + } + ], + "measurement": "logins.count", + "policy": "default", + "query": "SELECT mean(\"value\") FROM \"logins.count\" WHERE \"hostname\" =~ /$Hostname$/ AND $timeFilter GROUP BY time($interval), \"hostname\"", + "refId": "A", + "resultFormat": "time_series", + "select": [ + [ + { + "params": ["value"], + "type": "field" + }, + { + "params": [], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "datacenter", + "operator": "=~", + "value": "/^$datacenter$/" + }, + { + "condition": "AND", + "key": "hostname", + "operator": "=~", + "value": "/^$host$/" + } + ], + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Selected Servers", + "tooltip": { + "msResolution": false, + "query_as_alias": true, + "shared": false, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + }, + "zerofill": true + } + ], + "refresh": false, + "schemaVersion": 18, + "style": "dark", + "tags": ["gdev", "datasource-test", "influxdb"], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "text": "America", + "value": "America" + }, + "datasource": "gdev-influxdb", + "definition": "", + "hide": 0, + "includeAll": false, + "label": null, + "multi": false, + "name": "datacenter", + "options": [], + "query": "SHOW TAG VALUES WITH KEY = \"datacenter\" ", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": null, + "tags": [], + "tagsQuery": null, + "type": "query", + "useTags": false + }, + { + "allFormat": "regex values", + "allValue": null, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": "gdev-influxdb", + "definition": "", + "hide": 0, + "includeAll": true, + "label": null, + "multi": true, + "multiFormat": "regex values", + "name": "host", + "options": [], + "query": "SHOW TAG VALUES WITH KEY = \"hostname\" WHERE \"datacenter\" =~ /^$datacenter$/", + "refresh": 1, + "refresh_on_load": false, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": null, + "tags": [], + "tagsQuery": null, + "type": "query", + "useTags": false + }, + { + "allFormat": "glob", + "auto": true, + "auto_count": 5, + "auto_min": "10s", + "current": { + "text": "1m", + "value": "1m" + }, + "datasource": null, + "hide": 0, + "includeAll": false, + "label": "", + "name": "summarize", + "options": [ + { + "selected": false, + "text": "auto", + "value": "$__auto_interval_summarize" + }, + { + "selected": true, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "10m", + "value": "10m" + }, + { + "selected": false, + "text": "30m", + "value": "30m" + }, + { + "selected": false, + "text": "1h", + "value": "1h" + }, + { + "selected": false, + "text": "6h", + "value": "6h" + }, + { + "selected": false, + "text": "12h", + "value": "12h" + }, + { + "selected": false, + "text": "1d", + "value": "1d" + }, + { + "selected": false, + "text": "7d", + "value": "7d" + }, + { + "selected": false, + "text": "14d", + "value": "14d" + }, + { + "selected": false, + "text": "30d", + "value": "30d" + } + ], + "query": "1m,10m,30m,1h,6h,12h,1d,7d,14d,30d", + "refresh": 2, + "refresh_on_load": false, + "skipUrlSync": false, + "type": "interval" + }, + { + "datasource": "InfluxDB", + "filters": [], + "hide": 0, + "label": null, + "name": "adhoc", + "skipUrlSync": false, + "type": "adhoc" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "collapse": false, + "enable": true, + "notice": false, + "now": true, + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "status": "Stable", + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"], + "type": "timepicker" + }, + "timezone": "browser", + "title": "Datasource tests - InfluxDB Templated", + "uid": "000000002", + "version": 2 +} diff --git a/devenv/dev-dashboards/home.json b/devenv/dev-dashboards/home.json index 94751adf71faf..8608b0b82647f 100644 --- a/devenv/dev-dashboards/home.json +++ b/devenv/dev-dashboards/home.json @@ -25,12 +25,12 @@ "x": 0, "y": 0 }, - "headings": false, + "headings": true, "id": 7, "limit": 100, "links": [], "query": "", - "recent": false, + "recent": true, "search": false, "starred": true, "tags": [], @@ -167,5 +167,5 @@ "timezone": "", "title": "Grafana Dev Overview & Home", "uid": "j6T00KRZz", - "version": 5 + "version": 1 } diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 5b05daec8d966..6d58d200c9802 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -571,7 +571,7 @@ basic auth password Path to JSON key file associated with a Google service account to authenticate and authorize. Service Account keys can be created and downloaded from https://console.developers.google.com/permissions/serviceaccounts. -Service Account should have "Storage Object Writer" role. +Service Account should have "Storage Object Writer" role. The access control model of the bucket needs to be "Set object-level and bucket-level permissions". Grafana itself will make the images public readable. ### bucket name Bucket Name on Google Cloud Storage. diff --git a/go.mod b/go.mod index bebf6790dc823..fc72780af2f49 100644 --- a/go.mod +++ b/go.mod @@ -48,11 +48,12 @@ require ( github.com/onsi/gomega v1.5.0 // indirect github.com/opentracing/opentracing-go v1.1.0 github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/pkg/errors v0.8.1 // indirect + github.com/pkg/errors v0.8.1 github.com/prometheus/client_golang v0.9.2 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 github.com/prometheus/common v0.2.0 github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect + github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 github.com/sergi/go-diff v1.0.0 // indirect github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 // indirect github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a diff --git a/go.sum b/go.sum index 9d2ab18356330..55223ecbc74e8 100644 --- a/go.sum +++ b/go.sum @@ -167,6 +167,8 @@ github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nL github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q= +github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 h1:x7xEyJDP7Hv3LVgvWhzioQqbC/KtuUhTigKlH/8ehhE= +github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= diff --git a/jest.config.js b/jest.config.js index c5c6bcb9f5f1c..09342e1472079 100644 --- a/jest.config.js +++ b/jest.config.js @@ -23,4 +23,5 @@ module.exports = { "./public/test/jest-setup.ts" ], "snapshotSerializers": ["enzyme-to-json/serializer"], + "globals": { "ts-jest": { "isolatedModules": true } }, }; diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 41ed426f77c3f..1ae257b6016cd 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -23,7 +23,7 @@ "@types/react-color": "2.17.0", "classnames": "2.2.6", "d3": "5.9.1", - "jquery": "3.3.1", + "jquery": "3.4.0", "lodash": "4.17.11", "moment": "2.24.0", "papaparse": "4.6.3", diff --git a/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx b/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx index 394b898e845cf..b53355e01783f 100644 --- a/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx +++ b/packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx @@ -55,7 +55,7 @@ export class RefreshPicker extends PureComponent { const cssClasses = classNames({ 'refresh-picker': true, - 'refresh-picker--refreshing': selectedValue.label !== offOption.label, + 'refresh-picker--off': selectedValue.label === offOption.label, }); return ( diff --git a/packages/grafana-ui/src/components/RefreshPicker/_RefreshPicker.scss b/packages/grafana-ui/src/components/RefreshPicker/_RefreshPicker.scss index 96367cc8611a5..bfa67d87582c9 100644 --- a/packages/grafana-ui/src/components/RefreshPicker/_RefreshPicker.scss +++ b/packages/grafana-ui/src/components/RefreshPicker/_RefreshPicker.scss @@ -6,6 +6,10 @@ display: flex; } + .navbar-button--refresh { + border-right: 0; + } + .gf-form-input--form-dropdown { position: static; } @@ -16,13 +20,17 @@ width: 100%; } - &--refreshing { + .select-button-value { + color: $orange; + } + + &--off { .select-button-value { - color: $orange; + display: none; } } - @include media-breakpoint-up(md) { + @include media-breakpoint-up(sm) { display: block; } } diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss index 0f2bb6669ca49..4662be0322f3d 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss +++ b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss @@ -31,6 +31,7 @@ display: flex; align-items: center; justify-content: center; + flex-shrink: 0; cursor: pointer; &:hover { @@ -40,6 +41,7 @@ .thresholds-row-color-indicator { width: 10px; + flex-shrink: 0; } .thresholds-row-input { diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap b/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap index 09596e066dd6f..faa59ddb04603 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap +++ b/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap @@ -109,7 +109,7 @@ exports[`Render should render with base threshold 1`] = ` }, "breakpoints": Object { "lg": "992px", - "md": "768px", + "md": "769px", "sm": "544px", "xl": "1200px", "xs": "0", @@ -272,7 +272,7 @@ exports[`Render should render with base threshold 1`] = ` }, "breakpoints": Object { "lg": "992px", - "md": "768px", + "md": "769px", "sm": "544px", "xl": "1200px", "xs": "0", diff --git a/packages/grafana-ui/src/themes/default.ts b/packages/grafana-ui/src/themes/default.ts index dadaa2769efbd..4a0e42312b7bc 100644 --- a/packages/grafana-ui/src/themes/default.ts +++ b/packages/grafana-ui/src/themes/default.ts @@ -42,7 +42,7 @@ const theme: GrafanaThemeCommons = { breakpoints: { xs: '0', sm: '544px', - md: '768px', + md: '769px', // 1 more than regular ipad in portrait lg: '992px', xl: '1200px', }, diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index c06ea7acd42bb..5354df29d1de1 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -53,7 +53,7 @@ export type PanelTypeChangedHandler = ( prevOptions: any ) => Partial; -export class ReactPanelPlugin { +export class PanelPlugin { panel: ComponentType>; editor?: ComponentClass>; defaults?: TOptions; diff --git a/pkg/extensions/main.go b/pkg/extensions/main.go index 057974c3fec93..6ee742a4d8e34 100644 --- a/pkg/extensions/main.go +++ b/pkg/extensions/main.go @@ -2,6 +2,7 @@ package extensions import ( _ "github.com/gobwas/glob" + _ "github.com/robfig/cron" _ "gopkg.in/square/go-jose.v2" ) diff --git a/pkg/login/auth.go b/pkg/login/auth.go index 301927b6ad36e..56f614d92deac 100644 --- a/pkg/login/auth.go +++ b/pkg/login/auth.go @@ -5,10 +5,12 @@ import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + LDAP "github.com/grafana/grafana/pkg/services/ldap" ) var ( ErrEmailNotAllowed = errors.New("Required email domain not fulfilled") + ErrNoLDAPServers = errors.New("No LDAP servers are configured") ErrInvalidCredentials = errors.New("Invalid Username or Password") ErrNoEmail = errors.New("Login provider didn't return an email address") ErrProviderDeniedRequest = errors.New("Login provider denied login request") @@ -21,7 +23,6 @@ var ( func Init() { bus.AddHandler("auth", AuthenticateUser) - loadLdapConfig() } func AuthenticateUser(query *m.LoginUserQuery) error { @@ -40,14 +41,14 @@ func AuthenticateUser(query *m.LoginUserQuery) error { ldapEnabled, ldapErr := loginUsingLdap(query) if ldapEnabled { - if ldapErr == nil || ldapErr != ErrInvalidCredentials { + if ldapErr == nil || ldapErr != LDAP.ErrInvalidCredentials { return ldapErr } err = ldapErr } - if err == ErrInvalidCredentials { + if err == ErrInvalidCredentials || err == LDAP.ErrInvalidCredentials { saveInvalidLoginAttempt(query) } diff --git a/pkg/login/auth_test.go b/pkg/login/auth_test.go index a4cd8284cdd14..85ad3bc07dce7 100644 --- a/pkg/login/auth_test.go +++ b/pkg/login/auth_test.go @@ -4,8 +4,10 @@ import ( "errors" "testing" - m "github.com/grafana/grafana/pkg/models" . "github.com/smartystreets/goconvey/convey" + + m "github.com/grafana/grafana/pkg/models" + LDAP "github.com/grafana/grafana/pkg/services/ldap" ) func TestAuthenticateUser(t *testing.T) { @@ -100,13 +102,13 @@ func TestAuthenticateUser(t *testing.T) { authScenario("When a non-existing grafana user authenticate and invalid ldap credentials", func(sc *authScenarioContext) { mockLoginAttemptValidation(nil, sc) mockLoginUsingGrafanaDB(m.ErrUserNotFound, sc) - mockLoginUsingLdap(true, ErrInvalidCredentials, sc) + mockLoginUsingLdap(true, LDAP.ErrInvalidCredentials, sc) mockSaveInvalidLoginAttempt(sc) err := AuthenticateUser(sc.loginUserQuery) Convey("it should result in", func() { - So(err, ShouldEqual, ErrInvalidCredentials) + So(err, ShouldEqual, LDAP.ErrInvalidCredentials) So(sc.loginAttemptValidationWasCalled, ShouldBeTrue) So(sc.grafanaLoginWasCalled, ShouldBeTrue) So(sc.ldapLoginWasCalled, ShouldBeTrue) @@ -152,13 +154,13 @@ func TestAuthenticateUser(t *testing.T) { authScenario("When grafana user authenticate with invalid credentials and invalid ldap credentials", func(sc *authScenarioContext) { mockLoginAttemptValidation(nil, sc) mockLoginUsingGrafanaDB(ErrInvalidCredentials, sc) - mockLoginUsingLdap(true, ErrInvalidCredentials, sc) + mockLoginUsingLdap(true, LDAP.ErrInvalidCredentials, sc) mockSaveInvalidLoginAttempt(sc) err := AuthenticateUser(sc.loginUserQuery) Convey("it should result in", func() { - So(err, ShouldEqual, ErrInvalidCredentials) + So(err, ShouldEqual, LDAP.ErrInvalidCredentials) So(sc.loginAttemptValidationWasCalled, ShouldBeTrue) So(sc.grafanaLoginWasCalled, ShouldBeTrue) So(sc.ldapLoginWasCalled, ShouldBeTrue) diff --git a/pkg/login/grafana_login_test.go b/pkg/login/grafana_login_test.go index 90422678fd235..2c189ae007297 100644 --- a/pkg/login/grafana_login_test.go +++ b/pkg/login/grafana_login_test.go @@ -3,9 +3,10 @@ package login import ( "testing" + . "github.com/smartystreets/goconvey/convey" + "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" - . "github.com/smartystreets/goconvey/convey" ) func TestGrafanaLogin(t *testing.T) { diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go deleted file mode 100644 index 24ab6fdc0f8f6..0000000000000 --- a/pkg/login/ldap.go +++ /dev/null @@ -1,430 +0,0 @@ -package login - -import ( - "crypto/tls" - "crypto/x509" - "errors" - "fmt" - "io/ioutil" - "strings" - - "github.com/davecgh/go-spew/spew" - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/log" - m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/setting" - "gopkg.in/ldap.v3" -) - -type ILdapConn interface { - Bind(username, password string) error - UnauthenticatedBind(username string) error - Search(*ldap.SearchRequest) (*ldap.SearchResult, error) - StartTLS(*tls.Config) error - Close() -} - -type ILdapAuther interface { - Login(query *m.LoginUserQuery) error - SyncUser(query *m.LoginUserQuery) error - GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error) -} - -type ldapAuther struct { - server *LdapServerConf - conn ILdapConn - requireSecondBind bool - log log.Logger -} - -var NewLdapAuthenticator = func(server *LdapServerConf) ILdapAuther { - return &ldapAuther{server: server, log: log.New("ldap")} -} - -var ldapDial = func(network, addr string) (ILdapConn, error) { - return ldap.Dial(network, addr) -} - -func (a *ldapAuther) Dial() error { - var err error - var certPool *x509.CertPool - if a.server.RootCACert != "" { - certPool = x509.NewCertPool() - for _, caCertFile := range strings.Split(a.server.RootCACert, " ") { - pem, err := ioutil.ReadFile(caCertFile) - if err != nil { - return err - } - if !certPool.AppendCertsFromPEM(pem) { - return errors.New("Failed to append CA certificate " + caCertFile) - } - } - } - var clientCert tls.Certificate - if a.server.ClientCert != "" && a.server.ClientKey != "" { - clientCert, err = tls.LoadX509KeyPair(a.server.ClientCert, a.server.ClientKey) - if err != nil { - return err - } - } - for _, host := range strings.Split(a.server.Host, " ") { - address := fmt.Sprintf("%s:%d", host, a.server.Port) - if a.server.UseSSL { - tlsCfg := &tls.Config{ - InsecureSkipVerify: a.server.SkipVerifySSL, - ServerName: host, - RootCAs: certPool, - } - if len(clientCert.Certificate) > 0 { - tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert) - } - if a.server.StartTLS { - a.conn, err = ldap.Dial("tcp", address) - if err == nil { - if err = a.conn.StartTLS(tlsCfg); err == nil { - return nil - } - } - } else { - a.conn, err = ldap.DialTLS("tcp", address, tlsCfg) - } - } else { - a.conn, err = ldapDial("tcp", address) - } - - if err == nil { - return nil - } - } - return err -} - -func (a *ldapAuther) Login(query *m.LoginUserQuery) error { - // connect to ldap server - if err := a.Dial(); err != nil { - return err - } - defer a.conn.Close() - - // perform initial authentication - if err := a.initialBind(query.Username, query.Password); err != nil { - return err - } - - // find user entry & attributes - ldapUser, err := a.searchForUser(query.Username) - if err != nil { - return err - } - - a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser)) - - // check if a second user bind is needed - if a.requireSecondBind { - err = a.secondBind(ldapUser, query.Password) - if err != nil { - return err - } - } - - grafanaUser, err := a.GetGrafanaUserFor(query.ReqContext, ldapUser) - if err != nil { - return err - } - - query.User = grafanaUser - return nil -} - -func (a *ldapAuther) SyncUser(query *m.LoginUserQuery) error { - // connect to ldap server - err := a.Dial() - if err != nil { - return err - } - defer a.conn.Close() - - err = a.serverBind() - if err != nil { - return err - } - - // find user entry & attributes - ldapUser, err := a.searchForUser(query.Username) - if err != nil { - a.log.Error("Failed searching for user in ldap", "error", err) - return err - } - - a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser)) - - grafanaUser, err := a.GetGrafanaUserFor(query.ReqContext, ldapUser) - if err != nil { - return err - } - - query.User = grafanaUser - return nil -} - -func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error) { - extUser := &m.ExternalUserInfo{ - AuthModule: "ldap", - AuthId: ldapUser.DN, - Name: fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName), - Login: ldapUser.Username, - Email: ldapUser.Email, - Groups: ldapUser.MemberOf, - OrgRoles: map[int64]m.RoleType{}, - } - - for _, group := range a.server.LdapGroups { - // only use the first match for each org - if extUser.OrgRoles[group.OrgId] != "" { - continue - } - - if ldapUser.isMemberOf(group.GroupDN) { - extUser.OrgRoles[group.OrgId] = group.OrgRole - if extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin { - extUser.IsGrafanaAdmin = group.IsGrafanaAdmin - } - } - } - - // validate that the user has access - // if there are no ldap group mappings access is true - // otherwise a single group must match - if len(a.server.LdapGroups) > 0 && len(extUser.OrgRoles) < 1 { - a.log.Info( - "Ldap Auth: user does not belong in any of the specified ldap groups", - "username", ldapUser.Username, - "groups", ldapUser.MemberOf) - return nil, ErrInvalidCredentials - } - - // add/update user in grafana - upsertUserCmd := &m.UpsertUserCommand{ - ReqContext: ctx, - ExternalUser: extUser, - SignupAllowed: setting.LdapAllowSignup, - } - - err := bus.Dispatch(upsertUserCmd) - if err != nil { - return nil, err - } - - return upsertUserCmd.Result, nil -} - -func (a *ldapAuther) serverBind() error { - bindFn := func() error { - return a.conn.Bind(a.server.BindDN, a.server.BindPassword) - } - - if a.server.BindPassword == "" { - bindFn = func() error { - return a.conn.UnauthenticatedBind(a.server.BindDN) - } - } - - // bind_dn and bind_password to bind - if err := bindFn(); err != nil { - a.log.Info("LDAP initial bind failed, %v", err) - - if ldapErr, ok := err.(*ldap.Error); ok { - if ldapErr.ResultCode == 49 { - return ErrInvalidCredentials - } - } - return err - } - - return nil -} - -func (a *ldapAuther) secondBind(ldapUser *LdapUserInfo, userPassword string) error { - if err := a.conn.Bind(ldapUser.DN, userPassword); err != nil { - a.log.Info("Second bind failed", "error", err) - - if ldapErr, ok := err.(*ldap.Error); ok { - if ldapErr.ResultCode == 49 { - return ErrInvalidCredentials - } - } - return err - } - - return nil -} - -func (a *ldapAuther) initialBind(username, userPassword string) error { - if a.server.BindPassword != "" || a.server.BindDN == "" { - userPassword = a.server.BindPassword - a.requireSecondBind = true - } - - bindPath := a.server.BindDN - if strings.Contains(bindPath, "%s") { - bindPath = fmt.Sprintf(a.server.BindDN, username) - } - - bindFn := func() error { - return a.conn.Bind(bindPath, userPassword) - } - - if userPassword == "" { - bindFn = func() error { - return a.conn.UnauthenticatedBind(bindPath) - } - } - - if err := bindFn(); err != nil { - a.log.Info("Initial bind failed", "error", err) - - if ldapErr, ok := err.(*ldap.Error); ok { - if ldapErr.ResultCode == 49 { - return ErrInvalidCredentials - } - } - return err - } - - return nil -} - -func appendIfNotEmpty(slice []string, values ...string) []string { - for _, v := range values { - if v != "" { - slice = append(slice, v) - } - } - return slice -} - -func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) { - var searchResult *ldap.SearchResult - var err error - - for _, searchBase := range a.server.SearchBaseDNs { - attributes := make([]string, 0) - inputs := a.server.Attr - attributes = appendIfNotEmpty(attributes, - inputs.Username, - inputs.Surname, - inputs.Email, - inputs.Name, - inputs.MemberOf) - - searchReq := ldap.SearchRequest{ - BaseDN: searchBase, - Scope: ldap.ScopeWholeSubtree, - DerefAliases: ldap.NeverDerefAliases, - Attributes: attributes, - Filter: strings.Replace(a.server.SearchFilter, "%s", ldap.EscapeFilter(username), -1), - } - - a.log.Debug("Ldap Search For User Request", "info", spew.Sdump(searchReq)) - - searchResult, err = a.conn.Search(&searchReq) - if err != nil { - return nil, err - } - - if len(searchResult.Entries) > 0 { - break - } - } - - if len(searchResult.Entries) == 0 { - return nil, ErrInvalidCredentials - } - - if len(searchResult.Entries) > 1 { - return nil, errors.New("Ldap search matched more than one entry, please review your filter setting") - } - - var memberOf []string - if a.server.GroupSearchFilter == "" { - memberOf = getLdapAttrArray(a.server.Attr.MemberOf, searchResult) - } else { - // If we are using a POSIX LDAP schema it won't support memberOf, so we manually search the groups - var groupSearchResult *ldap.SearchResult - for _, groupSearchBase := range a.server.GroupSearchBaseDNs { - var filter_replace string - if a.server.GroupSearchFilterUserAttribute == "" { - filter_replace = getLdapAttr(a.server.Attr.Username, searchResult) - } else { - filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult) - } - - filter := strings.Replace(a.server.GroupSearchFilter, "%s", ldap.EscapeFilter(filter_replace), -1) - - a.log.Info("Searching for user's groups", "filter", filter) - - // support old way of reading settings - groupIdAttribute := a.server.Attr.MemberOf - // but prefer dn attribute if default settings are used - if groupIdAttribute == "" || groupIdAttribute == "memberOf" { - groupIdAttribute = "dn" - } - - groupSearchReq := ldap.SearchRequest{ - BaseDN: groupSearchBase, - Scope: ldap.ScopeWholeSubtree, - DerefAliases: ldap.NeverDerefAliases, - Attributes: []string{groupIdAttribute}, - Filter: filter, - } - - groupSearchResult, err = a.conn.Search(&groupSearchReq) - if err != nil { - return nil, err - } - - if len(groupSearchResult.Entries) > 0 { - for i := range groupSearchResult.Entries { - memberOf = append(memberOf, getLdapAttrN(groupIdAttribute, groupSearchResult, i)) - } - break - } - } - } - - return &LdapUserInfo{ - DN: searchResult.Entries[0].DN, - LastName: getLdapAttr(a.server.Attr.Surname, searchResult), - FirstName: getLdapAttr(a.server.Attr.Name, searchResult), - Username: getLdapAttr(a.server.Attr.Username, searchResult), - Email: getLdapAttr(a.server.Attr.Email, searchResult), - MemberOf: memberOf, - }, nil -} - -func getLdapAttrN(name string, result *ldap.SearchResult, n int) string { - if strings.ToLower(name) == "dn" { - return result.Entries[n].DN - } - for _, attr := range result.Entries[n].Attributes { - if attr.Name == name { - if len(attr.Values) > 0 { - return attr.Values[0] - } - } - } - return "" -} - -func getLdapAttr(name string, result *ldap.SearchResult) string { - return getLdapAttrN(name, result, 0) -} - -func getLdapAttrArray(name string, result *ldap.SearchResult) []string { - for _, attr := range result.Entries[0].Attributes { - if attr.Name == name { - return attr.Values - } - } - return []string{} -} diff --git a/pkg/login/ldap_login.go b/pkg/login/ldap_login.go index 5974e19d6912a..b8811158200cd 100644 --- a/pkg/login/ldap_login.go +++ b/pkg/login/ldap_login.go @@ -1,22 +1,34 @@ package login import ( - m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/models" + LDAP "github.com/grafana/grafana/pkg/services/ldap" ) -var loginUsingLdap = func(query *m.LoginUserQuery) (bool, error) { - if !setting.LdapEnabled { +var newLDAP = LDAP.New +var readLDAPConfig = LDAP.ReadConfig +var isLDAPEnabled = LDAP.IsEnabled + +var loginUsingLdap = func(query *models.LoginUserQuery) (bool, error) { + enabled := isLDAPEnabled() + + if !enabled { return false, nil } - for _, server := range LdapCfg.Servers { - author := NewLdapAuthenticator(server) - err := author.Login(query) - if err == nil || err != ErrInvalidCredentials { + config := readLDAPConfig() + if len(config.Servers) == 0 { + return true, ErrNoLDAPServers + } + + for _, server := range config.Servers { + auth := newLDAP(server) + + err := auth.Login(query) + if err == nil || err != LDAP.ErrInvalidCredentials { return true, err } } - return true, ErrInvalidCredentials + return true, LDAP.ErrInvalidCredentials } diff --git a/pkg/login/ldap_login_test.go b/pkg/login/ldap_login_test.go index 6067a06379511..c6bd30834a9ed 100644 --- a/pkg/login/ldap_login_test.go +++ b/pkg/login/ldap_login_test.go @@ -1,71 +1,41 @@ package login import ( + "errors" "testing" + . "github.com/smartystreets/goconvey/convey" + m "github.com/grafana/grafana/pkg/models" + LDAP "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/setting" - . "github.com/smartystreets/goconvey/convey" ) +var errTest = errors.New("Test error") + func TestLdapLogin(t *testing.T) { Convey("Login using ldap", t, func() { - Convey("Given ldap enabled and a server configured", func() { + Convey("Given ldap enabled and no server configured", func() { setting.LdapEnabled = true - LdapCfg.Servers = append(LdapCfg.Servers, - &LdapServerConf{ - Host: "", - }) - ldapLoginScenario("When login with invalid credentials", func(sc *ldapLoginScenarioContext) { + ldapLoginScenario("When login", func(sc *ldapLoginScenarioContext) { sc.withLoginResult(false) - enabled, err := loginUsingLdap(sc.loginUserQuery) - - Convey("it should return true", func() { - So(enabled, ShouldBeTrue) - }) - - Convey("it should return invalid credentials error", func() { - So(err, ShouldEqual, ErrInvalidCredentials) - }) - - Convey("it should call ldap login", func() { - So(sc.ldapAuthenticatorMock.loginCalled, ShouldBeTrue) - }) - }) - - ldapLoginScenario("When login with valid credentials", func(sc *ldapLoginScenarioContext) { - sc.withLoginResult(true) - enabled, err := loginUsingLdap(sc.loginUserQuery) - - Convey("it should return true", func() { - So(enabled, ShouldBeTrue) - }) + readLDAPConfig = func() *LDAP.Config { + config := &LDAP.Config{ + Servers: []*LDAP.ServerConfig{}, + } - Convey("it should not return error", func() { - So(err, ShouldBeNil) - }) + return config + } - Convey("it should call ldap login", func() { - So(sc.ldapAuthenticatorMock.loginCalled, ShouldBeTrue) - }) - }) - }) - - Convey("Given ldap enabled and no server configured", func() { - setting.LdapEnabled = true - LdapCfg.Servers = make([]*LdapServerConf, 0) - - ldapLoginScenario("When login", func(sc *ldapLoginScenarioContext) { - sc.withLoginResult(true) enabled, err := loginUsingLdap(sc.loginUserQuery) Convey("it should return true", func() { So(enabled, ShouldBeTrue) }) - Convey("it should return invalid credentials error", func() { - So(err, ShouldEqual, ErrInvalidCredentials) + Convey("it should return no LDAP servers error", func() { + So(err, ShouldEqual, ErrNoLDAPServers) }) Convey("it should not call ldap login", func() { @@ -100,51 +70,55 @@ func TestLdapLogin(t *testing.T) { }) } -func mockLdapAuthenticator(valid bool) *mockLdapAuther { - mock := &mockLdapAuther{ +func mockLdapAuthenticator(valid bool) *mockAuth { + mock := &mockAuth{ validLogin: valid, } - NewLdapAuthenticator = func(server *LdapServerConf) ILdapAuther { + newLDAP = func(server *LDAP.ServerConfig) LDAP.IAuth { return mock } return mock } -type mockLdapAuther struct { +type mockAuth struct { validLogin bool loginCalled bool } -func (a *mockLdapAuther) Login(query *m.LoginUserQuery) error { - a.loginCalled = true +func (auth *mockAuth) Login(query *m.LoginUserQuery) error { + auth.loginCalled = true - if !a.validLogin { - return ErrInvalidCredentials + if !auth.validLogin { + return errTest } return nil } -func (a *mockLdapAuther) SyncUser(query *m.LoginUserQuery) error { +func (auth *mockAuth) Users() ([]*LDAP.UserInfo, error) { + return nil, nil +} + +func (auth *mockAuth) SyncUser(query *m.LoginUserQuery) error { return nil } -func (a *mockLdapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error) { +func (auth *mockAuth) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LDAP.UserInfo) (*m.User, error) { return nil, nil } type ldapLoginScenarioContext struct { loginUserQuery *m.LoginUserQuery - ldapAuthenticatorMock *mockLdapAuther + ldapAuthenticatorMock *mockAuth } type ldapLoginScenarioFunc func(c *ldapLoginScenarioContext) func ldapLoginScenario(desc string, fn ldapLoginScenarioFunc) { Convey(desc, func() { - origNewLdapAuthenticator := NewLdapAuthenticator + mock := &mockAuth{} sc := &ldapLoginScenarioContext{ loginUserQuery: &m.LoginUserQuery{ @@ -152,11 +126,28 @@ func ldapLoginScenario(desc string, fn ldapLoginScenarioFunc) { Password: "pwd", IpAddress: "192.168.1.1:56433", }, - ldapAuthenticatorMock: &mockLdapAuther{}, + ldapAuthenticatorMock: mock, + } + + readLDAPConfig = func() *LDAP.Config { + config := &LDAP.Config{ + Servers: []*LDAP.ServerConfig{ + { + Host: "", + }, + }, + } + + return config + } + + newLDAP = func(server *LDAP.ServerConfig) LDAP.IAuth { + return mock } defer func() { - NewLdapAuthenticator = origNewLdapAuthenticator + newLDAP = LDAP.New + readLDAPConfig = LDAP.ReadConfig }() fn(sc) diff --git a/pkg/login/ldap_settings.go b/pkg/login/ldap_settings.go deleted file mode 100644 index 40791a509db9f..0000000000000 --- a/pkg/login/ldap_settings.go +++ /dev/null @@ -1,104 +0,0 @@ -package login - -import ( - "fmt" - "os" - - "github.com/BurntSushi/toml" - "github.com/grafana/grafana/pkg/log" - m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/setting" -) - -type LdapConfig struct { - Servers []*LdapServerConf `toml:"servers"` -} - -type LdapServerConf struct { - Host string `toml:"host"` - Port int `toml:"port"` - UseSSL bool `toml:"use_ssl"` - StartTLS bool `toml:"start_tls"` - SkipVerifySSL bool `toml:"ssl_skip_verify"` - RootCACert string `toml:"root_ca_cert"` - ClientCert string `toml:"client_cert"` - ClientKey string `toml:"client_key"` - BindDN string `toml:"bind_dn"` - BindPassword string `toml:"bind_password"` - Attr LdapAttributeMap `toml:"attributes"` - - SearchFilter string `toml:"search_filter"` - SearchBaseDNs []string `toml:"search_base_dns"` - - GroupSearchFilter string `toml:"group_search_filter"` - GroupSearchFilterUserAttribute string `toml:"group_search_filter_user_attribute"` - GroupSearchBaseDNs []string `toml:"group_search_base_dns"` - - LdapGroups []*LdapGroupToOrgRole `toml:"group_mappings"` -} - -type LdapAttributeMap struct { - Username string `toml:"username"` - Name string `toml:"name"` - Surname string `toml:"surname"` - Email string `toml:"email"` - MemberOf string `toml:"member_of"` -} - -type LdapGroupToOrgRole struct { - GroupDN string `toml:"group_dn"` - OrgId int64 `toml:"org_id"` - IsGrafanaAdmin *bool `toml:"grafana_admin"` // This is a pointer to know if it was set or not (for backwards compatibility) - OrgRole m.RoleType `toml:"org_role"` -} - -var LdapCfg LdapConfig -var ldapLogger log.Logger = log.New("ldap") - -func loadLdapConfig() { - if !setting.LdapEnabled { - return - } - - ldapLogger.Info("Ldap enabled, reading config file", "file", setting.LdapConfigFile) - - _, err := toml.DecodeFile(setting.LdapConfigFile, &LdapCfg) - if err != nil { - ldapLogger.Crit("Failed to load ldap config file", "error", err) - os.Exit(1) - } - - if len(LdapCfg.Servers) == 0 { - ldapLogger.Crit("ldap enabled but no ldap servers defined in config file") - os.Exit(1) - } - - // set default org id - for _, server := range LdapCfg.Servers { - assertNotEmptyCfg(server.SearchFilter, "search_filter") - assertNotEmptyCfg(server.SearchBaseDNs, "search_base_dns") - - for _, groupMap := range server.LdapGroups { - if groupMap.OrgId == 0 { - groupMap.OrgId = 1 - } - } - } -} - -func assertNotEmptyCfg(val interface{}, propName string) { - switch v := val.(type) { - case string: - if v == "" { - ldapLogger.Crit("LDAP config file is missing option", "option", propName) - os.Exit(1) - } - case []string: - if len(v) == 0 { - ldapLogger.Crit("LDAP config file is missing option", "option", propName) - os.Exit(1) - } - default: - fmt.Println("unknown") - } -} diff --git a/pkg/middleware/auth_proxy/auth_proxy.go b/pkg/middleware/auth_proxy/auth_proxy.go index 7b9ff8abba9d7..7eee806c082b5 100644 --- a/pkg/middleware/auth_proxy/auth_proxy.go +++ b/pkg/middleware/auth_proxy/auth_proxy.go @@ -10,8 +10,8 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/remotecache" - "github.com/grafana/grafana/pkg/login" - models "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/setting" ) @@ -21,6 +21,11 @@ const ( CachePrefix = "auth-proxy-sync-ttl:%s" ) +var ( + readLDAPConfig = ldap.ReadConfig + isLDAPEnabled = ldap.IsEnabled +) + // AuthProxy struct type AuthProxy struct { store *remotecache.RemoteCache @@ -28,14 +33,13 @@ type AuthProxy struct { orgID int64 header string - LDAP func(server *login.LdapServerConf) login.ILdapAuther + LDAP func(server *ldap.ServerConfig) ldap.IAuth enabled bool whitelistIP string headerType string headers map[string]string cacheTTL int - ldapEnabled bool } // Error auth proxy specific error @@ -74,14 +78,13 @@ func New(options *Options) *AuthProxy { orgID: options.OrgID, header: header, - LDAP: login.NewLdapAuthenticator, + LDAP: ldap.New, enabled: setting.AuthProxyEnabled, headerType: setting.AuthProxyHeaderProperty, headers: setting.AuthProxyHeaders, whitelistIP: setting.AuthProxyWhitelist, cacheTTL: setting.AuthProxyLdapSyncTtl, - ldapEnabled: setting.LdapEnabled, } } @@ -167,11 +170,14 @@ func (auth *AuthProxy) GetUserID() (int64, *Error) { return id, nil } - if auth.ldapEnabled { + if isLDAPEnabled() { id, err := auth.GetUserIDViaLDAP() - if err == login.ErrInvalidCredentials { - return 0, newError("Proxy authentication required", login.ErrInvalidCredentials) + if err == ldap.ErrInvalidCredentials { + return 0, newError( + "Proxy authentication required", + ldap.ErrInvalidCredentials, + ) } if err != nil { @@ -183,7 +189,10 @@ func (auth *AuthProxy) GetUserID() (int64, *Error) { id, err := auth.GetUserIDViaHeader() if err != nil { - return 0, newError("Failed to login as user specified in auth proxy header", err) + return 0, newError( + "Failed to login as user specified in auth proxy header", + err, + ) } return id, nil @@ -210,12 +219,12 @@ func (auth *AuthProxy) GetUserIDViaLDAP() (int64, *Error) { Username: auth.header, } - ldapCfg := login.LdapCfg - if len(ldapCfg.Servers) < 1 { + config := readLDAPConfig() + if len(config.Servers) == 0 { return 0, newError("No LDAP servers available", nil) } - for _, server := range ldapCfg.Servers { + for _, server := range config.Servers { author := auth.LDAP(server) if err := author.SyncUser(query); err != nil { return 0, newError(err.Error(), nil) diff --git a/pkg/middleware/auth_proxy/auth_proxy_test.go b/pkg/middleware/auth_proxy/auth_proxy_test.go index 47ab75744d639..849c156ac9821 100644 --- a/pkg/middleware/auth_proxy/auth_proxy_test.go +++ b/pkg/middleware/auth_proxy/auth_proxy_test.go @@ -5,16 +5,17 @@ import ( "net/http" "testing" - "github.com/grafana/grafana/pkg/infra/remotecache" - "github.com/grafana/grafana/pkg/login" - models "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" "gopkg.in/macaron.v1" + + "github.com/grafana/grafana/pkg/infra/remotecache" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/ldap" + "github.com/grafana/grafana/pkg/setting" ) type TestLDAP struct { - login.ILdapAuther + ldap.Auth ID int64 syncCalled bool } @@ -62,13 +63,23 @@ func TestMiddlewareContext(t *testing.T) { Convey("LDAP", func() { Convey("gets data from the LDAP", func() { - login.LdapCfg = login.LdapConfig{ - Servers: []*login.LdapServerConf{ - {}, - }, + isLDAPEnabled = func() bool { + return true } - setting.LdapEnabled = true + readLDAPConfig = func() *ldap.Config { + config := &ldap.Config{ + Servers: []*ldap.ServerConfig{ + {}, + }, + } + return config + } + + defer func() { + isLDAPEnabled = ldap.IsEnabled + readLDAPConfig = ldap.ReadConfig + }() store := remotecache.NewFakeStore(t) @@ -82,7 +93,7 @@ func TestMiddlewareContext(t *testing.T) { ID: 42, } - auth.LDAP = func(server *login.LdapServerConf) login.ILdapAuther { + auth.LDAP = func(server *ldap.ServerConfig) ldap.IAuth { return stub } @@ -94,7 +105,21 @@ func TestMiddlewareContext(t *testing.T) { }) Convey("gets nice error if ldap is enabled but not configured", func() { - setting.LdapEnabled = false + isLDAPEnabled = func() bool { + return true + } + + readLDAPConfig = func() *ldap.Config { + config := &ldap.Config{ + Servers: []*ldap.ServerConfig{}, + } + return config + } + + defer func() { + isLDAPEnabled = ldap.IsEnabled + readLDAPConfig = ldap.ReadConfig + }() store := remotecache.NewFakeStore(t) @@ -108,13 +133,14 @@ func TestMiddlewareContext(t *testing.T) { ID: 42, } - auth.LDAP = func(server *login.LdapServerConf) login.ILdapAuther { + auth.LDAP = func(server *ldap.ServerConfig) ldap.IAuth { return stub } id, err := auth.GetUserID() So(err, ShouldNotBeNil) + So(err.Error(), ShouldContainSubstring, "Failed to sync user") So(id, ShouldNotEqual, 42) So(stub.syncCalled, ShouldEqual, false) }) diff --git a/pkg/services/ldap/hooks.go b/pkg/services/ldap/hooks.go new file mode 100644 index 0000000000000..ece98e5e73b09 --- /dev/null +++ b/pkg/services/ldap/hooks.go @@ -0,0 +1,5 @@ +package ldap + +var ( + hookDial func(*Auth) error +) diff --git a/pkg/services/ldap/ldap.go b/pkg/services/ldap/ldap.go new file mode 100644 index 0000000000000..5ff74bd7a3e9c --- /dev/null +++ b/pkg/services/ldap/ldap.go @@ -0,0 +1,559 @@ +package ldap + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io/ioutil" + "strings" + + "github.com/davecgh/go-spew/spew" + LDAP "gopkg.in/ldap.v3" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + models "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" +) + +// IConnection is interface for LDAP connection manipulation +type IConnection interface { + Bind(username, password string) error + UnauthenticatedBind(username string) error + Search(*LDAP.SearchRequest) (*LDAP.SearchResult, error) + StartTLS(*tls.Config) error + Close() +} + +// IAuth is interface for LDAP authorization +type IAuth interface { + Login(query *models.LoginUserQuery) error + SyncUser(query *models.LoginUserQuery) error + GetGrafanaUserFor( + ctx *models.ReqContext, + user *UserInfo, + ) (*models.User, error) + Users() ([]*UserInfo, error) +} + +// Auth is basic struct of LDAP authorization +type Auth struct { + server *ServerConfig + conn IConnection + requireSecondBind bool + log log.Logger +} + +var ( + + // ErrInvalidCredentials is returned if username and password do not match + ErrInvalidCredentials = errors.New("Invalid Username or Password") +) + +var dial = func(network, addr string) (IConnection, error) { + return LDAP.Dial(network, addr) +} + +// New creates the new LDAP auth +func New(server *ServerConfig) IAuth { + return &Auth{ + server: server, + log: log.New("ldap"), + } +} + +// Dial dials in the LDAP +func (auth *Auth) Dial() error { + if hookDial != nil { + return hookDial(auth) + } + + var err error + var certPool *x509.CertPool + if auth.server.RootCACert != "" { + certPool = x509.NewCertPool() + for _, caCertFile := range strings.Split(auth.server.RootCACert, " ") { + pem, err := ioutil.ReadFile(caCertFile) + if err != nil { + return err + } + if !certPool.AppendCertsFromPEM(pem) { + return errors.New("Failed to append CA certificate " + caCertFile) + } + } + } + var clientCert tls.Certificate + if auth.server.ClientCert != "" && auth.server.ClientKey != "" { + clientCert, err = tls.LoadX509KeyPair(auth.server.ClientCert, auth.server.ClientKey) + if err != nil { + return err + } + } + for _, host := range strings.Split(auth.server.Host, " ") { + address := fmt.Sprintf("%s:%d", host, auth.server.Port) + if auth.server.UseSSL { + tlsCfg := &tls.Config{ + InsecureSkipVerify: auth.server.SkipVerifySSL, + ServerName: host, + RootCAs: certPool, + } + if len(clientCert.Certificate) > 0 { + tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert) + } + if auth.server.StartTLS { + auth.conn, err = dial("tcp", address) + if err == nil { + if err = auth.conn.StartTLS(tlsCfg); err == nil { + return nil + } + } + } else { + auth.conn, err = LDAP.DialTLS("tcp", address, tlsCfg) + } + } else { + auth.conn, err = dial("tcp", address) + } + + if err == nil { + return nil + } + } + return err +} + +// Login logs in the user +func (auth *Auth) Login(query *models.LoginUserQuery) error { + // connect to ldap server + if err := auth.Dial(); err != nil { + return err + } + defer auth.conn.Close() + + // perform initial authentication + if err := auth.initialBind(query.Username, query.Password); err != nil { + return err + } + + // find user entry & attributes + user, err := auth.searchForUser(query.Username) + if err != nil { + return err + } + + auth.log.Debug("Ldap User found", "info", spew.Sdump(user)) + + // check if a second user bind is needed + if auth.requireSecondBind { + err = auth.secondBind(user, query.Password) + if err != nil { + return err + } + } + + grafanaUser, err := auth.GetGrafanaUserFor(query.ReqContext, user) + if err != nil { + return err + } + + query.User = grafanaUser + return nil +} + +// SyncUser syncs user with Grafana +func (auth *Auth) SyncUser(query *models.LoginUserQuery) error { + // connect to ldap server + err := auth.Dial() + if err != nil { + return err + } + defer auth.conn.Close() + + err = auth.serverBind() + if err != nil { + return err + } + + // find user entry & attributes + user, err := auth.searchForUser(query.Username) + if err != nil { + auth.log.Error("Failed searching for user in ldap", "error", err) + return err + } + + auth.log.Debug("Ldap User found", "info", spew.Sdump(user)) + + grafanaUser, err := auth.GetGrafanaUserFor(query.ReqContext, user) + if err != nil { + return err + } + + query.User = grafanaUser + return nil +} + +func (auth *Auth) GetGrafanaUserFor( + ctx *models.ReqContext, + user *UserInfo, +) (*models.User, error) { + extUser := &models.ExternalUserInfo{ + AuthModule: "ldap", + AuthId: user.DN, + Name: fmt.Sprintf("%s %s", user.FirstName, user.LastName), + Login: user.Username, + Email: user.Email, + Groups: user.MemberOf, + OrgRoles: map[int64]models.RoleType{}, + } + + for _, group := range auth.server.Groups { + // only use the first match for each org + if extUser.OrgRoles[group.OrgId] != "" { + continue + } + + if user.isMemberOf(group.GroupDN) { + extUser.OrgRoles[group.OrgId] = group.OrgRole + if extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin { + extUser.IsGrafanaAdmin = group.IsGrafanaAdmin + } + } + } + + // validate that the user has access + // if there are no ldap group mappings access is true + // otherwise a single group must match + if len(auth.server.Groups) > 0 && len(extUser.OrgRoles) < 1 { + auth.log.Info( + "Ldap Auth: user does not belong in any of the specified ldap groups", + "username", user.Username, + "groups", user.MemberOf, + ) + return nil, ErrInvalidCredentials + } + + // add/update user in grafana + upsertUserCmd := &models.UpsertUserCommand{ + ReqContext: ctx, + ExternalUser: extUser, + SignupAllowed: setting.LdapAllowSignup, + } + + err := bus.Dispatch(upsertUserCmd) + if err != nil { + return nil, err + } + + return upsertUserCmd.Result, nil +} + +func (auth *Auth) serverBind() error { + bindFn := func() error { + return auth.conn.Bind(auth.server.BindDN, auth.server.BindPassword) + } + + if auth.server.BindPassword == "" { + bindFn = func() error { + return auth.conn.UnauthenticatedBind(auth.server.BindDN) + } + } + + // bind_dn and bind_password to bind + if err := bindFn(); err != nil { + auth.log.Info("LDAP initial bind failed, %v", err) + + if ldapErr, ok := err.(*LDAP.Error); ok { + if ldapErr.ResultCode == 49 { + return ErrInvalidCredentials + } + } + return err + } + + return nil +} + +func (auth *Auth) secondBind(user *UserInfo, userPassword string) error { + if err := auth.conn.Bind(user.DN, userPassword); err != nil { + auth.log.Info("Second bind failed", "error", err) + + if ldapErr, ok := err.(*LDAP.Error); ok { + if ldapErr.ResultCode == 49 { + return ErrInvalidCredentials + } + } + return err + } + + return nil +} + +func (auth *Auth) initialBind(username, userPassword string) error { + if auth.server.BindPassword != "" || auth.server.BindDN == "" { + userPassword = auth.server.BindPassword + auth.requireSecondBind = true + } + + bindPath := auth.server.BindDN + if strings.Contains(bindPath, "%s") { + bindPath = fmt.Sprintf(auth.server.BindDN, username) + } + + bindFn := func() error { + return auth.conn.Bind(bindPath, userPassword) + } + + if userPassword == "" { + bindFn = func() error { + return auth.conn.UnauthenticatedBind(bindPath) + } + } + + if err := bindFn(); err != nil { + auth.log.Info("Initial bind failed", "error", err) + + if ldapErr, ok := err.(*LDAP.Error); ok { + if ldapErr.ResultCode == 49 { + return ErrInvalidCredentials + } + } + return err + } + + return nil +} + +func (auth *Auth) searchForUser(username string) (*UserInfo, error) { + var searchResult *LDAP.SearchResult + var err error + + for _, searchBase := range auth.server.SearchBaseDNs { + attributes := make([]string, 0) + inputs := auth.server.Attr + attributes = appendIfNotEmpty(attributes, + inputs.Username, + inputs.Surname, + inputs.Email, + inputs.Name, + inputs.MemberOf) + + searchReq := LDAP.SearchRequest{ + BaseDN: searchBase, + Scope: LDAP.ScopeWholeSubtree, + DerefAliases: LDAP.NeverDerefAliases, + Attributes: attributes, + Filter: strings.Replace( + auth.server.SearchFilter, + "%s", LDAP.EscapeFilter(username), + -1, + ), + } + + auth.log.Debug("Ldap Search For User Request", "info", spew.Sdump(searchReq)) + + searchResult, err = auth.conn.Search(&searchReq) + if err != nil { + return nil, err + } + + if len(searchResult.Entries) > 0 { + break + } + } + + if len(searchResult.Entries) == 0 { + return nil, ErrInvalidCredentials + } + + if len(searchResult.Entries) > 1 { + return nil, errors.New("Ldap search matched more than one entry, please review your filter setting") + } + + var memberOf []string + if auth.server.GroupSearchFilter == "" { + memberOf = getLdapAttrArray(auth.server.Attr.MemberOf, searchResult) + } else { + // If we are using a POSIX LDAP schema it won't support memberOf, so we manually search the groups + var groupSearchResult *LDAP.SearchResult + for _, groupSearchBase := range auth.server.GroupSearchBaseDNs { + var filter_replace string + if auth.server.GroupSearchFilterUserAttribute == "" { + filter_replace = getLdapAttr(auth.server.Attr.Username, searchResult) + } else { + filter_replace = getLdapAttr(auth.server.GroupSearchFilterUserAttribute, searchResult) + } + + filter := strings.Replace( + auth.server.GroupSearchFilter, "%s", + LDAP.EscapeFilter(filter_replace), + -1, + ) + + auth.log.Info("Searching for user's groups", "filter", filter) + + // support old way of reading settings + groupIdAttribute := auth.server.Attr.MemberOf + // but prefer dn attribute if default settings are used + if groupIdAttribute == "" || groupIdAttribute == "memberOf" { + groupIdAttribute = "dn" + } + + groupSearchReq := LDAP.SearchRequest{ + BaseDN: groupSearchBase, + Scope: LDAP.ScopeWholeSubtree, + DerefAliases: LDAP.NeverDerefAliases, + Attributes: []string{groupIdAttribute}, + Filter: filter, + } + + groupSearchResult, err = auth.conn.Search(&groupSearchReq) + if err != nil { + return nil, err + } + + if len(groupSearchResult.Entries) > 0 { + for i := range groupSearchResult.Entries { + memberOf = append(memberOf, getLdapAttrN(groupIdAttribute, groupSearchResult, i)) + } + break + } + } + } + + return &UserInfo{ + DN: searchResult.Entries[0].DN, + LastName: getLdapAttr(auth.server.Attr.Surname, searchResult), + FirstName: getLdapAttr(auth.server.Attr.Name, searchResult), + Username: getLdapAttr(auth.server.Attr.Username, searchResult), + Email: getLdapAttr(auth.server.Attr.Email, searchResult), + MemberOf: memberOf, + }, nil +} + +func (ldap *Auth) Users() ([]*UserInfo, error) { + var result *LDAP.SearchResult + var err error + server := ldap.server + + if err := ldap.Dial(); err != nil { + return nil, err + } + defer ldap.conn.Close() + + for _, base := range server.SearchBaseDNs { + attributes := make([]string, 0) + inputs := server.Attr + attributes = appendIfNotEmpty( + attributes, + inputs.Username, + inputs.Surname, + inputs.Email, + inputs.Name, + inputs.MemberOf, + ) + + req := LDAP.SearchRequest{ + BaseDN: base, + Scope: LDAP.ScopeWholeSubtree, + DerefAliases: LDAP.NeverDerefAliases, + Attributes: attributes, + + // Doing a star here to get all the users in one go + Filter: strings.Replace(server.SearchFilter, "%s", "*", -1), + } + + result, err = ldap.conn.Search(&req) + if err != nil { + return nil, err + } + + if len(result.Entries) > 0 { + break + } + } + + return ldap.serializeUsers(result), nil +} + +func (ldap *Auth) serializeUsers(users *LDAP.SearchResult) []*UserInfo { + var serialized []*UserInfo + + for index := range users.Entries { + serialize := &UserInfo{ + DN: getLdapAttrN( + "dn", + users, + index, + ), + LastName: getLdapAttrN( + ldap.server.Attr.Surname, + users, + index, + ), + FirstName: getLdapAttrN( + ldap.server.Attr.Name, + users, + index, + ), + Username: getLdapAttrN( + ldap.server.Attr.Username, + users, + index, + ), + Email: getLdapAttrN( + ldap.server.Attr.Email, + users, + index, + ), + MemberOf: getLdapAttrArrayN( + ldap.server.Attr.MemberOf, + users, + index, + ), + } + + serialized = append(serialized, serialize) + } + + return serialized +} + +func appendIfNotEmpty(slice []string, values ...string) []string { + for _, v := range values { + if v != "" { + slice = append(slice, v) + } + } + return slice +} + +func getLdapAttr(name string, result *LDAP.SearchResult) string { + return getLdapAttrN(name, result, 0) +} + +func getLdapAttrN(name string, result *LDAP.SearchResult, n int) string { + if strings.ToLower(name) == "dn" { + return result.Entries[n].DN + } + for _, attr := range result.Entries[n].Attributes { + if attr.Name == name { + if len(attr.Values) > 0 { + return attr.Values[0] + } + } + } + return "" +} + +func getLdapAttrArray(name string, result *LDAP.SearchResult) []string { + return getLdapAttrArrayN(name, result, 0) +} + +func getLdapAttrArrayN(name string, result *LDAP.SearchResult, n int) []string { + for _, attr := range result.Entries[n].Attributes { + if attr.Name == name { + return attr.Values + } + } + return []string{} +} diff --git a/pkg/services/ldap/ldap_login_test.go b/pkg/services/ldap/ldap_login_test.go new file mode 100644 index 0000000000000..5afed5462463e --- /dev/null +++ b/pkg/services/ldap/ldap_login_test.go @@ -0,0 +1,86 @@ +package ldap + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" + "gopkg.in/ldap.v3" + + "github.com/grafana/grafana/pkg/log" +) + +func TestLdapLogin(t *testing.T) { + Convey("Login using ldap", t, func() { + AuthScenario("When login with invalid credentials", func(scenario *scenarioContext) { + conn := &mockLdapConn{} + entry := ldap.Entry{} + result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}} + conn.setSearchResult(&result) + + conn.bindProvider = func(username, password string) error { + return &ldap.Error{ + ResultCode: 49, + } + } + auth := &Auth{ + server: &ServerConfig{ + Attr: AttributeMap{ + Username: "username", + Name: "name", + MemberOf: "memberof", + }, + SearchBaseDNs: []string{"BaseDNHere"}, + }, + conn: conn, + log: log.New("test-logger"), + } + + err := auth.Login(scenario.loginUserQuery) + + Convey("it should return invalid credentials error", func() { + So(err, ShouldEqual, ErrInvalidCredentials) + }) + }) + + AuthScenario("When login with valid credentials", func(scenario *scenarioContext) { + conn := &mockLdapConn{} + entry := ldap.Entry{ + DN: "dn", Attributes: []*ldap.EntryAttribute{ + {Name: "username", Values: []string{"markelog"}}, + {Name: "surname", Values: []string{"Gaidarenko"}}, + {Name: "email", Values: []string{"markelog@gmail.com"}}, + {Name: "name", Values: []string{"Oleg"}}, + {Name: "memberof", Values: []string{"admins"}}, + }, + } + result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}} + conn.setSearchResult(&result) + + conn.bindProvider = func(username, password string) error { + return nil + } + auth := &Auth{ + server: &ServerConfig{ + Attr: AttributeMap{ + Username: "username", + Name: "name", + MemberOf: "memberof", + }, + SearchBaseDNs: []string{"BaseDNHere"}, + }, + conn: conn, + log: log.New("test-logger"), + } + + err := auth.Login(scenario.loginUserQuery) + + Convey("it should not return error", func() { + So(err, ShouldBeNil) + }) + + Convey("it should get user", func() { + So(scenario.loginUserQuery.User.Login, ShouldEqual, "markelog") + }) + }) + }) +} diff --git a/pkg/login/ldap_test.go b/pkg/services/ldap/ldap_test.go similarity index 57% rename from pkg/login/ldap_test.go rename to pkg/services/ldap/ldap_test.go index 543cc90378ca7..c5232a54e2bd5 100644 --- a/pkg/login/ldap_test.go +++ b/pkg/services/ldap/ldap_test.go @@ -1,18 +1,18 @@ -package login +package ldap import ( "context" - "crypto/tls" "testing" + . "github.com/smartystreets/goconvey/convey" + "gopkg.in/ldap.v3" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" - . "github.com/smartystreets/goconvey/convey" - "gopkg.in/ldap.v3" ) -func TestLdapAuther(t *testing.T) { +func TestAuth(t *testing.T) { Convey("initialBind", t, func() { Convey("Given bind dn and password configured", func() { conn := &mockLdapConn{} @@ -22,16 +22,16 @@ func TestLdapAuther(t *testing.T) { actualPassword = password return nil } - ldapAuther := &ldapAuther{ + Auth := &Auth{ conn: conn, - server: &LdapServerConf{ + server: &ServerConfig{ BindDN: "cn=%s,o=users,dc=grafana,dc=org", BindPassword: "bindpwd", }, } - err := ldapAuther.initialBind("user", "pwd") + err := Auth.initialBind("user", "pwd") So(err, ShouldBeNil) - So(ldapAuther.requireSecondBind, ShouldBeTrue) + So(Auth.requireSecondBind, ShouldBeTrue) So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org") So(actualPassword, ShouldEqual, "bindpwd") }) @@ -44,15 +44,15 @@ func TestLdapAuther(t *testing.T) { actualPassword = password return nil } - ldapAuther := &ldapAuther{ + Auth := &Auth{ conn: conn, - server: &LdapServerConf{ + server: &ServerConfig{ BindDN: "cn=%s,o=users,dc=grafana,dc=org", }, } - err := ldapAuther.initialBind("user", "pwd") + err := Auth.initialBind("user", "pwd") So(err, ShouldBeNil) - So(ldapAuther.requireSecondBind, ShouldBeFalse) + So(Auth.requireSecondBind, ShouldBeFalse) So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org") So(actualPassword, ShouldEqual, "pwd") }) @@ -66,13 +66,13 @@ func TestLdapAuther(t *testing.T) { actualUsername = username return nil } - ldapAuther := &ldapAuther{ + Auth := &Auth{ conn: conn, - server: &LdapServerConf{}, + server: &ServerConfig{}, } - err := ldapAuther.initialBind("user", "pwd") + err := Auth.initialBind("user", "pwd") So(err, ShouldBeNil) - So(ldapAuther.requireSecondBind, ShouldBeTrue) + So(Auth.requireSecondBind, ShouldBeTrue) So(unauthenticatedBindWasCalled, ShouldBeTrue) So(actualUsername, ShouldBeEmpty) }) @@ -87,14 +87,14 @@ func TestLdapAuther(t *testing.T) { actualPassword = password return nil } - ldapAuther := &ldapAuther{ + Auth := &Auth{ conn: conn, - server: &LdapServerConf{ + server: &ServerConfig{ BindDN: "o=users,dc=grafana,dc=org", BindPassword: "bindpwd", }, } - err := ldapAuther.serverBind() + err := Auth.serverBind() So(err, ShouldBeNil) So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org") So(actualPassword, ShouldEqual, "bindpwd") @@ -109,13 +109,13 @@ func TestLdapAuther(t *testing.T) { actualUsername = username return nil } - ldapAuther := &ldapAuther{ + Auth := &Auth{ conn: conn, - server: &LdapServerConf{ + server: &ServerConfig{ BindDN: "o=users,dc=grafana,dc=org", }, } - err := ldapAuther.serverBind() + err := Auth.serverBind() So(err, ShouldBeNil) So(unauthenticatedBindWasCalled, ShouldBeTrue) So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org") @@ -130,11 +130,11 @@ func TestLdapAuther(t *testing.T) { actualUsername = username return nil } - ldapAuther := &ldapAuther{ + Auth := &Auth{ conn: conn, - server: &LdapServerConf{}, + server: &ServerConfig{}, } - err := ldapAuther.serverBind() + err := Auth.serverBind() So(err, ShouldBeNil) So(unauthenticatedBindWasCalled, ShouldBeTrue) So(actualUsername, ShouldBeEmpty) @@ -152,59 +152,59 @@ func TestLdapAuther(t *testing.T) { }) Convey("Given no ldap group map match", func() { - ldapAuther := NewLdapAuthenticator(&LdapServerConf{ - LdapGroups: []*LdapGroupToOrgRole{{}}, + Auth := New(&ServerConfig{ + Groups: []*GroupToOrgRole{{}}, }) - _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{}) + _, err := Auth.GetGrafanaUserFor(nil, &UserInfo{}) So(err, ShouldEqual, ErrInvalidCredentials) }) - ldapAutherScenario("Given wildcard group match", func(sc *scenarioContext) { - ldapAuther := NewLdapAuthenticator(&LdapServerConf{ - LdapGroups: []*LdapGroupToOrgRole{ + AuthScenario("Given wildcard group match", func(sc *scenarioContext) { + Auth := New(&ServerConfig{ + Groups: []*GroupToOrgRole{ {GroupDN: "*", OrgRole: "Admin"}, }, }) sc.userQueryReturns(user1) - result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{}) + result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{}) So(err, ShouldBeNil) So(result, ShouldEqual, user1) }) - ldapAutherScenario("Given exact group match", func(sc *scenarioContext) { - ldapAuther := NewLdapAuthenticator(&LdapServerConf{ - LdapGroups: []*LdapGroupToOrgRole{ + AuthScenario("Given exact group match", func(sc *scenarioContext) { + Auth := New(&ServerConfig{ + Groups: []*GroupToOrgRole{ {GroupDN: "cn=users", OrgRole: "Admin"}, }, }) sc.userQueryReturns(user1) - result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{MemberOf: []string{"cn=users"}}) + result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{MemberOf: []string{"cn=users"}}) So(err, ShouldBeNil) So(result, ShouldEqual, user1) }) - ldapAutherScenario("Given group match with different case", func(sc *scenarioContext) { - ldapAuther := NewLdapAuthenticator(&LdapServerConf{ - LdapGroups: []*LdapGroupToOrgRole{ + AuthScenario("Given group match with different case", func(sc *scenarioContext) { + Auth := New(&ServerConfig{ + Groups: []*GroupToOrgRole{ {GroupDN: "cn=users", OrgRole: "Admin"}, }, }) sc.userQueryReturns(user1) - result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{MemberOf: []string{"CN=users"}}) + result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{MemberOf: []string{"CN=users"}}) So(err, ShouldBeNil) So(result, ShouldEqual, user1) }) - ldapAutherScenario("Given no existing grafana user", func(sc *scenarioContext) { - ldapAuther := NewLdapAuthenticator(&LdapServerConf{ - LdapGroups: []*LdapGroupToOrgRole{ + AuthScenario("Given no existing grafana user", func(sc *scenarioContext) { + Auth := New(&ServerConfig{ + Groups: []*GroupToOrgRole{ {GroupDN: "cn=admin", OrgRole: "Admin"}, {GroupDN: "cn=editor", OrgRole: "Editor"}, {GroupDN: "*", OrgRole: "Viewer"}, @@ -213,7 +213,7 @@ func TestLdapAuther(t *testing.T) { sc.userQueryReturns(nil) - result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{ + result, err := Auth.GetGrafanaUserFor(nil, &UserInfo{ DN: "torkelo", Username: "torkelo", Email: "my@email.com", @@ -235,15 +235,15 @@ func TestLdapAuther(t *testing.T) { }) Convey("When syncing ldap groups to grafana org roles", t, func() { - ldapAutherScenario("given no current user orgs", func(sc *scenarioContext) { - ldapAuther := NewLdapAuthenticator(&LdapServerConf{ - LdapGroups: []*LdapGroupToOrgRole{ + AuthScenario("given no current user orgs", func(sc *scenarioContext) { + Auth := New(&ServerConfig{ + Groups: []*GroupToOrgRole{ {GroupDN: "cn=users", OrgRole: "Admin"}, }, }) sc.userOrgsQueryReturns([]*m.UserOrgDTO{}) - _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{ + _, err := Auth.GetGrafanaUserFor(nil, &UserInfo{ MemberOf: []string{"cn=users"}, }) @@ -254,15 +254,15 @@ func TestLdapAuther(t *testing.T) { }) }) - ldapAutherScenario("given different current org role", func(sc *scenarioContext) { - ldapAuther := NewLdapAuthenticator(&LdapServerConf{ - LdapGroups: []*LdapGroupToOrgRole{ + AuthScenario("given different current org role", func(sc *scenarioContext) { + Auth := New(&ServerConfig{ + Groups: []*GroupToOrgRole{ {GroupDN: "cn=users", OrgId: 1, OrgRole: "Admin"}, }, }) sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}}) - _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{ + _, err := Auth.GetGrafanaUserFor(nil, &UserInfo{ MemberOf: []string{"cn=users"}, }) @@ -274,9 +274,9 @@ func TestLdapAuther(t *testing.T) { }) }) - ldapAutherScenario("given current org role is removed in ldap", func(sc *scenarioContext) { - ldapAuther := NewLdapAuthenticator(&LdapServerConf{ - LdapGroups: []*LdapGroupToOrgRole{ + AuthScenario("given current org role is removed in ldap", func(sc *scenarioContext) { + Auth := New(&ServerConfig{ + Groups: []*GroupToOrgRole{ {GroupDN: "cn=users", OrgId: 2, OrgRole: "Admin"}, }, }) @@ -285,7 +285,7 @@ func TestLdapAuther(t *testing.T) { {OrgId: 1, Role: m.ROLE_EDITOR}, {OrgId: 2, Role: m.ROLE_EDITOR}, }) - _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{ + _, err := Auth.GetGrafanaUserFor(nil, &UserInfo{ MemberOf: []string{"cn=users"}, }) @@ -296,16 +296,16 @@ func TestLdapAuther(t *testing.T) { }) }) - ldapAutherScenario("given org role is updated in config", func(sc *scenarioContext) { - ldapAuther := NewLdapAuthenticator(&LdapServerConf{ - LdapGroups: []*LdapGroupToOrgRole{ + AuthScenario("given org role is updated in config", func(sc *scenarioContext) { + Auth := New(&ServerConfig{ + Groups: []*GroupToOrgRole{ {GroupDN: "cn=admin", OrgId: 1, OrgRole: "Admin"}, {GroupDN: "cn=users", OrgId: 1, OrgRole: "Viewer"}, }, }) sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}}) - _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{ + _, err := Auth.GetGrafanaUserFor(nil, &UserInfo{ MemberOf: []string{"cn=users"}, }) @@ -317,16 +317,16 @@ func TestLdapAuther(t *testing.T) { }) }) - ldapAutherScenario("given multiple matching ldap groups", func(sc *scenarioContext) { - ldapAuther := NewLdapAuthenticator(&LdapServerConf{ - LdapGroups: []*LdapGroupToOrgRole{ + AuthScenario("given multiple matching ldap groups", func(sc *scenarioContext) { + Auth := New(&ServerConfig{ + Groups: []*GroupToOrgRole{ {GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin"}, {GroupDN: "*", OrgId: 1, OrgRole: "Viewer"}, }, }) sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_ADMIN}}) - _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{ + _, err := Auth.GetGrafanaUserFor(nil, &UserInfo{ MemberOf: []string{"cn=admins"}, }) @@ -337,16 +337,16 @@ func TestLdapAuther(t *testing.T) { }) }) - ldapAutherScenario("given multiple matching ldap groups and no existing groups", func(sc *scenarioContext) { - ldapAuther := NewLdapAuthenticator(&LdapServerConf{ - LdapGroups: []*LdapGroupToOrgRole{ + AuthScenario("given multiple matching ldap groups and no existing groups", func(sc *scenarioContext) { + Auth := New(&ServerConfig{ + Groups: []*GroupToOrgRole{ {GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin"}, {GroupDN: "*", OrgId: 1, OrgRole: "Viewer"}, }, }) sc.userOrgsQueryReturns([]*m.UserOrgDTO{}) - _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{ + _, err := Auth.GetGrafanaUserFor(nil, &UserInfo{ MemberOf: []string{"cn=admins"}, }) @@ -362,17 +362,17 @@ func TestLdapAuther(t *testing.T) { }) }) - ldapAutherScenario("given ldap groups with grafana_admin=true", func(sc *scenarioContext) { + AuthScenario("given ldap groups with grafana_admin=true", func(sc *scenarioContext) { trueVal := true - ldapAuther := NewLdapAuthenticator(&LdapServerConf{ - LdapGroups: []*LdapGroupToOrgRole{ + Auth := New(&ServerConfig{ + Groups: []*GroupToOrgRole{ {GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin", IsGrafanaAdmin: &trueVal}, }, }) sc.userOrgsQueryReturns([]*m.UserOrgDTO{}) - _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{ + _, err := Auth.GetGrafanaUserFor(nil, &UserInfo{ MemberOf: []string{"cn=admins"}, }) @@ -384,16 +384,16 @@ func TestLdapAuther(t *testing.T) { }) Convey("When calling SyncUser", t, func() { - mockLdapConnection := &mockLdapConn{} - ldapAuther := NewLdapAuthenticator( - &LdapServerConf{ + + auth := &Auth{ + server: &ServerConfig{ Host: "", RootCACert: "", - LdapGroups: []*LdapGroupToOrgRole{ + Groups: []*GroupToOrgRole{ {GroupDN: "*", OrgRole: "Admin"}, }, - Attr: LdapAttributeMap{ + Attr: AttributeMap{ Username: "username", Surname: "surname", Email: "email", @@ -402,10 +402,12 @@ func TestLdapAuther(t *testing.T) { }, SearchBaseDNs: []string{"BaseDNHere"}, }, - ) + conn: mockLdapConnection, + log: log.New("test-logger"), + } dialCalled := false - ldapDial = func(network, addr string) (ILdapConn, error) { + dial = func(network, addr string) (IConnection, error) { dialCalled = true return mockLdapConnection, nil } @@ -421,12 +423,14 @@ func TestLdapAuther(t *testing.T) { result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}} mockLdapConnection.setSearchResult(&result) - ldapAutherScenario("When ldapUser found call syncInfo and orgRoles", func(sc *scenarioContext) { + AuthScenario("When ldapUser found call syncInfo and orgRoles", func(sc *scenarioContext) { // arrange query := &m.LoginUserQuery{ Username: "roelgerrits", } + hookDial = nil + sc.userQueryReturns(&m.User{ Id: 1, Email: "roel@test.net", @@ -436,7 +440,7 @@ func TestLdapAuther(t *testing.T) { sc.userOrgsQueryReturns([]*m.UserOrgDTO{}) // act - syncErrResult := ldapAuther.SyncUser(query) + syncErrResult := auth.SyncUser(query) // assert So(dialCalled, ShouldBeTrue) @@ -465,9 +469,9 @@ func TestLdapAuther(t *testing.T) { mockLdapConnection.setSearchResult(&result) // Set up attribute map without surname and email - ldapAuther := &ldapAuther{ - server: &LdapServerConf{ - Attr: LdapAttributeMap{ + Auth := &Auth{ + server: &ServerConfig{ + Attr: AttributeMap{ Username: "username", Name: "name", MemberOf: "memberof", @@ -478,7 +482,7 @@ func TestLdapAuther(t *testing.T) { log: log.New("test-logger"), } - searchResult, err := ldapAuther.searchForUser("roelgerrits") + searchResult, err := Auth.searchForUser("roelgerrits") So(err, ShouldBeNil) So(searchResult, ShouldNotBeNil) @@ -490,143 +494,3 @@ func TestLdapAuther(t *testing.T) { So(len(mockLdapConnection.searchAttributes), ShouldEqual, 3) }) } - -type mockLdapConn struct { - result *ldap.SearchResult - searchCalled bool - searchAttributes []string - bindProvider func(username, password string) error - unauthenticatedBindProvider func(username string) error -} - -func (c *mockLdapConn) Bind(username, password string) error { - if c.bindProvider != nil { - return c.bindProvider(username, password) - } - - return nil -} - -func (c *mockLdapConn) UnauthenticatedBind(username string) error { - if c.unauthenticatedBindProvider != nil { - return c.unauthenticatedBindProvider(username) - } - - return nil -} - -func (c *mockLdapConn) Close() {} - -func (c *mockLdapConn) setSearchResult(result *ldap.SearchResult) { - c.result = result -} - -func (c *mockLdapConn) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, error) { - c.searchCalled = true - c.searchAttributes = sr.Attributes - return c.result, nil -} - -func (c *mockLdapConn) StartTLS(*tls.Config) error { - return nil -} - -func ldapAutherScenario(desc string, fn scenarioFunc) { - Convey(desc, func() { - defer bus.ClearBusHandlers() - - sc := &scenarioContext{} - loginService := &LoginService{ - Bus: bus.GetBus(), - } - - bus.AddHandler("test", loginService.UpsertUser) - - bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.SyncTeamsCommand) error { - return nil - }) - - bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.UpdateUserPermissionsCommand) error { - sc.updateUserPermissionsCmd = cmd - return nil - }) - - bus.AddHandler("test", func(cmd *m.GetUserByAuthInfoQuery) error { - sc.getUserByAuthInfoQuery = cmd - sc.getUserByAuthInfoQuery.Result = &m.User{Login: cmd.Login} - return nil - }) - - bus.AddHandler("test", func(cmd *m.GetUserOrgListQuery) error { - sc.getUserOrgListQuery = cmd - return nil - }) - - bus.AddHandler("test", func(cmd *m.CreateUserCommand) error { - sc.createUserCmd = cmd - sc.createUserCmd.Result = m.User{Login: cmd.Login} - return nil - }) - - bus.AddHandler("test", func(cmd *m.AddOrgUserCommand) error { - sc.addOrgUserCmd = cmd - return nil - }) - - bus.AddHandler("test", func(cmd *m.UpdateOrgUserCommand) error { - sc.updateOrgUserCmd = cmd - return nil - }) - - bus.AddHandler("test", func(cmd *m.RemoveOrgUserCommand) error { - sc.removeOrgUserCmd = cmd - return nil - }) - - bus.AddHandler("test", func(cmd *m.UpdateUserCommand) error { - sc.updateUserCmd = cmd - return nil - }) - - bus.AddHandler("test", func(cmd *m.SetUsingOrgCommand) error { - sc.setUsingOrgCmd = cmd - return nil - }) - - fn(sc) - }) -} - -type scenarioContext struct { - getUserByAuthInfoQuery *m.GetUserByAuthInfoQuery - getUserOrgListQuery *m.GetUserOrgListQuery - createUserCmd *m.CreateUserCommand - addOrgUserCmd *m.AddOrgUserCommand - updateOrgUserCmd *m.UpdateOrgUserCommand - removeOrgUserCmd *m.RemoveOrgUserCommand - updateUserCmd *m.UpdateUserCommand - setUsingOrgCmd *m.SetUsingOrgCommand - updateUserPermissionsCmd *m.UpdateUserPermissionsCommand -} - -func (sc *scenarioContext) userQueryReturns(user *m.User) { - bus.AddHandler("test", func(query *m.GetUserByAuthInfoQuery) error { - if user == nil { - return m.ErrUserNotFound - } - query.Result = user - return nil - }) - bus.AddHandler("test", func(query *m.SetAuthInfoCommand) error { - return nil - }) -} - -func (sc *scenarioContext) userOrgsQueryReturns(orgs []*m.UserOrgDTO) { - bus.AddHandler("test", func(query *m.GetUserOrgListQuery) error { - query.Result = orgs - return nil - }) -} - -type scenarioFunc func(c *scenarioContext) diff --git a/pkg/services/ldap/settings.go b/pkg/services/ldap/settings.go new file mode 100644 index 0000000000000..c726fff3968e0 --- /dev/null +++ b/pkg/services/ldap/settings.go @@ -0,0 +1,126 @@ +package ldap + +import ( + "fmt" + "os" + + "github.com/BurntSushi/toml" + + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" +) + +type Config struct { + Servers []*ServerConfig `toml:"servers"` +} + +type ServerConfig struct { + Host string `toml:"host"` + Port int `toml:"port"` + UseSSL bool `toml:"use_ssl"` + StartTLS bool `toml:"start_tls"` + SkipVerifySSL bool `toml:"ssl_skip_verify"` + RootCACert string `toml:"root_ca_cert"` + ClientCert string `toml:"client_cert"` + ClientKey string `toml:"client_key"` + BindDN string `toml:"bind_dn"` + BindPassword string `toml:"bind_password"` + Attr AttributeMap `toml:"attributes"` + + SearchFilter string `toml:"search_filter"` + SearchBaseDNs []string `toml:"search_base_dns"` + + GroupSearchFilter string `toml:"group_search_filter"` + GroupSearchFilterUserAttribute string `toml:"group_search_filter_user_attribute"` + GroupSearchBaseDNs []string `toml:"group_search_base_dns"` + + Groups []*GroupToOrgRole `toml:"group_mappings"` +} + +type AttributeMap struct { + Username string `toml:"username"` + Name string `toml:"name"` + Surname string `toml:"surname"` + Email string `toml:"email"` + MemberOf string `toml:"member_of"` +} + +type GroupToOrgRole struct { + GroupDN string `toml:"group_dn"` + OrgId int64 `toml:"org_id"` + IsGrafanaAdmin *bool `toml:"grafana_admin"` // This is a pointer to know if it was set or not (for backwards compatibility) + OrgRole m.RoleType `toml:"org_role"` +} + +var config *Config +var logger = log.New("ldap") + +// IsEnabled checks if ldap is enabled +func IsEnabled() bool { + return setting.LdapEnabled +} + +// ReadConfig reads the config if +// ldap is enabled otherwise it will return nil +func ReadConfig() *Config { + if IsEnabled() == false { + return nil + } + + // Make it a singleton + if config != nil { + return config + } + + config = getConfig(setting.LdapConfigFile) + + return config +} +func getConfig(configFile string) *Config { + result := &Config{} + + logger.Info("Ldap enabled, reading config file", "file", configFile) + + _, err := toml.DecodeFile(configFile, result) + if err != nil { + logger.Crit("Failed to load ldap config file", "error", err) + os.Exit(1) + } + + if len(result.Servers) == 0 { + logger.Crit("ldap enabled but no ldap servers defined in config file") + os.Exit(1) + } + + // set default org id + for _, server := range result.Servers { + assertNotEmptyCfg(server.SearchFilter, "search_filter") + assertNotEmptyCfg(server.SearchBaseDNs, "search_base_dns") + + for _, groupMap := range server.Groups { + if groupMap.OrgId == 0 { + groupMap.OrgId = 1 + } + } + } + + return result +} + +func assertNotEmptyCfg(val interface{}, propName string) { + switch v := val.(type) { + case string: + if v == "" { + logger.Crit("LDAP config file is missing option", "option", propName) + os.Exit(1) + } + case []string: + if len(v) == 0 { + logger.Crit("LDAP config file is missing option", "option", propName) + os.Exit(1) + } + default: + fmt.Println("unknown") + } +} diff --git a/pkg/services/ldap/test.go b/pkg/services/ldap/test.go new file mode 100644 index 0000000000000..98d169b9a1ad1 --- /dev/null +++ b/pkg/services/ldap/test.go @@ -0,0 +1,165 @@ +package ldap + +import ( + "context" + "crypto/tls" + + . "github.com/smartystreets/goconvey/convey" + "gopkg.in/ldap.v3" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/login" +) + +type mockLdapConn struct { + result *ldap.SearchResult + searchCalled bool + searchAttributes []string + bindProvider func(username, password string) error + unauthenticatedBindProvider func(username string) error +} + +func (c *mockLdapConn) Bind(username, password string) error { + if c.bindProvider != nil { + return c.bindProvider(username, password) + } + + return nil +} + +func (c *mockLdapConn) UnauthenticatedBind(username string) error { + if c.unauthenticatedBindProvider != nil { + return c.unauthenticatedBindProvider(username) + } + + return nil +} + +func (c *mockLdapConn) Close() {} + +func (c *mockLdapConn) setSearchResult(result *ldap.SearchResult) { + c.result = result +} + +func (c *mockLdapConn) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, error) { + c.searchCalled = true + c.searchAttributes = sr.Attributes + return c.result, nil +} + +func (c *mockLdapConn) StartTLS(*tls.Config) error { + return nil +} + +func AuthScenario(desc string, fn scenarioFunc) { + Convey(desc, func() { + defer bus.ClearBusHandlers() + + sc := &scenarioContext{ + loginUserQuery: &models.LoginUserQuery{ + Username: "user", + Password: "pwd", + IpAddress: "192.168.1.1:56433", + }, + } + + hookDial = func(auth *Auth) error { + return nil + } + + loginService := &login.LoginService{ + Bus: bus.GetBus(), + } + + bus.AddHandler("test", loginService.UpsertUser) + + bus.AddHandlerCtx("test", func(ctx context.Context, cmd *models.SyncTeamsCommand) error { + return nil + }) + + bus.AddHandlerCtx("test", func(ctx context.Context, cmd *models.UpdateUserPermissionsCommand) error { + sc.updateUserPermissionsCmd = cmd + return nil + }) + + bus.AddHandler("test", func(cmd *models.GetUserByAuthInfoQuery) error { + sc.getUserByAuthInfoQuery = cmd + sc.getUserByAuthInfoQuery.Result = &models.User{Login: cmd.Login} + return nil + }) + + bus.AddHandler("test", func(cmd *models.GetUserOrgListQuery) error { + sc.getUserOrgListQuery = cmd + return nil + }) + + bus.AddHandler("test", func(cmd *models.CreateUserCommand) error { + sc.createUserCmd = cmd + sc.createUserCmd.Result = models.User{Login: cmd.Login} + return nil + }) + + bus.AddHandler("test", func(cmd *models.AddOrgUserCommand) error { + sc.addOrgUserCmd = cmd + return nil + }) + + bus.AddHandler("test", func(cmd *models.UpdateOrgUserCommand) error { + sc.updateOrgUserCmd = cmd + return nil + }) + + bus.AddHandler("test", func(cmd *models.RemoveOrgUserCommand) error { + sc.removeOrgUserCmd = cmd + return nil + }) + + bus.AddHandler("test", func(cmd *models.UpdateUserCommand) error { + sc.updateUserCmd = cmd + return nil + }) + + bus.AddHandler("test", func(cmd *models.SetUsingOrgCommand) error { + sc.setUsingOrgCmd = cmd + return nil + }) + + fn(sc) + }) +} + +type scenarioContext struct { + loginUserQuery *models.LoginUserQuery + getUserByAuthInfoQuery *models.GetUserByAuthInfoQuery + getUserOrgListQuery *models.GetUserOrgListQuery + createUserCmd *models.CreateUserCommand + addOrgUserCmd *models.AddOrgUserCommand + updateOrgUserCmd *models.UpdateOrgUserCommand + removeOrgUserCmd *models.RemoveOrgUserCommand + updateUserCmd *models.UpdateUserCommand + setUsingOrgCmd *models.SetUsingOrgCommand + updateUserPermissionsCmd *models.UpdateUserPermissionsCommand +} + +func (sc *scenarioContext) userQueryReturns(user *models.User) { + bus.AddHandler("test", func(query *models.GetUserByAuthInfoQuery) error { + if user == nil { + return models.ErrUserNotFound + } + query.Result = user + return nil + }) + bus.AddHandler("test", func(query *models.SetAuthInfoCommand) error { + return nil + }) +} + +func (sc *scenarioContext) userOrgsQueryReturns(orgs []*models.UserOrgDTO) { + bus.AddHandler("test", func(query *models.GetUserOrgListQuery) error { + query.Result = orgs + return nil + }) +} + +type scenarioFunc func(c *scenarioContext) diff --git a/pkg/login/ldap_user.go b/pkg/services/ldap/user.go similarity index 75% rename from pkg/login/ldap_user.go rename to pkg/services/ldap/user.go index 3651d9e5e2372..0e4a0063da244 100644 --- a/pkg/login/ldap_user.go +++ b/pkg/services/ldap/user.go @@ -1,10 +1,10 @@ -package login +package ldap import ( "strings" ) -type LdapUserInfo struct { +type UserInfo struct { DN string FirstName string LastName string @@ -13,7 +13,7 @@ type LdapUserInfo struct { MemberOf []string } -func (u *LdapUserInfo) isMemberOf(group string) bool { +func (u *UserInfo) isMemberOf(group string) bool { if group == "*" { return true } diff --git a/pkg/services/login/errors.go b/pkg/services/login/errors.go new file mode 100644 index 0000000000000..0889b4613a6b8 --- /dev/null +++ b/pkg/services/login/errors.go @@ -0,0 +1,15 @@ +package login + +import "errors" + +var ( + ErrEmailNotAllowed = errors.New("Required email domain not fulfilled") + ErrInvalidCredentials = errors.New("Invalid Username or Password") + ErrNoEmail = errors.New("Login provider didn't return an email address") + ErrProviderDeniedRequest = errors.New("Login provider denied login request") + ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter") + ErrTooManyLoginAttempts = errors.New("Too many consecutive incorrect login attempts for user. Login for user temporarily blocked") + ErrPasswordEmpty = errors.New("No password provided.") + ErrUsersQuotaReached = errors.New("Users quota reached") + ErrGettingUserQuota = errors.New("Error getting user quota") +) diff --git a/pkg/login/ext_user.go b/pkg/services/login/login.go similarity index 99% rename from pkg/login/ext_user.go rename to pkg/services/login/login.go index e698110c9c989..9b2a258dea02b 100644 --- a/pkg/login/ext_user.go +++ b/pkg/services/login/login.go @@ -93,6 +93,7 @@ func (ls *LoginService) UpsertUser(cmd *m.UpsertUserCommand) error { } err = syncOrgRoles(cmd.Result, extUser) + if err != nil { return err } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 2aab0001182eb..0ed02f7f0ed81 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -164,10 +164,11 @@ var ( GoogleTagManagerId string // LDAP - LdapEnabled bool - LdapConfigFile string - LdapSyncCron string - LdapAllowSignup = true + LdapEnabled bool + LdapConfigFile string + LdapSyncCron string + LdapAllowSignup bool + LdapActiveSyncEnabled bool // QUOTA Quota QuotaSettings @@ -869,14 +870,8 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { analytics := iniFile.Section("analytics") ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true) CheckForUpdates = analytics.Key("check_for_updates").MustBool(true) - GoogleAnalyticsId, err = valueAsString(analytics, "google_analytics_ua_id", "") - if err != nil { - return err - } - GoogleTagManagerId, err = valueAsString(analytics, "google_tag_manager_id", "") - if err != nil { - return err - } + GoogleAnalyticsId = analytics.Key("google_analytics_ua_id").String() + GoogleTagManagerId = analytics.Key("google_tag_manager_id").String() alerting := iniFile.Section("alerting") AlertingEnabled = alerting.Key("enabled").MustBool(true) @@ -977,10 +972,11 @@ type RemoteCacheOptions struct { func (cfg *Cfg) readLDAPConfig() { ldapSec := cfg.Raw.Section("auth.ldap") - LdapEnabled = ldapSec.Key("enabled").MustBool(false) LdapConfigFile = ldapSec.Key("config_file").String() - LdapAllowSignup = ldapSec.Key("allow_sign_up").MustBool(true) LdapSyncCron = ldapSec.Key("sync_cron").String() + LdapEnabled = ldapSec.Key("enabled").MustBool(false) + LdapActiveSyncEnabled = ldapSec.Key("active_sync_enabled").MustBool(false) + LdapAllowSignup = ldapSec.Key("allow_sign_up").MustBool(true) } func (cfg *Cfg) readSessionConfig() { diff --git a/public/app/core/config.ts b/public/app/core/config.ts index 58aaedffa7cb1..4d83cb8f5b17f 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { PanelPlugin } from 'app/types/plugins'; +import { PanelPluginMeta } from 'app/types/plugins'; import { GrafanaTheme, getTheme, GrafanaThemeType, DataSourceInstanceSettings } from '@grafana/ui'; export interface BuildInfo { @@ -13,7 +13,7 @@ export interface BuildInfo { export class Settings { datasources: { [str: string]: DataSourceInstanceSettings }; - panels: { [key: string]: PanelPlugin }; + panels: { [key: string]: PanelPluginMeta }; appSubUrl: string; windowTitlePrefix: string; buildInfo: BuildInfo; diff --git a/public/app/core/controllers/error_ctrl.ts b/public/app/core/controllers/error_ctrl.ts index a47cae9fea39f..7b834946f407b 100644 --- a/public/app/core/controllers/error_ctrl.ts +++ b/public/app/core/controllers/error_ctrl.ts @@ -4,7 +4,7 @@ import appEvents from 'app/core/app_events'; export class ErrorCtrl { /** @ngInject */ - constructor($scope, contextSrv, navModelSrv) { + constructor($scope: any, contextSrv: any, navModelSrv: any) { $scope.navModel = navModelSrv.getNotFoundNav(); $scope.appSubUrl = config.appSubUrl; diff --git a/public/app/core/controllers/invited_ctrl.ts b/public/app/core/controllers/invited_ctrl.ts index 63f9d975c1faa..dba074dc89025 100644 --- a/public/app/core/controllers/invited_ctrl.ts +++ b/public/app/core/controllers/invited_ctrl.ts @@ -3,7 +3,7 @@ import config from 'app/core/config'; export class InvitedCtrl { /** @ngInject */ - constructor($scope, $routeParams, contextSrv, backendSrv) { + constructor($scope: any, $routeParams: any, contextSrv: any, backendSrv: any) { contextSrv.sidemenu = false; $scope.formModel = {}; @@ -17,7 +17,7 @@ export class InvitedCtrl { }; $scope.init = () => { - backendSrv.get('/api/user/invite/' + $routeParams.code).then(invite => { + backendSrv.get('/api/user/invite/' + $routeParams.code).then((invite: any) => { $scope.formModel.name = invite.name; $scope.formModel.email = invite.email; $scope.formModel.username = invite.email; diff --git a/public/app/core/controllers/json_editor_ctrl.ts b/public/app/core/controllers/json_editor_ctrl.ts index a0384aa8d364d..0c33a0408e7f7 100644 --- a/public/app/core/controllers/json_editor_ctrl.ts +++ b/public/app/core/controllers/json_editor_ctrl.ts @@ -3,7 +3,7 @@ import coreModule from '../core_module'; export class JsonEditorCtrl { /** @ngInject */ - constructor($scope) { + constructor($scope: any) { $scope.json = angular.toJson($scope.model.object, true); $scope.canUpdate = $scope.model.updateHandler !== void 0 && $scope.model.canUpdate; $scope.canCopy = $scope.model.enableCopy; diff --git a/public/app/core/controllers/login_ctrl.ts b/public/app/core/controllers/login_ctrl.ts index 3a72379aa81a5..991ce1146a623 100644 --- a/public/app/core/controllers/login_ctrl.ts +++ b/public/app/core/controllers/login_ctrl.ts @@ -1,10 +1,11 @@ import _ from 'lodash'; import coreModule from '../core_module'; import config from 'app/core/config'; +import { BackendSrv } from '../services/backend_srv'; export class LoginCtrl { /** @ngInject */ - constructor($scope, backendSrv, contextSrv, $location) { + constructor($scope: any, backendSrv: BackendSrv, $location: any) { $scope.formModel = { user: '', email: '', @@ -15,8 +16,6 @@ export class LoginCtrl { $scope.result = ''; $scope.loggingIn = false; - contextSrv.sidemenu = false; - $scope.oauth = config.oauth; $scope.oauthEnabled = _.keys(config.oauth).length > 0; $scope.ldapEnabled = config.ldapEnabled; @@ -83,7 +82,7 @@ export class LoginCtrl { $scope.toGrafana(); }; - $scope.loginModeChanged = newValue => { + $scope.loginModeChanged = (newValue: boolean) => { $scope.submitBtnText = newValue ? 'Log in' : 'Sign up'; }; @@ -92,7 +91,7 @@ export class LoginCtrl { return; } - backendSrv.post('/api/user/signup', $scope.formModel).then(result => { + backendSrv.post('/api/user/signup', $scope.formModel).then((result: any) => { if (result.status === 'SignUpCreated') { $location.path('/signup').search({ email: $scope.formModel.email }); } else { @@ -111,7 +110,7 @@ export class LoginCtrl { backendSrv .post('/login', $scope.formModel) - .then(result => { + .then((result: any) => { $scope.result = result; if ($scope.formModel.password !== 'admin' || $scope.ldapEnabled || $scope.authProxyEnabled) { diff --git a/public/app/core/controllers/reset_password_ctrl.ts b/public/app/core/controllers/reset_password_ctrl.ts index 9ad6864ece230..86c0caf8b2319 100644 --- a/public/app/core/controllers/reset_password_ctrl.ts +++ b/public/app/core/controllers/reset_password_ctrl.ts @@ -1,10 +1,10 @@ import coreModule from '../core_module'; import config from 'app/core/config'; +import { BackendSrv } from '../services/backend_srv'; export class ResetPasswordCtrl { /** @ngInject */ - constructor($scope, contextSrv, backendSrv, $location) { - contextSrv.sidemenu = false; + constructor($scope: any, backendSrv: BackendSrv, $location: any) { $scope.formModel = {}; $scope.mode = 'send'; $scope.ldapEnabled = config.ldapEnabled; diff --git a/public/app/core/directives/array_join.ts b/public/app/core/directives/array_join.ts index 137689ab7d283..f456911ce3fa6 100644 --- a/public/app/core/directives/array_join.ts +++ b/public/app/core/directives/array_join.ts @@ -7,12 +7,12 @@ export function arrayJoin() { return { restrict: 'A', require: 'ngModel', - link: (scope, element, attr, ngModel) => { - function split_array(text) { + link: (scope: any, element: any, attr: any, ngModel: any) => { + function split_array(text: string) { return (text || '').split(','); } - function join_array(text) { + function join_array(text: string) { if (_.isArray(text)) { return ((text || '') as any).join(','); } else { diff --git a/public/app/core/directives/autofill_event_fix.ts b/public/app/core/directives/autofill_event_fix.ts index 4aaa63e276f15..d6bd4af55fa9d 100644 --- a/public/app/core/directives/autofill_event_fix.ts +++ b/public/app/core/directives/autofill_event_fix.ts @@ -1,7 +1,7 @@ import coreModule from '../core_module'; /** @ngInject */ -export function autofillEventFix($compile) { +export function autofillEventFix($compile: any) { return { link: ($scope: any, elem: any) => { const input = elem[0]; diff --git a/public/app/core/directives/diff-view.ts b/public/app/core/directives/diff-view.ts index 84cb597e5430e..b7c41e022c902 100644 --- a/public/app/core/directives/diff-view.ts +++ b/public/app/core/directives/diff-view.ts @@ -5,8 +5,8 @@ export class DeltaCtrl { observer: any; /** @ngInject */ - constructor(private $rootScope) { - const waitForCompile = mutations => { + constructor(private $rootScope: any) { + const waitForCompile = (mutations: any) => { if (mutations.length === 1) { this.$rootScope.appEvent('json-diff-ready'); } @@ -42,10 +42,10 @@ coreModule.directive('diffDelta', delta); // Link to JSON line number export class LinkJSONCtrl { /** @ngInject */ - constructor(private $scope, private $rootScope, private $anchorScroll) {} + constructor(private $scope: any, private $rootScope: any, private $anchorScroll: any) {} goToLine(line: number) { - let unbind; + let unbind: () => void; const scroll = () => { this.$anchorScroll(`l${line}`); diff --git a/public/app/core/directives/dropdown_typeahead.ts b/public/app/core/directives/dropdown_typeahead.ts index 7456df8de53d8..05e2eb49ec56e 100644 --- a/public/app/core/directives/dropdown_typeahead.ts +++ b/public/app/core/directives/dropdown_typeahead.ts @@ -3,7 +3,7 @@ import $ from 'jquery'; import coreModule from '../core_module'; /** @ngInject */ -export function dropdownTypeahead($compile) { +export function dropdownTypeahead($compile: any) { const inputTemplate = ' { + link: ($scope: any, elem: any, attrs: any) => { const $input = $(inputTemplate); const $button = $(buttonTemplate); $input.appendTo(elem); @@ -31,7 +31,7 @@ export function dropdownTypeahead($compile) { } if (attrs.ngModel) { - $scope.$watch('model', newValue => { + $scope.$watch('model', (newValue: any) => { _.each($scope.menuItems, item => { _.each(item.submenu, subItem => { if (subItem.value === newValue) { @@ -59,7 +59,7 @@ export function dropdownTypeahead($compile) { [] ); - $scope.menuItemSelected = (index, subIndex) => { + $scope.menuItemSelected = (index: number, subIndex: number) => { const menuItem = $scope.menuItems[index]; const payload: any = { $item: menuItem }; if (menuItem.submenu && subIndex !== void 0) { @@ -73,7 +73,7 @@ export function dropdownTypeahead($compile) { source: typeaheadValues, minLength: 1, items: 10, - updater: value => { + updater: (value: string) => { const result: any = {}; _.each($scope.menuItems, menuItem => { _.each(menuItem.submenu, submenuItem => { @@ -123,7 +123,7 @@ export function dropdownTypeahead($compile) { } /** @ngInject */ -export function dropdownTypeahead2($compile) { +export function dropdownTypeahead2($compile: any) { const inputTemplate = ''; @@ -139,7 +139,7 @@ export function dropdownTypeahead2($compile) { model: '=ngModel', buttonTemplateClass: '@', }, - link: ($scope, elem, attrs) => { + link: ($scope: any, elem: any, attrs: any) => { const $input = $(inputTemplate); if (!$scope.buttonTemplateClass) { @@ -148,7 +148,7 @@ export function dropdownTypeahead2($compile) { const $button = $(buttonTemplate); const timeoutId = { - blur: null, + blur: null as any, }; $input.appendTo(elem); $button.appendTo(elem); @@ -158,7 +158,7 @@ export function dropdownTypeahead2($compile) { } if (attrs.ngModel) { - $scope.$watch('model', newValue => { + $scope.$watch('model', (newValue: any) => { _.each($scope.menuItems, item => { _.each(item.submenu, subItem => { if (subItem.value === newValue) { @@ -194,7 +194,7 @@ export function dropdownTypeahead2($compile) { elem.removeClass('open'); }; - $scope.menuItemSelected = (index, subIndex) => { + $scope.menuItemSelected = (index: number, subIndex: number) => { const menuItem = $scope.menuItems[index]; const payload: any = { $item: menuItem }; if (menuItem.submenu && subIndex !== void 0) { @@ -209,7 +209,7 @@ export function dropdownTypeahead2($compile) { source: typeaheadValues, minLength: 1, items: 10, - updater: value => { + updater: (value: string) => { const result: any = {}; _.each($scope.menuItems, menuItem => { _.each(menuItem.submenu, submenuItem => { diff --git a/public/app/core/directives/give_focus.ts b/public/app/core/directives/give_focus.ts index 37549ad7229bc..c10dbb9f9ac4a 100644 --- a/public/app/core/directives/give_focus.ts +++ b/public/app/core/directives/give_focus.ts @@ -1,14 +1,14 @@ import coreModule from '../core_module'; coreModule.directive('giveFocus', () => { - return (scope, element, attrs) => { - element.click(e => { + return (scope: any, element: any, attrs: any) => { + element.click((e: any) => { e.stopPropagation(); }); scope.$watch( attrs.giveFocus, - newValue => { + (newValue: any) => { if (!newValue) { return; } diff --git a/public/app/core/directives/metric_segment.ts b/public/app/core/directives/metric_segment.ts index 03bb8209b54b9..22bf6789b8e0b 100644 --- a/public/app/core/directives/metric_segment.ts +++ b/public/app/core/directives/metric_segment.ts @@ -1,9 +1,10 @@ import _ from 'lodash'; import $ from 'jquery'; import coreModule from '../core_module'; +import { TemplateSrv } from 'app/features/templating/template_srv'; /** @ngInject */ -export function metricSegment($compile, $sce, templateSrv) { +export function metricSegment($compile: any, $sce: any, templateSrv: TemplateSrv) { const inputTemplate = ' { + link: ($scope: any, elem: any) => { const $input = $(inputTemplate); const segment = $scope.segment; const $button = $(segment.selectMode ? selectTemplate : linkTemplate); let options = null; - let cancelBlur = null; + let cancelBlur: any = null; let linkMode = true; const debounceLookup = $scope.debounce; $input.appendTo(elem); $button.appendTo(elem); - $scope.updateVariableValue = value => { + $scope.updateVariableValue = (value: string) => { if (value === '' || segment.value === value) { return; } @@ -63,7 +64,7 @@ export function metricSegment($compile, $sce, templateSrv) { }); }; - $scope.switchToLink = fromClick => { + $scope.switchToLink = (fromClick: boolean) => { if (linkMode && !fromClick) { return; } @@ -82,9 +83,9 @@ export function metricSegment($compile, $sce, templateSrv) { cancelBlur = setTimeout($scope.switchToLink, 200); }; - $scope.source = (query, callback) => { + $scope.source = (query: string, callback: any) => { $scope.$apply(() => { - $scope.getOptions({ $query: query }).then(altSegments => { + $scope.getOptions({ $query: query }).then((altSegments: any) => { $scope.altSegments = altSegments; options = _.map($scope.altSegments, alt => { return _.escape(alt.value); @@ -102,7 +103,7 @@ export function metricSegment($compile, $sce, templateSrv) { }); }; - $scope.updater = value => { + $scope.updater = (value: string) => { value = _.unescape(value); if (value === segment.value) { clearTimeout(cancelBlur); @@ -116,7 +117,7 @@ export function metricSegment($compile, $sce, templateSrv) { return value; }; - $scope.matcher = function(item) { + $scope.matcher = function(item: string) { if (linkMode) { return false; } @@ -186,7 +187,7 @@ export function metricSegment($compile, $sce, templateSrv) { } /** @ngInject */ -export function metricSegmentModel(uiSegmentSrv, $q) { +export function metricSegmentModel(uiSegmentSrv: any, $q: any) { return { template: '', @@ -198,10 +199,10 @@ export function metricSegmentModel(uiSegmentSrv, $q) { onChange: '&', }, link: { - pre: function postLink($scope, elem, attrs) { - let cachedOptions; + pre: function postLink($scope: any, elem: any, attrs: any) { + let cachedOptions: any; - $scope.valueToSegment = value => { + $scope.valueToSegment = (value: any) => { const option: any = _.find($scope.options, { value: value }); const segment = { cssClass: attrs.cssClass, @@ -222,7 +223,7 @@ export function metricSegmentModel(uiSegmentSrv, $q) { }) ); } else { - return $scope.getOptions().then(options => { + return $scope.getOptions().then((options: any) => { cachedOptions = options; return _.map(options, option => { if (option.html) { diff --git a/public/app/core/directives/misc.ts b/public/app/core/directives/misc.ts index 192e2df4167c6..1fa0eacac4f25 100644 --- a/public/app/core/directives/misc.ts +++ b/public/app/core/directives/misc.ts @@ -5,10 +5,10 @@ import kbn from 'app/core/utils/kbn'; import { appEvents } from 'app/core/core'; /** @ngInject */ -function tip($compile) { +function tip($compile: any) { return { restrict: 'E', - link: (scope, elem, attrs) => { + link: (scope: any, elem: any, attrs: any) => { let _t = '' + attrs.tip + '' : ''; const showIf = attrs.showIf ? ' ng-show="' + attrs.showIf + '" ' : ''; @@ -115,10 +115,10 @@ function editorOptBool($compile) { } /** @ngInject */ -function editorCheckbox($compile, $interpolate) { +function editorCheckbox($compile: any, $interpolate: any) { return { restrict: 'E', - link: (scope, elem, attrs) => { + link: (scope: any, elem: any, attrs: any) => { const text = $interpolate(attrs.text)(scope); const model = $interpolate(attrs.model)(scope); const ngchange = attrs.change ? ' ng-change="' + attrs.change + '"' : ''; @@ -150,8 +150,8 @@ function editorCheckbox($compile, $interpolate) { } /** @ngInject */ -function gfDropdown($parse, $compile, $timeout) { - function buildTemplate(items, placement?) { +function gfDropdown($parse: any, $compile: any, $timeout: any) { + function buildTemplate(items: any, placement?: any) { const upclass = placement === 'top' ? 'dropup' : ''; const ul = ['']; @@ -191,7 +191,7 @@ function gfDropdown($parse, $compile, $timeout) { return { restrict: 'EA', scope: true, - link: function postLink(scope, iElement, iAttrs) { + link: function postLink(scope: any, iElement: any, iAttrs: any) { const getter = $parse(iAttrs.gfDropdown), items = getter(scope); $timeout(() => { diff --git a/public/app/core/directives/ng_model_on_blur.ts b/public/app/core/directives/ng_model_on_blur.ts index 7e903c1f88932..45991f99b5515 100644 --- a/public/app/core/directives/ng_model_on_blur.ts +++ b/public/app/core/directives/ng_model_on_blur.ts @@ -6,7 +6,7 @@ function ngModelOnBlur() { restrict: 'A', priority: 1, require: 'ngModel', - link: (scope, elm, attr, ngModelCtrl) => { + link: (scope: any, elm: any, attr: any, ngModelCtrl: any) => { if (attr.type === 'radio' || attr.type === 'checkbox') { return; } @@ -25,8 +25,8 @@ function emptyToNull() { return { restrict: 'A', require: 'ngModel', - link: (scope, elm, attrs, ctrl) => { - ctrl.$parsers.push(viewValue => { + link: (scope: any, elm: any, attrs: any, ctrl: any) => { + ctrl.$parsers.push((viewValue: any) => { if (viewValue === '') { return null; } @@ -39,8 +39,8 @@ function emptyToNull() { function validTimeSpan() { return { require: 'ngModel', - link: (scope, elm, attrs, ctrl) => { - ctrl.$validators.integer = (modelValue, viewValue) => { + link: (scope: any, elm: any, attrs: any, ctrl: any) => { + ctrl.$validators.integer = (modelValue: any, viewValue: any) => { if (ctrl.$isEmpty(modelValue)) { return true; } diff --git a/public/app/core/directives/rebuild_on_change.ts b/public/app/core/directives/rebuild_on_change.ts index 43edf9d703f51..57b42ecaa63be 100644 --- a/public/app/core/directives/rebuild_on_change.ts +++ b/public/app/core/directives/rebuild_on_change.ts @@ -1,16 +1,16 @@ import $ from 'jquery'; import coreModule from '../core_module'; -function getBlockNodes(nodes) { +function getBlockNodes(nodes: any[]) { let node = nodes[0]; const endNode = nodes[nodes.length - 1]; - let blockNodes; + let blockNodes: any[]; node = node.nextSibling; for (let i = 1; node !== endNode && node; i++) { if (blockNodes || nodes[i] !== node) { if (!blockNodes) { - blockNodes = $([].slice.call(nodes, 0, i)); + blockNodes = $([].slice.call(nodes, 0, i)) as any; } blockNodes.push(node); } @@ -21,15 +21,15 @@ function getBlockNodes(nodes) { } /** @ngInject */ -function rebuildOnChange($animate) { +function rebuildOnChange($animate: any) { return { multiElement: true, terminal: true, transclude: true, priority: 600, restrict: 'E', - link: (scope, elem, attrs, ctrl, transclude) => { - let block, childScope, previousElements; + link: (scope: any, elem: any, attrs: any, ctrl: any, transclude: any) => { + let block: any, childScope: any, previousElements: any; function cleanUp() { if (previousElements) { @@ -49,13 +49,13 @@ function rebuildOnChange($animate) { } } - scope.$watch(attrs.property, function rebuildOnChangeAction(value, oldValue) { + scope.$watch(attrs.property, function rebuildOnChangeAction(value: any, oldValue: any) { if (childScope && value !== oldValue) { cleanUp(); } if (!childScope && (value || attrs.showNull)) { - transclude((clone, newScope) => { + transclude((clone: any, newScope: any) => { childScope = newScope; clone[clone.length++] = document.createComment(' end rebuild on change '); block = { clone: clone }; diff --git a/public/app/core/directives/tags.ts b/public/app/core/directives/tags.ts index 27bddfb18833c..fb4420f4f2cce 100644 --- a/public/app/core/directives/tags.ts +++ b/public/app/core/directives/tags.ts @@ -4,7 +4,7 @@ import coreModule from '../core_module'; import tags from 'app/core/utils/tags'; import 'vendor/tagsinput/bootstrap-tagsinput.js'; -function setColor(name, element) { +function setColor(name: string, element: JQuery) { const { color, borderColor } = tags.getTagColorsFromName(name); element.css('background-color', color); element.css('border-color', borderColor); @@ -13,14 +13,14 @@ function setColor(name, element) { function tagColorFromName() { return { scope: { tagColorFromName: '=' }, - link: (scope, element) => { + link: (scope: any, element: any) => { setColor(scope.tagColorFromName, element); }, }; } function bootstrapTagsinput() { - function getItemProperty(scope, property) { + function getItemProperty(scope: any, property: any) { if (!property) { return undefined; } @@ -29,7 +29,7 @@ function bootstrapTagsinput() { return scope.$parent[property]; } - return item => { + return (item: any) => { return item[property]; }; } @@ -42,7 +42,7 @@ function bootstrapTagsinput() { }, template: '', replace: false, - link: function(scope, element, attrs) { + link: function(scope: any, element: any, attrs: any) { if (!angular.isArray(scope.model)) { scope.model = []; } diff --git a/public/app/core/directives/value_select_dropdown.ts b/public/app/core/directives/value_select_dropdown.ts index 0df1b2758be04..90a4ad64477b7 100644 --- a/public/app/core/directives/value_select_dropdown.ts +++ b/public/app/core/directives/value_select_dropdown.ts @@ -18,7 +18,7 @@ export class ValueSelectDropdownCtrl { onUpdated: any; /** @ngInject */ - constructor(private $q) {} + constructor(private $q: any) {} show() { this.oldVariableText = this.variable.current.text; @@ -84,7 +84,7 @@ export class ValueSelectDropdownCtrl { this.selectionsChanged(false); } - selectTag(tag) { + selectTag(tag: any) { tag.selected = !tag.selected; let tagValuesPromise; if (!tag.values) { @@ -93,7 +93,7 @@ export class ValueSelectDropdownCtrl { tagValuesPromise = this.$q.when(tag.values); } - return tagValuesPromise.then(values => { + return tagValuesPromise.then((values: any) => { tag.values = values; tag.valuesText = values.join(' + '); _.each(this.options, option => { @@ -106,7 +106,7 @@ export class ValueSelectDropdownCtrl { }); } - keyDown(evt) { + keyDown(evt: any) { if (evt.keyCode === 27) { this.hide(); } @@ -128,11 +128,11 @@ export class ValueSelectDropdownCtrl { } } - moveHighlight(direction) { + moveHighlight(direction: number) { this.highlightIndex = (this.highlightIndex + direction) % this.search.options.length; } - selectValue(option, event, commitChange?, excludeOthers?) { + selectValue(option: any, event: any, commitChange?: boolean, excludeOthers?: boolean) { if (!option) { return; } @@ -142,7 +142,7 @@ export class ValueSelectDropdownCtrl { commitChange = commitChange || false; excludeOthers = excludeOthers || false; - const setAllExceptCurrentTo = newValue => { + const setAllExceptCurrentTo = (newValue: any) => { _.each(this.options, other => { if (option !== other) { other.selected = newValue; @@ -169,7 +169,7 @@ export class ValueSelectDropdownCtrl { this.selectionsChanged(commitChange); } - selectionsChanged(commitChange) { + selectionsChanged(commitChange: boolean) { this.selectedValues = _.filter(this.options, { selected: true }); if (this.selectedValues.length > 1) { @@ -238,14 +238,14 @@ export class ValueSelectDropdownCtrl { } /** @ngInject */ -export function valueSelectDropdown($compile, $window, $timeout, $rootScope) { +export function valueSelectDropdown($compile: any, $window: any, $timeout: any, $rootScope: any) { return { scope: { dashboard: '=', variable: '=', onUpdated: '&' }, templateUrl: 'public/app/partials/valueSelectDropdown.html', controller: 'ValueSelectDropdownCtrl', controllerAs: 'vm', bindToController: true, - link: (scope, elem) => { + link: (scope: any, elem: any) => { const bodyEl = angular.element($window.document.body); const linkEl = elem.find('.variable-value-link'); const inputEl = elem.find('input'); @@ -272,7 +272,7 @@ export function valueSelectDropdown($compile, $window, $timeout, $rootScope) { bodyEl.off('click', bodyOnClick); } - function bodyOnClick(e) { + function bodyOnClick(e: any) { if (elem.has(e.target).length === 0) { scope.$apply(() => { scope.vm.commitChanges(); @@ -280,7 +280,7 @@ export function valueSelectDropdown($compile, $window, $timeout, $rootScope) { } } - scope.$watch('vm.dropdownVisible', newValue => { + scope.$watch('vm.dropdownVisible', (newValue: any) => { if (newValue) { openDropdown(); } else { diff --git a/public/app/core/filters/filters.ts b/public/app/core/filters/filters.ts index c4dbf6b7535be..22e1f9a79837f 100644 --- a/public/app/core/filters/filters.ts +++ b/public/app/core/filters/filters.ts @@ -2,23 +2,25 @@ import _ from 'lodash'; import angular from 'angular'; import moment from 'moment'; import coreModule from '../core_module'; +import { TemplateSrv } from 'app/features/templating/template_srv'; coreModule.filter('stringSort', () => { - return input => { + return (input: any) => { return input.sort(); }; }); coreModule.filter('slice', () => { - return (arr, start, end) => { + return (arr: any[], start: any, end: any) => { if (!_.isUndefined(arr)) { return arr.slice(start, end); } + return arr; }; }); coreModule.filter('stringify', () => { - return arr => { + return (arr: any[]) => { if (_.isObject(arr) && !_.isArray(arr)) { return angular.toJson(arr); } else { @@ -28,7 +30,7 @@ coreModule.filter('stringify', () => { }); coreModule.filter('moment', () => { - return (date, mode) => { + return (date: string, mode: string) => { switch (mode) { case 'ago': return moment(date).fromNow(); @@ -37,25 +39,9 @@ coreModule.filter('moment', () => { }; }); -coreModule.filter('noXml', () => { - const noXml = text => { - return _.isString(text) - ? text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/'/g, ''') - .replace(/"/g, '"') - : text; - }; - return text => { - return _.isArray(text) ? _.map(text, noXml) : noXml(text); - }; -}); - /** @ngInject */ -function interpolateTemplateVars(templateSrv) { - const filterFunc: any = (text, scope) => { +function interpolateTemplateVars(templateSrv: TemplateSrv) { + const filterFunc: any = (text: string, scope: any) => { let scopedVars; if (scope.ctrl) { scopedVars = (scope.ctrl.panel || scope.ctrl.row).scopedVars; diff --git a/public/app/core/live/live_srv.ts b/public/app/core/live/live_srv.ts index 3666fde3b91ec..1c761f373b1bd 100644 --- a/public/app/core/live/live_srv.ts +++ b/public/app/core/live/live_srv.ts @@ -30,7 +30,7 @@ export class LiveSrv { console.log('Live: connecting...'); this.conn = new WebSocket(this.getWebSocketUrl()); - this.conn.onclose = evt => { + this.conn.onclose = (evt: any) => { console.log('Live: websocket onclose', evt); reject({ message: 'Connection closed' }); @@ -38,17 +38,17 @@ export class LiveSrv { setTimeout(this.reconnect.bind(this), 2000); }; - this.conn.onmessage = evt => { + this.conn.onmessage = (evt: any) => { this.handleMessage(evt.data); }; - this.conn.onerror = evt => { + this.conn.onerror = (evt: any) => { this.initPromise = null; reject({ message: 'Connection error' }); console.log('Live: websocket error', evt); }; - this.conn.onopen = evt => { + this.conn.onopen = (evt: any) => { console.log('opened'); this.initPromise = null; resolve(this.conn); @@ -58,7 +58,7 @@ export class LiveSrv { return this.initPromise; } - handleMessage(message) { + handleMessage(message: any) { message = JSON.parse(message); if (!message.stream) { @@ -83,38 +83,38 @@ export class LiveSrv { console.log('LiveSrv: Reconnecting'); - this.getConnection().then(conn => { + this.getConnection().then((conn: any) => { _.each(this.observers, (value, key) => { this.send({ action: 'subscribe', stream: key }); }); }); } - send(data) { + send(data: any) { this.conn.send(JSON.stringify(data)); } - addObserver(stream, observer) { + addObserver(stream: any, observer: any) { this.observers[stream] = observer; - this.getConnection().then(conn => { + this.getConnection().then((conn: any) => { this.send({ action: 'subscribe', stream: stream }); }); } - removeObserver(stream, observer) { + removeObserver(stream: any, observer: any) { console.log('unsubscribe', stream); delete this.observers[stream]; - this.getConnection().then(conn => { + this.getConnection().then((conn: any) => { this.send({ action: 'unsubscribe', stream: stream }); }); } - subscribe(streamName) { + subscribe(streamName: string) { console.log('LiveSrv.subscribe: ' + streamName); - return Observable.create(observer => { + return Observable.create((observer: any) => { this.addObserver(streamName, observer); return () => { diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts index ac0e51854e7f1..942739e45e6f0 100644 --- a/public/app/core/reducers/navModel.ts +++ b/public/app/core/reducers/navModel.ts @@ -27,7 +27,7 @@ export const initialState: NavIndex = buildInitialState(); export const navIndexReducer = (state = initialState, action: Action): NavIndex => { switch (action.type) { case ActionTypes.UpdateNavIndex: - const newPages = {}; + const newPages: NavIndex = {}; const payload = action.payload; for (const node of payload.children) { diff --git a/public/app/core/selectors/location.ts b/public/app/core/selectors/location.ts index adc31f47e89dd..b49bdbe0513af 100644 --- a/public/app/core/selectors/location.ts +++ b/public/app/core/selectors/location.ts @@ -1,3 +1,4 @@ -export const getRouteParamsId = state => state.routeParams.id; +import { LocationState } from 'app/types'; -export const getRouteParamsPage = state => state.routeParams.page; +export const getRouteParamsId = (state: LocationState) => state.routeParams.id; +export const getRouteParamsPage = (state: LocationState) => state.routeParams.page; diff --git a/public/app/core/services/AngularLoader.ts b/public/app/core/services/AngularLoader.ts index d9b78e66cbab9..817e9c9f39857 100644 --- a/public/app/core/services/AngularLoader.ts +++ b/public/app/core/services/AngularLoader.ts @@ -3,16 +3,16 @@ import coreModule from 'app/core/core_module'; import _ from 'lodash'; export interface AngularComponent { - destroy(); - digest(); - getScope(); + destroy(): void; + digest(): void; + getScope(): any; } export class AngularLoader { /** @ngInject */ - constructor(private $compile, private $rootScope) {} + constructor(private $compile: any, private $rootScope: any) {} - load(elem, scopeProps, template): AngularComponent { + load(elem: any, scopeProps: any, template: string): AngularComponent { const scope = this.$rootScope.$new(); _.assign(scope, scopeProps); diff --git a/public/app/core/services/analytics.ts b/public/app/core/services/analytics.ts index be4371adb26c2..1bf143d00cf97 100644 --- a/public/app/core/services/analytics.ts +++ b/public/app/core/services/analytics.ts @@ -4,7 +4,7 @@ import config from 'app/core/config'; export class Analytics { /** @ngInject */ - constructor(private $rootScope, private $location) {} + constructor(private $rootScope: any, private $location: any) {} gaInit() { $.ajax({ @@ -35,7 +35,7 @@ export class Analytics { } /** @ngInject */ -function startAnalytics(googleAnalyticsSrv) { +function startAnalytics(googleAnalyticsSrv: Analytics) { if ((config as any).googleAnalyticsId) { googleAnalyticsSrv.init(); } diff --git a/public/app/core/services/bridge_srv.ts b/public/app/core/services/bridge_srv.ts index 8bb828310cf1d..c5769a61c6b9b 100644 --- a/public/app/core/services/bridge_srv.ts +++ b/public/app/core/services/bridge_srv.ts @@ -3,13 +3,20 @@ import appEvents from 'app/core/app_events'; import { store } from 'app/store/store'; import locationUtil from 'app/core/utils/location_util'; import { updateLocation } from 'app/core/actions'; +import { ITimeoutService, ILocationService, IWindowService, IRootScopeService } from 'angular'; // Services that handles angular -> redux store sync & other react <-> angular sync export class BridgeSrv { - private fullPageReloadRoutes; + private fullPageReloadRoutes: string[]; /** @ngInject */ - constructor(private $location, private $timeout, private $window, private $rootScope, private $route) { + constructor( + private $location: ILocationService, + private $timeout: ITimeoutService, + private $window: IWindowService, + private $rootScope: IRootScopeService, + private $route: any + ) { this.fullPageReloadRoutes = ['/logout']; } @@ -55,7 +62,7 @@ export class BridgeSrv { } }); - appEvents.on('location-change', payload => { + appEvents.on('location-change', (payload: any) => { const urlWithoutBase = locationUtil.stripBaseFromUrl(payload.href); if (this.fullPageReloadRoutes.indexOf(urlWithoutBase) > -1) { this.$window.location.href = payload.href; diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 214c7efb7826e..bbb4488638663 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -44,7 +44,7 @@ export class ContextSrv { this.hasEditPermissionInFolders = this.user.hasEditPermissionInFolders; } - hasRole(role) { + hasRole(role: string) { return this.user.orgRole === role; } diff --git a/public/app/core/services/dynamic_directive_srv.ts b/public/app/core/services/dynamic_directive_srv.ts index c27842ab54ffe..30c4c86024067 100644 --- a/public/app/core/services/dynamic_directive_srv.ts +++ b/public/app/core/services/dynamic_directive_srv.ts @@ -3,9 +3,9 @@ import coreModule from '../core_module'; class DynamicDirectiveSrv { /** @ngInject */ - constructor(private $compile) {} + constructor(private $compile: angular.ICompileService) {} - addDirective(element, name, scope) { + addDirective(element: any, name: string, scope: any) { const child = angular.element(document.createElement(name)); this.$compile(child)(scope); @@ -13,7 +13,7 @@ class DynamicDirectiveSrv { element.append(child); } - link(scope, elem, attrs, options) { + link(scope: any, elem: JQLite, attrs: any, options: any) { const directiveInfo = options.directive(scope); if (!directiveInfo || !directiveInfo.fn) { elem.empty(); @@ -28,13 +28,13 @@ class DynamicDirectiveSrv { this.addDirective(elem, directiveInfo.name, scope); } - create(options) { + create(options: any) { const directiveDef = { restrict: 'E', scope: options.scope, - link: (scope, elem, attrs) => { + link: (scope: any, elem: JQLite, attrs: any) => { if (options.watchPath) { - let childScope = null; + let childScope: any = null; scope.$watch(options.watchPath, () => { if (childScope) { childScope.$destroy(); diff --git a/public/app/core/services/impression_srv.ts b/public/app/core/services/impression_srv.ts index 57464f773b336..8e8a58d664fa2 100644 --- a/public/app/core/services/impression_srv.ts +++ b/public/app/core/services/impression_srv.ts @@ -5,8 +5,8 @@ import config from 'app/core/config'; export class ImpressionSrv { constructor() {} - addDashboardImpression(dashboardId) { - const impressionsKey = this.impressionKey(config); + addDashboardImpression(dashboardId: number) { + const impressionsKey = this.impressionKey(); let impressions = []; if (store.exists(impressionsKey)) { impressions = JSON.parse(store.get(impressionsKey)); @@ -28,7 +28,7 @@ export class ImpressionSrv { } getDashboardOpened() { - let impressions = store.get(this.impressionKey(config)) || '[]'; + let impressions = store.get(this.impressionKey()) || '[]'; impressions = JSON.parse(impressions); @@ -39,7 +39,7 @@ export class ImpressionSrv { return impressions; } - impressionKey(config) { + impressionKey() { return 'dashboard_impressions-' + config.bootData.user.orgId; } } diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 2473c1b8e363c..6fb50dd4ec294 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -9,6 +9,7 @@ import { store } from 'app/store/store'; import Mousetrap from 'mousetrap'; import 'mousetrap-global-bind'; import { ContextSrv } from './context_srv'; +import { ILocationService, ITimeoutService } from 'angular'; export class KeybindingSrv { helpModal: boolean; @@ -17,11 +18,11 @@ export class KeybindingSrv { /** @ngInject */ constructor( - private $rootScope, - private $location, - private $timeout, - private datasourceSrv, - private timeSrv, + private $rootScope: any, + private $location: ILocationService, + private $timeout: ITimeoutService, + private datasourceSrv: any, + private timeSrv: any, private contextSrv: ContextSrv ) { // clear out all shortcuts on route change @@ -114,10 +115,10 @@ export class KeybindingSrv { } } - bind(keyArg, fn) { + bind(keyArg: string | string[], fn: () => void) { Mousetrap.bind( keyArg, - evt => { + (evt: any) => { evt.preventDefault(); evt.stopPropagation(); evt.returnValue = false; @@ -127,10 +128,10 @@ export class KeybindingSrv { ); } - bindGlobal(keyArg, fn) { + bindGlobal(keyArg: string, fn: () => void) { Mousetrap.bindGlobal( keyArg, - evt => { + (evt: any) => { evt.preventDefault(); evt.stopPropagation(); evt.returnValue = false; @@ -149,14 +150,14 @@ export class KeybindingSrv { this.$location.search(search); } - setupDashboardBindings(scope, dashboard) { + setupDashboardBindings(scope: any, dashboard: any) { this.bind('mod+o', () => { dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3; appEvents.emit('graph-hover-clear'); dashboard.startRefresh(); }); - this.bind('mod+s', e => { + this.bind('mod+s', () => { scope.appEvent('save-dashboard'); }); @@ -272,7 +273,7 @@ export class KeybindingSrv { dashboard.expandRows(); }); - this.bind('d n', e => { + this.bind('d n', () => { this.$location.url('/dashboard/new'); }); diff --git a/public/app/core/services/timer.ts b/public/app/core/services/timer.ts index 8234b6288d4e7..65303ed78e473 100644 --- a/public/app/core/services/timer.ts +++ b/public/app/core/services/timer.ts @@ -1,20 +1,21 @@ import _ from 'lodash'; import coreModule from 'app/core/core_module'; +import { ITimeoutService } from 'angular'; // This service really just tracks a list of $timeout promises to give us a // method for canceling them all when we need to export class Timer { - timers = []; + timers: Array> = []; /** @ngInject */ - constructor(private $timeout) {} + constructor(private $timeout: ITimeoutService) {} - register(promise) { + register(promise: angular.IPromise) { this.timers.push(promise); return promise; } - cancel(promise) { + cancel(promise: angular.IPromise) { this.timers = _.without(this.timers, promise); this.$timeout.cancel(promise); } diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts index 61e0ca93c38c2..76151e26c1584 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts @@ -9,7 +9,7 @@ import config from 'app/core/config'; import { DashboardExporter } from './DashboardExporter'; import { DashboardModel } from '../../state/DashboardModel'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { PanelPlugin } from 'app/types'; +import { PanelPluginMeta } from 'app/types'; describe('given dashboard with repeated panels', () => { let dash: any, exported: any; @@ -97,19 +97,19 @@ describe('given dashboard with repeated panels', () => { id: 'graph', name: 'Graph', info: { version: '1.1.0' }, - } as PanelPlugin; + } as PanelPluginMeta; config.panels['table'] = { id: 'table', name: 'Table', info: { version: '1.1.1' }, - } as PanelPlugin; + } as PanelPluginMeta; config.panels['heatmap'] = { id: 'heatmap', name: 'Heatmap', info: { version: '1.1.2' }, - } as PanelPlugin; + } as PanelPluginMeta; dash = new DashboardModel(dash, {}); const exporter = new DashboardExporter(datasourceSrvStub); diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts index 6be4dd2e28081..78d84350b572f 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts @@ -4,7 +4,7 @@ import config from 'app/core/config'; import { DashboardModel } from '../../state/DashboardModel'; import DatasourceSrv from 'app/features/plugins/datasource_srv'; import { PanelModel } from 'app/features/dashboard/state'; -import { PanelPlugin } from 'app/types/plugins'; +import { PanelPluginMeta } from 'app/types/plugins'; interface Input { name: string; @@ -119,7 +119,7 @@ export class DashboardExporter { } } - const panelDef: PanelPlugin = config.panels[panel.type]; + const panelDef: PanelPluginMeta = config.panels[panel.type]; if (panelDef) { requires['panel' + panelDef.id] = { type: 'panel', diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index cf82453cfa727..50e66e97e7a8a 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -267,8 +267,8 @@ export class DashNav extends PureComponent { {!dashboard.timepicker.hidden && (
-
(this.timePickerEl = element)} /> +
)}
diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index cac0c57390092..44ae626cc2050 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -17,8 +17,8 @@ import { PanelResizer } from './PanelResizer'; // Types import { PanelModel, DashboardModel } from '../state'; -import { PanelPlugin } from 'app/types'; -import { AngularPanelPlugin, ReactPanelPlugin } from '@grafana/ui/src/types/panel'; +import { PanelPluginMeta } from 'app/types'; +import { AngularPanelPlugin, PanelPlugin } from '@grafana/ui/src/types/panel'; import { AutoSizer } from 'react-virtualized'; export interface Props { @@ -29,7 +29,7 @@ export interface Props { } export interface State { - plugin: PanelPlugin; + plugin: PanelPluginMeta; angularPanel: AngularComponent; } @@ -61,7 +61,7 @@ export class DashboardPanel extends PureComponent { return ; } - onPluginTypeChanged = (plugin: PanelPlugin) => { + onPluginTypeChanged = (plugin: PanelPluginMeta) => { this.loadPlugin(plugin.id); }; @@ -92,7 +92,7 @@ export class DashboardPanel extends PureComponent { } } - async importPanelPluginModule(plugin: PanelPlugin): Promise { + async importPanelPluginModule(plugin: PanelPluginMeta): Promise { if (plugin.hasBeenImported) { return plugin; } @@ -101,8 +101,8 @@ export class DashboardPanel extends PureComponent { const importedPlugin = await importPanelPlugin(plugin.module); if (importedPlugin instanceof AngularPanelPlugin) { plugin.angularPlugin = importedPlugin as AngularPanelPlugin; - } else if (importedPlugin instanceof ReactPanelPlugin) { - plugin.reactPlugin = importedPlugin as ReactPanelPlugin; + } else if (importedPlugin instanceof PanelPlugin) { + plugin.vizPlugin = importedPlugin as PanelPlugin; } } catch (e) { plugin = getPanelPluginNotFound(plugin.id); @@ -210,7 +210,7 @@ export class DashboardPanel extends PureComponent { onMouseLeave={this.onMouseLeave} style={styles} > - {plugin.reactPlugin && this.renderReactPanel()} + {plugin.vizPlugin && this.renderReactPanel()} {plugin.angularPlugin && this.renderAngularPanel()} )} diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index c69e7be0c4e63..148c559f6aac8 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -16,7 +16,7 @@ import config from 'app/core/config'; // Types import { DashboardModel, PanelModel } from '../state'; -import { PanelPlugin } from 'app/types'; +import { PanelPluginMeta } from 'app/types'; import { LoadingState, PanelData } from '@grafana/ui'; import { ScopedVars } from '@grafana/ui'; @@ -30,7 +30,7 @@ const DEFAULT_PLUGIN_ERROR = 'Error in plugin'; export interface Props { panel: PanelModel; dashboard: DashboardModel; - plugin: PanelPlugin; + plugin: PanelPluginMeta; isFullscreen: boolean; width: number; height: number; @@ -216,7 +216,7 @@ export class PanelChrome extends PureComponent { renderPanel(width: number, height: number): JSX.Element { const { panel, plugin } = this.props; const { renderCounter, data, isFirstLoad } = this.state; - const PanelComponent = plugin.reactPlugin.panel; + const PanelComponent = plugin.vizPlugin.panel; // This is only done to increase a counter that is used by backend // image rendering (phantomjs/headless chrome) to know when to capture image @@ -237,7 +237,7 @@ export class PanelChrome extends PureComponent { { } } -export function getPanelPluginNotFound(id: string): PanelPlugin { +export function getPanelPluginNotFound(id: string): PanelPluginMeta { const NotFound = class NotFound extends PureComponent { render() { return ; @@ -63,7 +63,7 @@ export function getPanelPluginNotFound(id: string): PanelPlugin { updated: '', version: '', }, - reactPlugin: new ReactPanelPlugin(NotFound), + vizPlugin: new PanelPlugin(NotFound), angularPlugin: null, }; } diff --git a/public/app/features/dashboard/panel_editor/PanelEditor.tsx b/public/app/features/dashboard/panel_editor/PanelEditor.tsx index 1bc42a2fd88b9..bc49207e5cc4f 100644 --- a/public/app/features/dashboard/panel_editor/PanelEditor.tsx +++ b/public/app/features/dashboard/panel_editor/PanelEditor.tsx @@ -13,16 +13,16 @@ import { AngularComponent } from 'app/core/services/AngularLoader'; import { PanelModel } from '../state/PanelModel'; import { DashboardModel } from '../state/DashboardModel'; -import { PanelPlugin } from 'app/types/plugins'; +import { PanelPluginMeta } from 'app/types/plugins'; import { Tooltip } from '@grafana/ui'; interface PanelEditorProps { panel: PanelModel; dashboard: DashboardModel; - plugin: PanelPlugin; + plugin: PanelPluginMeta; angularPanel?: AngularComponent; - onTypeChanged: (newType: PanelPlugin) => void; + onTypeChanged: (newType: PanelPluginMeta) => void; } interface PanelEditorTab { diff --git a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx index a8f2c28c2c0ba..3247cf631a764 100644 --- a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx +++ b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx @@ -16,16 +16,16 @@ import { FadeIn } from 'app/core/components/Animations/FadeIn'; // Types import { PanelModel } from '../state'; import { DashboardModel } from '../state'; -import { PanelPlugin } from 'app/types/plugins'; +import { PanelPluginMeta } from 'app/types/plugins'; import { VizPickerSearch } from './VizPickerSearch'; import PluginStateinfo from 'app/features/plugins/PluginStateInfo'; interface Props { panel: PanelModel; dashboard: DashboardModel; - plugin: PanelPlugin; + plugin: PanelPluginMeta; angularPanel?: AngularComponent; - onTypeChanged: (newType: PanelPlugin) => void; + onTypeChanged: (newType: PanelPluginMeta) => void; updateLocation: typeof updateLocation; urlOpenVizPicker: boolean; } @@ -54,7 +54,7 @@ export class VisualizationTab extends PureComponent { getReactPanelOptions = () => { const { panel, plugin } = this.props; - return panel.getOptions(plugin.reactPlugin.defaults); + return panel.getOptions(plugin.vizPlugin.defaults); }; renderPanelOptions() { @@ -64,8 +64,8 @@ export class VisualizationTab extends PureComponent { return
(this.element = element)} />; } - if (plugin.reactPlugin) { - const PanelEditor = plugin.reactPlugin.editor; + if (plugin.vizPlugin) { + const PanelEditor = plugin.vizPlugin.editor; if (PanelEditor) { return ; @@ -197,7 +197,7 @@ export class VisualizationTab extends PureComponent { } }; - onTypeChanged = (plugin: PanelPlugin) => { + onTypeChanged = (plugin: PanelPluginMeta) => { if (plugin.id === this.props.plugin.id) { this.setState({ isVizPickerOpen: false }); } else { diff --git a/public/app/features/dashboard/panel_editor/VizPickerSearch.tsx b/public/app/features/dashboard/panel_editor/VizPickerSearch.tsx index ddf9485dab907..beb9a8508f969 100644 --- a/public/app/features/dashboard/panel_editor/VizPickerSearch.tsx +++ b/public/app/features/dashboard/panel_editor/VizPickerSearch.tsx @@ -2,10 +2,10 @@ import React, { PureComponent } from 'react'; import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; -import { PanelPlugin } from 'app/types'; +import { PanelPluginMeta } from 'app/types'; interface Props { - plugin: PanelPlugin; + plugin: PanelPluginMeta; searchQuery: string; onChange: (query: string) => void; onClose: () => void; diff --git a/public/app/features/dashboard/panel_editor/VizTypePicker.tsx b/public/app/features/dashboard/panel_editor/VizTypePicker.tsx index efbfed65a99d4..71a02f9644eea 100644 --- a/public/app/features/dashboard/panel_editor/VizTypePicker.tsx +++ b/public/app/features/dashboard/panel_editor/VizTypePicker.tsx @@ -1,13 +1,13 @@ import React, { PureComponent } from 'react'; import config from 'app/core/config'; -import { PanelPlugin } from 'app/types/plugins'; +import { PanelPluginMeta } from 'app/types/plugins'; import VizTypePickerPlugin from './VizTypePickerPlugin'; import { EmptySearchResult } from '@grafana/ui'; export interface Props { - current: PanelPlugin; - onTypeChanged: (newType: PanelPlugin) => void; + current: PanelPluginMeta; + onTypeChanged: (newType: PanelPluginMeta) => void; searchQuery: string; onClose: () => void; } @@ -25,16 +25,16 @@ export class VizTypePicker extends PureComponent { return filteredPluginList.length - 1; } - get getPanelPlugins(): PanelPlugin[] { + get getPanelPlugins(): PanelPluginMeta[] { const allPanels = config.panels; return Object.keys(allPanels) .filter(key => allPanels[key]['hideFromList'] === false) .map(key => allPanels[key]) - .sort((a: PanelPlugin, b: PanelPlugin) => a.sort - b.sort); + .sort((a: PanelPluginMeta, b: PanelPluginMeta) => a.sort - b.sort); } - renderVizPlugin = (plugin: PanelPlugin, index: number) => { + renderVizPlugin = (plugin: PanelPluginMeta, index: number) => { const { onTypeChanged } = this.props; const isCurrent = plugin.id === this.props.current.id; @@ -48,7 +48,7 @@ export class VizTypePicker extends PureComponent { ); }; - getFilteredPluginList = (): PanelPlugin[] => { + getFilteredPluginList = (): PanelPluginMeta[] => { const { searchQuery } = this.props; const regex = new RegExp(searchQuery, 'i'); const pluginList = this.pluginList; diff --git a/public/app/features/dashboard/panel_editor/VizTypePickerPlugin.tsx b/public/app/features/dashboard/panel_editor/VizTypePickerPlugin.tsx index 430cf7c7ee398..a91917e918c67 100644 --- a/public/app/features/dashboard/panel_editor/VizTypePickerPlugin.tsx +++ b/public/app/features/dashboard/panel_editor/VizTypePickerPlugin.tsx @@ -1,10 +1,10 @@ import React from 'react'; import classNames from 'classnames'; -import { PanelPlugin } from 'app/types/plugins'; +import { PanelPluginMeta } from 'app/types/plugins'; interface Props { isCurrent: boolean; - plugin: PanelPlugin; + plugin: PanelPluginMeta; onClick: () => void; } diff --git a/public/app/features/dashboard/services/TimeSrv.test.ts b/public/app/features/dashboard/services/TimeSrv.test.ts index e5b4c24078540..8bb93fc3aec5f 100644 --- a/public/app/features/dashboard/services/TimeSrv.test.ts +++ b/public/app/features/dashboard/services/TimeSrv.test.ts @@ -1,5 +1,6 @@ -import { TimeSrv } from './TimeSrv'; import moment from 'moment'; +import { TimeSrv } from './TimeSrv'; +import { ContextSrvStub } from 'test/specs/helpers'; describe('timeSrv', () => { const rootScope = { @@ -26,7 +27,7 @@ describe('timeSrv', () => { }; beforeEach(() => { - timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() }); + timeSrv = new TimeSrv(rootScope as any, jest.fn() as any, location as any, timer, new ContextSrvStub() as any); timeSrv.init(_dashboard); _dashboard.refresh = false; }); @@ -56,7 +57,7 @@ describe('timeSrv', () => { })), }; - timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() }); + timeSrv = new TimeSrv(rootScope as any, jest.fn() as any, location as any, timer, new ContextSrvStub() as any); timeSrv.init(_dashboard); const time = timeSrv.timeRange(); expect(time.raw.from).toBe('now-2d'); @@ -71,7 +72,7 @@ describe('timeSrv', () => { })), }; - timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() }); + timeSrv = new TimeSrv(rootScope as any, jest.fn() as any, location as any, timer, new ContextSrvStub() as any); timeSrv.init(_dashboard); const time = timeSrv.timeRange(); @@ -87,7 +88,7 @@ describe('timeSrv', () => { })), }; - timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() }); + timeSrv = new TimeSrv(rootScope as any, jest.fn() as any, location as any, timer, new ContextSrvStub() as any); // dashboard saved with refresh on _dashboard.refresh = true; @@ -104,7 +105,7 @@ describe('timeSrv', () => { })), }; - timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() }); + timeSrv = new TimeSrv(rootScope as any, jest.fn() as any, location as any, timer, new ContextSrvStub() as any); timeSrv.init(_dashboard); const time = timeSrv.timeRange(); @@ -120,7 +121,7 @@ describe('timeSrv', () => { })), }; - timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() }); + timeSrv = new TimeSrv(rootScope as any, jest.fn() as any, location as any, timer, new ContextSrvStub() as any); timeSrv.init(_dashboard); const time = timeSrv.timeRange(); @@ -136,7 +137,7 @@ describe('timeSrv', () => { })), }; - timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() }); + timeSrv = new TimeSrv(rootScope as any, jest.fn() as any, location as any, timer, new ContextSrvStub() as any); _dashboard.time.from = 'now-6h'; timeSrv.init(_dashboard); diff --git a/public/app/features/dashboard/services/TimeSrv.ts b/public/app/features/dashboard/services/TimeSrv.ts index e54dd212539f8..5fa0c63c35e71 100644 --- a/public/app/features/dashboard/services/TimeSrv.ts +++ b/public/app/features/dashboard/services/TimeSrv.ts @@ -8,19 +8,28 @@ import coreModule from 'app/core/core_module'; import * as dateMath from 'app/core/utils/datemath'; // Types -import { TimeRange } from '@grafana/ui'; +import { TimeRange, RawTimeRange } from '@grafana/ui'; +import { ITimeoutService, ILocationService } from 'angular'; +import { ContextSrv } from 'app/core/services/context_srv'; +import { DashboardModel } from '../state/DashboardModel'; export class TimeSrv { time: any; refreshTimer: any; - refresh: boolean; + refresh: any; oldRefresh: boolean; - dashboard: any; + dashboard: Partial; timeAtLoad: any; private autoRefreshBlocked: boolean; /** @ngInject */ - constructor($rootScope, private $timeout, private $location, private timer, private contextSrv) { + constructor( + $rootScope: any, + private $timeout: ITimeoutService, + private $location: ILocationService, + private timer: any, + private contextSrv: ContextSrv + ) { // default time this.time = { from: '6h', to: 'now' }; @@ -35,7 +44,7 @@ export class TimeSrv { }); } - init(dashboard) { + init(dashboard: Partial) { this.timer.cancelAll(); this.dashboard = dashboard; @@ -63,7 +72,7 @@ export class TimeSrv { } } - private parseUrlParam(value) { + private parseUrlParam(value: any) { if (value.indexOf('now') !== -1) { return value; } @@ -121,7 +130,7 @@ export class TimeSrv { return this.timeAtLoad && (this.timeAtLoad.from !== this.time.from || this.timeAtLoad.to !== this.time.to); } - setAutoRefresh(interval) { + setAutoRefresh(interval: any) { this.dashboard.refresh = interval; this.cancelNextRefresh(); @@ -153,7 +162,7 @@ export class TimeSrv { this.dashboard.timeRangeUpdated(this.timeRange()); } - private startNextRefreshTimer(afterMs) { + private startNextRefreshTimer(afterMs: number) { this.cancelNextRefresh(); this.refreshTimer = this.timer.register( this.$timeout(() => { @@ -171,7 +180,7 @@ export class TimeSrv { this.timer.cancel(this.refreshTimer); } - setTime(time, fromRouteUpdate?) { + setTime(time: RawTimeRange, fromRouteUpdate?: boolean) { _.extend(this.time, time); // disable refresh if zoom in or zoom out @@ -224,7 +233,7 @@ export class TimeSrv { }; } - zoomOut(e, factor) { + zoomOut(e: any, factor: number) { const range = this.timeRange(); const timespan = range.to.valueOf() - range.from.valueOf(); @@ -237,7 +246,7 @@ export class TimeSrv { } } -let singleton; +let singleton: TimeSrv; export function setTimeSrv(srv: TimeSrv) { singleton = srv; diff --git a/public/app/features/dashboard/state/PanelModel.test.ts b/public/app/features/dashboard/state/PanelModel.test.ts index 366c933d58d61..6cde516d1af6a 100644 --- a/public/app/features/dashboard/state/PanelModel.test.ts +++ b/public/app/features/dashboard/state/PanelModel.test.ts @@ -1,6 +1,6 @@ import { PanelModel } from './PanelModel'; import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks'; -import { ReactPanelPlugin, AngularPanelPlugin } from '@grafana/ui/src/types/panel'; +import { PanelPlugin, AngularPanelPlugin } from '@grafana/ui/src/types/panel'; class TablePanelCtrl {} @@ -123,13 +123,13 @@ describe('PanelModel', () => { describe('when changing to react panel', () => { const onPanelTypeChanged = jest.fn(); - const reactPlugin = new ReactPanelPlugin({} as any).setPanelChangeHandler(onPanelTypeChanged as any); + const reactPlugin = new PanelPlugin({} as any).setPanelChangeHandler(onPanelTypeChanged as any); beforeEach(() => { model.changePlugin( getPanelPlugin({ id: 'react', - reactPlugin: reactPlugin, + vizPlugin: reactPlugin, }) ); }); diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index d730b86b828f5..e79d8df00acce 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -7,7 +7,7 @@ import { getNextRefIdChar } from 'app/core/utils/query'; // Types import { DataQuery, Threshold, ScopedVars, DataQueryResponseData } from '@grafana/ui'; -import { PanelPlugin } from 'app/types'; +import { PanelPluginMeta } from 'app/types'; import config from 'app/core/config'; import { PanelQueryRunner } from './PanelQueryRunner'; @@ -117,7 +117,7 @@ export class PanelModel { cacheTimeout?: any; cachedPluginOptions?: any; legend?: { show: boolean }; - plugin?: PanelPlugin; + plugin?: PanelPluginMeta; private queryRunner?: PanelQueryRunner; constructor(model: any) { @@ -249,23 +249,23 @@ export class PanelModel { }); } - private getPluginVersion(plugin: PanelPlugin): string { + private getPluginVersion(plugin: PanelPluginMeta): string { return this.plugin && this.plugin.info.version ? this.plugin.info.version : config.buildInfo.version; } - pluginLoaded(plugin: PanelPlugin) { + pluginLoaded(plugin: PanelPluginMeta) { this.plugin = plugin; - if (plugin.reactPlugin && plugin.reactPlugin.onPanelMigration) { + if (plugin.vizPlugin && plugin.vizPlugin.onPanelMigration) { const version = this.getPluginVersion(plugin); if (version !== this.pluginVersion) { - this.options = plugin.reactPlugin.onPanelMigration(this); + this.options = plugin.vizPlugin.onPanelMigration(this); this.pluginVersion = version; } } } - changePlugin(newPlugin: PanelPlugin) { + changePlugin(newPlugin: PanelPluginMeta) { const pluginId = newPlugin.id; const oldOptions: any = this.getOptionsToRemember(); const oldPluginId = this.type; @@ -292,7 +292,7 @@ export class PanelModel { this.plugin = newPlugin; // Let panel plugins inspect options from previous panel and keep any that it can use - const reactPanel = newPlugin.reactPlugin; + const reactPanel = newPlugin.vizPlugin; if (reactPanel) { if (reactPanel.onPanelTypeChanged) { diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 4222b000eb9cc..0ee34958f4fe1 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -38,9 +38,9 @@ const createResponsiveButton = (options: { return ( ); }; @@ -172,7 +172,7 @@ export class UnConnectedExploreToolbar extends PureComponent {
-
diff --git a/public/app/features/plugins/__mocks__/pluginMocks.ts b/public/app/features/plugins/__mocks__/pluginMocks.ts index f56fb21de2941..089a39ff4953b 100644 --- a/public/app/features/plugins/__mocks__/pluginMocks.ts +++ b/public/app/features/plugins/__mocks__/pluginMocks.ts @@ -1,4 +1,4 @@ -import { Plugin, PanelPlugin, PanelDataFormat } from 'app/types'; +import { Plugin, PanelPluginMeta, PanelDataFormat } from 'app/types'; import { PluginType } from '@grafana/ui'; export const getMockPlugins = (amount: number): Plugin[] => { @@ -34,7 +34,7 @@ export const getMockPlugins = (amount: number): Plugin[] => { return plugins; }; -export const getPanelPlugin = (options: Partial): PanelPlugin => { +export const getPanelPlugin = (options: Partial): PanelPluginMeta => { return { id: options.id, type: PluginType.panel, @@ -58,7 +58,7 @@ export const getPanelPlugin = (options: Partial): PanelPlugin => { hideFromList: options.hideFromList === true, module: '', baseUrl: '', - reactPlugin: options.reactPlugin, + vizPlugin: options.vizPlugin, angularPlugin: options.angularPlugin, }; }; diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index 063c6f989db56..756724ace9d06 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -18,7 +18,7 @@ import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; import TableModel from 'app/core/table_model'; import { coreModule, appEvents, contextSrv } from 'app/core/core'; -import { DataSourcePlugin, AppPlugin, ReactPanelPlugin, AngularPanelPlugin, PluginMeta } from '@grafana/ui/src/types'; +import { DataSourcePlugin, AppPlugin, PanelPlugin, AngularPanelPlugin, PluginMeta } from '@grafana/ui/src/types'; import * as datemath from 'app/core/utils/datemath'; import * as fileExport from 'app/core/utils/file_export'; import * as flatten from 'app/core/utils/flatten'; @@ -182,10 +182,10 @@ export function importAppPlugin(meta: PluginMeta): Promise { }); } -export function importPanelPlugin(path: string): Promise { +export function importPanelPlugin(path: string): Promise { return importPluginModule(path).then(pluginExports => { - if (pluginExports.reactPanel) { - return pluginExports.reactPanel as ReactPanelPlugin; + if (pluginExports.plugin) { + return pluginExports.plugin as PanelPlugin; } else { return new AngularPanelPlugin(pluginExports.PanelCtrl); } diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index e6b70e7b33b4c..ae346aaaa97f2 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -126,7 +126,7 @@ export class TeamPages extends PureComponent { function mapStateToProps(state) { const teamId = getRouteParamsId(state.location); const pageName = getRouteParamsPage(state.location) || 'members'; - const teamLoadingNav = getTeamLoadingNav(pageName); + const teamLoadingNav = getTeamLoadingNav(pageName as string); const navModel = getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav); const team = getTeam(state.team, teamId); const members = getTeamMembers(state.team); diff --git a/public/app/plugins/panel/bargauge/module.tsx b/public/app/plugins/panel/bargauge/module.tsx index 0d0312087e84a..20c973aeaf5df 100644 --- a/public/app/plugins/panel/bargauge/module.tsx +++ b/public/app/plugins/panel/bargauge/module.tsx @@ -1,10 +1,10 @@ -import { ReactPanelPlugin, sharedSingleStatOptionsCheck } from '@grafana/ui'; +import { PanelPlugin, sharedSingleStatOptionsCheck } from '@grafana/ui'; import { BarGaugePanel } from './BarGaugePanel'; import { BarGaugePanelEditor } from './BarGaugePanelEditor'; import { BarGaugeOptions, defaults } from './types'; -export const reactPanel = new ReactPanelPlugin(BarGaugePanel) +export const plugin = new PanelPlugin(BarGaugePanel) .setDefaults(defaults) .setEditor(BarGaugePanelEditor) .setPanelChangeHandler(sharedSingleStatOptionsCheck); diff --git a/public/app/plugins/panel/gauge/module.tsx b/public/app/plugins/panel/gauge/module.tsx index 7cf7841d225f0..6272181e566a0 100644 --- a/public/app/plugins/panel/gauge/module.tsx +++ b/public/app/plugins/panel/gauge/module.tsx @@ -1,9 +1,9 @@ -import { ReactPanelPlugin, sharedSingleStatMigrationCheck, sharedSingleStatOptionsCheck } from '@grafana/ui'; +import { PanelPlugin, sharedSingleStatMigrationCheck, sharedSingleStatOptionsCheck } from '@grafana/ui'; import { GaugePanelEditor } from './GaugePanelEditor'; import { GaugePanel } from './GaugePanel'; import { GaugeOptions, defaults } from './types'; -export const reactPanel = new ReactPanelPlugin(GaugePanel) +export const plugin = new PanelPlugin(GaugePanel) .setDefaults(defaults) .setEditor(GaugePanelEditor) .setPanelChangeHandler(sharedSingleStatOptionsCheck) diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index 8d27c36492f8f..0daf85c3f912b 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -1,6 +1,6 @@ -import { ReactPanelPlugin } from '@grafana/ui'; +import { PanelPlugin } from '@grafana/ui'; import { GraphPanelEditor } from './GraphPanelEditor'; import { GraphPanel } from './GraphPanel'; import { Options, defaults } from './types'; -export const reactPanel = new ReactPanelPlugin(GraphPanel).setDefaults(defaults).setEditor(GraphPanelEditor); +export const plugin = new PanelPlugin(GraphPanel).setDefaults(defaults).setEditor(GraphPanelEditor); diff --git a/public/app/plugins/panel/piechart/module.tsx b/public/app/plugins/panel/piechart/module.tsx index fd9c14a47609d..83c7ddeb5c9e0 100644 --- a/public/app/plugins/panel/piechart/module.tsx +++ b/public/app/plugins/panel/piechart/module.tsx @@ -1,8 +1,8 @@ -import { ReactPanelPlugin } from '@grafana/ui'; +import { PanelPlugin } from '@grafana/ui'; import { PieChartPanelEditor } from './PieChartPanelEditor'; import { PieChartPanel } from './PieChartPanel'; import { PieChartOptions, defaults } from './types'; -export const reactPanel = new ReactPanelPlugin(PieChartPanel) +export const plugin = new PanelPlugin(PieChartPanel) .setDefaults(defaults) .setEditor(PieChartPanelEditor); diff --git a/public/app/plugins/panel/singlestat2/module.tsx b/public/app/plugins/panel/singlestat2/module.tsx index 2be94666d5cc7..abf794f02d3d9 100644 --- a/public/app/plugins/panel/singlestat2/module.tsx +++ b/public/app/plugins/panel/singlestat2/module.tsx @@ -1,9 +1,9 @@ -import { ReactPanelPlugin, sharedSingleStatMigrationCheck, sharedSingleStatOptionsCheck } from '@grafana/ui'; +import { PanelPlugin, sharedSingleStatMigrationCheck, sharedSingleStatOptionsCheck } from '@grafana/ui'; import { SingleStatOptions, defaults } from './types'; import { SingleStatPanel } from './SingleStatPanel'; import { SingleStatEditor } from './SingleStatEditor'; -export const reactPanel = new ReactPanelPlugin(SingleStatPanel) +export const plugin = new PanelPlugin(SingleStatPanel) .setDefaults(defaults) .setEditor(SingleStatEditor) .setPanelChangeHandler(sharedSingleStatOptionsCheck) diff --git a/public/app/plugins/panel/table2/module.tsx b/public/app/plugins/panel/table2/module.tsx index ed04e6867b560..297abee5e2738 100644 --- a/public/app/plugins/panel/table2/module.tsx +++ b/public/app/plugins/panel/table2/module.tsx @@ -1,7 +1,7 @@ -import { ReactPanelPlugin } from '@grafana/ui'; +import { PanelPlugin } from '@grafana/ui'; import { TablePanelEditor } from './TablePanelEditor'; import { TablePanel } from './TablePanel'; import { Options, defaults } from './types'; -export const reactPanel = new ReactPanelPlugin(TablePanel).setDefaults(defaults).setEditor(TablePanelEditor); +export const plugin = new PanelPlugin(TablePanel).setDefaults(defaults).setEditor(TablePanelEditor); diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx index 29d5167463e05..ca3f981a94648 100644 --- a/public/app/plugins/panel/text2/module.tsx +++ b/public/app/plugins/panel/text2/module.tsx @@ -1,10 +1,10 @@ -import { ReactPanelPlugin } from '@grafana/ui'; +import { PanelPlugin } from '@grafana/ui'; import { TextPanelEditor } from './TextPanelEditor'; import { TextPanel } from './TextPanel'; import { TextOptions, defaults } from './types'; -export const reactPanel = new ReactPanelPlugin(TextPanel) +export const plugin = new PanelPlugin(TextPanel) .setDefaults(defaults) .setEditor(TextPanelEditor) .setPanelChangeHandler((options: TextOptions, prevPluginId: string, prevOptions: any) => { diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index ce71bf0243a4e..e1225a387421e 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -1,12 +1,10 @@ -import { AngularPanelPlugin, ReactPanelPlugin, PluginMetaInfo, PluginMeta } from '@grafana/ui/src/types'; +import { AngularPanelPlugin, PanelPlugin, PluginMeta } from '@grafana/ui/src/types'; -export interface PanelPlugin extends PluginMeta { +export interface PanelPluginMeta extends PluginMeta { hideFromList?: boolean; - baseUrl: string; - info: PluginMetaInfo; sort: number; angularPlugin: AngularPanelPlugin | null; - reactPlugin: ReactPanelPlugin | null; + vizPlugin: PanelPlugin | null; hasBeenImported?: boolean; dataFormats: PanelDataFormat[]; } diff --git a/public/sass/_variables.generated.scss b/public/sass/_variables.generated.scss index 16277534f2bdd..510e5649974e9 100644 --- a/public/sass/_variables.generated.scss +++ b/public/sass/_variables.generated.scss @@ -67,7 +67,7 @@ $spacers: ( $grid-breakpoints: ( xs: 0, sm: 544px, - md: 768px, + md: 769px, lg: 992px, xl: 1200px, ) !default; diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index 0128defe16103..8c950f7285900 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -9,6 +9,11 @@ transition-duration: 350ms; transition-timing-function: ease-in-out; transition-property: box-shadow, border-bottom; + + @include media-breakpoint-up(md) { + padding-left: $dashboard-padding; + margin-left: 0; + } } @mixin navbar-alt-look() { @@ -75,13 +80,18 @@ opacity: 0.75; margin-right: 10px; display: none; + + @include media-breakpoint-up(md) { + display: inline-block; + } } &--folder { color: $text-color-weak; + display: none; - @include media-breakpoint-down(md) { - display: none; + @include media-breakpoint-up(lg) { + display: inline-block; } } } @@ -170,17 +180,11 @@ &--secondary { @include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl); } -} -@include media-breakpoint-up(sm) { - .navbar { - padding-left: 20px; - margin-left: 0; - } - - .navbar-page-btn { - .gicon { - display: inline-block; + @include media-breakpoint-down(lg) { + .btn-title { + margin-left: $space-xs; + display: none; } } } diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss index 4e2bc86d960d0..8007e77a81d83 100644 --- a/public/sass/components/_panel_logs.scss +++ b/public/sass/components/_panel_logs.scss @@ -71,7 +71,6 @@ $column-horizontal-spacing: 10px; > div { display: table-cell; padding-right: $column-horizontal-spacing; - vertical-align: middle; border-top: 1px solid transparent; border-bottom: 1px solid transparent; } diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 0e8008926190d..196075d20b502 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -262,7 +262,7 @@ background: $panel-bg; } -@include media-breakpoint-up(sm) { +@include media-breakpoint-up(md) { .search-container { left: $side-menu-width; } diff --git a/public/sass/components/_sidemenu.scss b/public/sass/components/_sidemenu.scss index 7e68ed8d34123..334378036c384 100644 --- a/public/sass/components/_sidemenu.scss +++ b/public/sass/components/_sidemenu.scss @@ -1,3 +1,5 @@ +$mobile-menu-breakpoint: md; + .sidemenu { position: fixed; display: flex; @@ -17,7 +19,7 @@ display: none; } - @include media-breakpoint-up(sm) { + @include media-breakpoint-up($mobile-menu-breakpoint) { background: $side-menu-bg; height: auto; box-shadow: $side-menu-shadow; @@ -46,7 +48,7 @@ .sidemenu__bottom { display: none; - @include media-breakpoint-up(sm) { + @include media-breakpoint-up($mobile-menu-breakpoint) { display: block; } } @@ -55,7 +57,7 @@ position: relative; @include left-brand-border(); - @include media-breakpoint-up(sm) { + @include media-breakpoint-up($mobile-menu-breakpoint) { &.active, &:hover { background-color: $side-menu-item-hover-bg; @@ -214,10 +216,10 @@ li.sidemenu-org-switcher { } } -@include media-breakpoint-down(xs) { +@include media-breakpoint-down(sm) { .sidemenu-open--xs { li { - font-size: $font-size-lg; + font-size: $font-size-md; } .sidemenu { @@ -283,7 +285,6 @@ li.sidemenu-org-switcher { position: unset; width: 100%; float: none; - margin-top: $space-sm; margin-bottom: $space-sm; > li > a { diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index c7bea01df4cf3..fd0a14193abc1 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -31,11 +31,15 @@ flex-flow: row wrap; justify-content: flex-start; height: auto; - padding: 0 $dashboard-padding; + padding: 0 $dashboard-padding 0 50px; border-bottom: 1px solid #0000; transition-duration: 0.35s; transition-timing-function: ease-in-out; transition-property: box-shadow, border-bottom; + + @include media-breakpoint-up(md) { + padding-left: $dashboard-padding; + } } .explore-toolbar-item { @@ -65,11 +69,6 @@ font-size: 18px; min-height: 55px; line-height: 55px; - justify-content: space-between; - margin-left: $space-xl; -} - -.explore-toolbar-header { justify-content: space-between; align-items: center; } @@ -126,12 +125,6 @@ } } -@media only screen and (max-width: 803px) { - .btn-title { - display: none; - } -} - @media only screen and (max-width: 702px) { .explore-toolbar-content-item:first-child { padding-left: 2px; @@ -139,14 +132,6 @@ } } -@media only screen and (max-width: 544px) { - .explore-toolbar-header-title { - .navbar-page-btn { - margin-left: $dashboard-padding; - } - } -} - .explore { flex: 1 1 auto; } diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts index 9b6036d0cfb5b..9c56c65288465 100644 --- a/public/test/specs/helpers.ts +++ b/public/test/specs/helpers.ts @@ -3,7 +3,7 @@ import config from 'app/core/config'; import * as dateMath from 'app/core/utils/datemath'; import { angularMocks, sinon } from '../lib/common'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { PanelPlugin } from 'app/types'; +import { PanelPluginMeta } from 'app/types'; import { RawTimeRange } from '@grafana/ui/src/types'; export function ControllerTestContext(this: any) { @@ -64,7 +64,7 @@ export function ControllerTestContext(this: any) { $rootScope.colors.push('#' + i); } - config.panels['test'] = { info: {} } as PanelPlugin; + config.panels['test'] = { info: {} } as PanelPluginMeta; self.ctrl = $controller( Ctrl, { $scope: self.scope }, @@ -172,6 +172,8 @@ export class TimeSrvStub { } export class ContextSrvStub { + isGrafanaVisibile = jest.fn(); + hasRole() { return true; } diff --git a/scripts/backend-lint.sh b/scripts/backend-lint.sh index 2954243dd82a4..304f91550e0c3 100755 --- a/scripts/backend-lint.sh +++ b/scripts/backend-lint.sh @@ -20,7 +20,7 @@ go get -u github.com/golangci/golangci-lint/cmd/golangci-lint # use gometalinter when lints are not available in golangci or # when gometalinter is better. Eg. goconst for gometalinter does not lint test files -# which is not desired. +# which is not desired. exit_if_fail gometalinter --enable-gc --vendor --deadline 10m --disable-all \ --enable=goconst\ --enable=staticcheck diff --git a/scripts/ci-frontend-metrics.sh b/scripts/ci-frontend-metrics.sh index 476247c01bef7..d9e5fcb82dcae 100755 --- a/scripts/ci-frontend-metrics.sh +++ b/scripts/ci-frontend-metrics.sh @@ -2,7 +2,7 @@ echo -e "Collecting code stats (typescript errors & more)" -ERROR_COUNT_LIMIT=5954 +ERROR_COUNT_LIMIT=5946 DIRECTIVES_LIMIT=175 CONTROLLERS_LIMIT=140 diff --git a/scripts/circle-test-postgres.sh b/scripts/circle-test-postgres.sh index df4897484ad4f..7dc6232b1f2a0 100755 --- a/scripts/circle-test-postgres.sh +++ b/scripts/circle-test-postgres.sh @@ -12,7 +12,6 @@ function exit_if_fail { export GRAFANA_TEST_DB=postgres -exit_if_fail go test -v -run="StatsDataAccess" -tags=integration ./pkg/services/sqlstore/... -#time for d in $(go list ./pkg/...); do -# exit_if_fail go test -tags=integration $d -#done \ No newline at end of file +time for d in $(go list ./pkg/...); do + exit_if_fail go test -tags=integration $d +done \ No newline at end of file diff --git a/vendor/github.com/go-macaron/session/memcache/memcache.go b/vendor/github.com/go-macaron/session/memcache/memcache.go deleted file mode 100644 index 496939398b0cd..0000000000000 --- a/vendor/github.com/go-macaron/session/memcache/memcache.go +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright 2013 Beego Authors -// Copyright 2014 The Macaron Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package session - -import ( - "fmt" - "strings" - "sync" - - "github.com/bradfitz/gomemcache/memcache" - - "github.com/go-macaron/session" -) - -// MemcacheStore represents a memcache session store implementation. -type MemcacheStore struct { - c *memcache.Client - sid string - expire int32 - lock sync.RWMutex - data map[interface{}]interface{} -} - -// NewMemcacheStore creates and returns a memcache session store. -func NewMemcacheStore(c *memcache.Client, sid string, expire int32, kv map[interface{}]interface{}) *MemcacheStore { - return &MemcacheStore{ - c: c, - sid: sid, - expire: expire, - data: kv, - } -} - -func NewItem(sid string, data []byte, expire int32) *memcache.Item { - return &memcache.Item{ - Key: sid, - Value: data, - Expiration: expire, - } -} - -// Set sets value to given key in session. -func (s *MemcacheStore) Set(key, val interface{}) error { - s.lock.Lock() - defer s.lock.Unlock() - - s.data[key] = val - return nil -} - -// Get gets value by given key in session. -func (s *MemcacheStore) Get(key interface{}) interface{} { - s.lock.RLock() - defer s.lock.RUnlock() - - return s.data[key] -} - -// Delete delete a key from session. -func (s *MemcacheStore) Delete(key interface{}) error { - s.lock.Lock() - defer s.lock.Unlock() - - delete(s.data, key) - return nil -} - -// ID returns current session ID. -func (s *MemcacheStore) ID() string { - return s.sid -} - -// Release releases resource and save data to provider. -func (s *MemcacheStore) Release() error { - // Skip encoding if the data is empty - if len(s.data) == 0 { - return nil - } - - data, err := session.EncodeGob(s.data) - if err != nil { - return err - } - - return s.c.Set(NewItem(s.sid, data, s.expire)) -} - -// Flush deletes all session data. -func (s *MemcacheStore) Flush() error { - s.lock.Lock() - defer s.lock.Unlock() - - s.data = make(map[interface{}]interface{}) - return nil -} - -// MemcacheProvider represents a memcache session provider implementation. -type MemcacheProvider struct { - c *memcache.Client - expire int32 -} - -// Init initializes memcache session provider. -// connStrs: 127.0.0.1:9090;127.0.0.1:9091 -func (p *MemcacheProvider) Init(expire int64, connStrs string) error { - p.expire = int32(expire) - p.c = memcache.New(strings.Split(connStrs, ";")...) - return nil -} - -// Read returns raw session store by session ID. -func (p *MemcacheProvider) Read(sid string) (session.RawStore, error) { - if !p.Exist(sid) { - if err := p.c.Set(NewItem(sid, []byte(""), p.expire)); err != nil { - return nil, err - } - } - - var kv map[interface{}]interface{} - item, err := p.c.Get(sid) - if err != nil { - return nil, err - } - if len(item.Value) == 0 { - kv = make(map[interface{}]interface{}) - } else { - kv, err = session.DecodeGob(item.Value) - if err != nil { - return nil, err - } - } - - return NewMemcacheStore(p.c, sid, p.expire, kv), nil -} - -// Exist returns true if session with given ID exists. -func (p *MemcacheProvider) Exist(sid string) bool { - _, err := p.c.Get(sid) - return err == nil -} - -// Destory deletes a session by session ID. -func (p *MemcacheProvider) Destory(sid string) error { - return p.c.Delete(sid) -} - -// Regenerate regenerates a session store from old session ID to new one. -func (p *MemcacheProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err error) { - if p.Exist(sid) { - return nil, fmt.Errorf("new sid '%s' already exists", sid) - } - - item := NewItem(sid, []byte(""), p.expire) - if p.Exist(oldsid) { - item, err = p.c.Get(oldsid) - if err != nil { - return nil, err - } else if err = p.c.Delete(oldsid); err != nil { - return nil, err - } - item.Key = sid - } - if err = p.c.Set(item); err != nil { - return nil, err - } - - var kv map[interface{}]interface{} - if len(item.Value) == 0 { - kv = make(map[interface{}]interface{}) - } else { - kv, err = session.DecodeGob(item.Value) - if err != nil { - return nil, err - } - } - - return NewMemcacheStore(p.c, sid, p.expire, kv), nil -} - -// Count counts and returns number of sessions. -func (p *MemcacheProvider) Count() int { - // FIXME: how come this library does not have Stats method? - return -1 -} - -// GC calls GC to clean expired sessions. -func (p *MemcacheProvider) GC() {} - -func init() { - session.Register("memcache", &MemcacheProvider{}) -} diff --git a/vendor/github.com/go-macaron/session/memcache/memcache.goconvey b/vendor/github.com/go-macaron/session/memcache/memcache.goconvey deleted file mode 100644 index 8485e986e458a..0000000000000 --- a/vendor/github.com/go-macaron/session/memcache/memcache.goconvey +++ /dev/null @@ -1 +0,0 @@ -ignore \ No newline at end of file diff --git a/vendor/github.com/go-macaron/session/postgres/postgres.go b/vendor/github.com/go-macaron/session/postgres/postgres.go deleted file mode 100644 index f1e034b501faa..0000000000000 --- a/vendor/github.com/go-macaron/session/postgres/postgres.go +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright 2013 Beego Authors -// Copyright 2014 The Macaron Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package session - -import ( - "database/sql" - "fmt" - "log" - "sync" - "time" - - _ "github.com/lib/pq" - - "github.com/go-macaron/session" -) - -// PostgresStore represents a postgres session store implementation. -type PostgresStore struct { - c *sql.DB - sid string - lock sync.RWMutex - data map[interface{}]interface{} -} - -// NewPostgresStore creates and returns a postgres session store. -func NewPostgresStore(c *sql.DB, sid string, kv map[interface{}]interface{}) *PostgresStore { - return &PostgresStore{ - c: c, - sid: sid, - data: kv, - } -} - -// Set sets value to given key in session. -func (s *PostgresStore) Set(key, value interface{}) error { - s.lock.Lock() - defer s.lock.Unlock() - - s.data[key] = value - return nil -} - -// Get gets value by given key in session. -func (s *PostgresStore) Get(key interface{}) interface{} { - s.lock.RLock() - defer s.lock.RUnlock() - - return s.data[key] -} - -// Delete delete a key from session. -func (s *PostgresStore) Delete(key interface{}) error { - s.lock.Lock() - defer s.lock.Unlock() - - delete(s.data, key) - return nil -} - -// ID returns current session ID. -func (s *PostgresStore) ID() string { - return s.sid -} - -// save postgres session values to database. -// must call this method to save values to database. -func (s *PostgresStore) Release() error { - // Skip encoding if the data is empty - if len(s.data) == 0 { - return nil - } - - data, err := session.EncodeGob(s.data) - if err != nil { - return err - } - - _, err = s.c.Exec("UPDATE session SET data=$1, expiry=$2 WHERE key=$3", - data, time.Now().Unix(), s.sid) - return err -} - -// Flush deletes all session data. -func (s *PostgresStore) Flush() error { - s.lock.Lock() - defer s.lock.Unlock() - - s.data = make(map[interface{}]interface{}) - return nil -} - -// PostgresProvider represents a postgres session provider implementation. -type PostgresProvider struct { - c *sql.DB - maxlifetime int64 -} - -// Init initializes postgres session provider. -// connStr: user=a password=b host=localhost port=5432 dbname=c sslmode=disable -func (p *PostgresProvider) Init(maxlifetime int64, connStr string) (err error) { - p.maxlifetime = maxlifetime - - p.c, err = sql.Open("postgres", connStr) - if err != nil { - return err - } - return p.c.Ping() -} - -// Read returns raw session store by session ID. -func (p *PostgresProvider) Read(sid string) (session.RawStore, error) { - var data []byte - err := p.c.QueryRow("SELECT data FROM session WHERE key=$1", sid).Scan(&data) - if err == sql.ErrNoRows { - _, err = p.c.Exec("INSERT INTO session(key,data,expiry) VALUES($1,$2,$3)", - sid, "", time.Now().Unix()) - } - if err != nil { - return nil, err - } - - var kv map[interface{}]interface{} - if len(data) == 0 { - kv = make(map[interface{}]interface{}) - } else { - kv, err = session.DecodeGob(data) - if err != nil { - return nil, err - } - } - - return NewPostgresStore(p.c, sid, kv), nil -} - -// Exist returns true if session with given ID exists. -func (p *PostgresProvider) Exist(sid string) bool { - var data []byte - err := p.c.QueryRow("SELECT data FROM session WHERE key=$1", sid).Scan(&data) - if err != nil && err != sql.ErrNoRows { - panic("session/postgres: error checking existence: " + err.Error()) - } - return err != sql.ErrNoRows -} - -// Destory deletes a session by session ID. -func (p *PostgresProvider) Destory(sid string) error { - _, err := p.c.Exec("DELETE FROM session WHERE key=$1", sid) - return err -} - -// Regenerate regenerates a session store from old session ID to new one. -func (p *PostgresProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err error) { - if p.Exist(sid) { - return nil, fmt.Errorf("new sid '%s' already exists", sid) - } - - if !p.Exist(oldsid) { - if _, err = p.c.Exec("INSERT INTO session(key,data,expiry) VALUES($1,$2,$3)", - oldsid, "", time.Now().Unix()); err != nil { - return nil, err - } - } - - if _, err = p.c.Exec("UPDATE session SET key=$1 WHERE key=$2", sid, oldsid); err != nil { - return nil, err - } - - return p.Read(sid) -} - -// Count counts and returns number of sessions. -func (p *PostgresProvider) Count() (total int) { - if err := p.c.QueryRow("SELECT COUNT(*) AS NUM FROM session").Scan(&total); err != nil { - panic("session/postgres: error counting records: " + err.Error()) - } - return total -} - -// GC calls GC to clean expired sessions. -func (p *PostgresProvider) GC() { - if _, err := p.c.Exec("DELETE FROM session WHERE EXTRACT(EPOCH FROM NOW()) - expiry > $1", p.maxlifetime); err != nil { - log.Printf("session/postgres: error garbage collecting: %v", err) - } -} - -func init() { - session.Register("postgres", &PostgresProvider{}) -} diff --git a/vendor/github.com/go-macaron/session/postgres/postgres.goconvey b/vendor/github.com/go-macaron/session/postgres/postgres.goconvey deleted file mode 100644 index 8485e986e458a..0000000000000 --- a/vendor/github.com/go-macaron/session/postgres/postgres.goconvey +++ /dev/null @@ -1 +0,0 @@ -ignore \ No newline at end of file diff --git a/vendor/github.com/go-macaron/session/redis/redis.go b/vendor/github.com/go-macaron/session/redis/redis.go deleted file mode 100644 index 2d7fe984055d0..0000000000000 --- a/vendor/github.com/go-macaron/session/redis/redis.go +++ /dev/null @@ -1,240 +0,0 @@ -// Copyright 2013 Beego Authors -// Copyright 2014 The Macaron Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package session - -import ( - "fmt" - "strings" - "sync" - "time" - - "github.com/Unknwon/com" - "gopkg.in/ini.v1" - "gopkg.in/redis.v2" - - "github.com/go-macaron/session" -) - -// RedisStore represents a redis session store implementation. -type RedisStore struct { - c *redis.Client - prefix, sid string - duration time.Duration - lock sync.RWMutex - data map[interface{}]interface{} -} - -// NewRedisStore creates and returns a redis session store. -func NewRedisStore(c *redis.Client, prefix, sid string, dur time.Duration, kv map[interface{}]interface{}) *RedisStore { - return &RedisStore{ - c: c, - prefix: prefix, - sid: sid, - duration: dur, - data: kv, - } -} - -// Set sets value to given key in session. -func (s *RedisStore) Set(key, val interface{}) error { - s.lock.Lock() - defer s.lock.Unlock() - - s.data[key] = val - return nil -} - -// Get gets value by given key in session. -func (s *RedisStore) Get(key interface{}) interface{} { - s.lock.RLock() - defer s.lock.RUnlock() - - return s.data[key] -} - -// Delete delete a key from session. -func (s *RedisStore) Delete(key interface{}) error { - s.lock.Lock() - defer s.lock.Unlock() - - delete(s.data, key) - return nil -} - -// ID returns current session ID. -func (s *RedisStore) ID() string { - return s.sid -} - -// Release releases resource and save data to provider. -func (s *RedisStore) Release() error { - // Skip encoding if the data is empty - if len(s.data) == 0 { - return nil - } - - data, err := session.EncodeGob(s.data) - if err != nil { - return err - } - - return s.c.SetEx(s.prefix+s.sid, s.duration, string(data)).Err() -} - -// Flush deletes all session data. -func (s *RedisStore) Flush() error { - s.lock.Lock() - defer s.lock.Unlock() - - s.data = make(map[interface{}]interface{}) - return nil -} - -// RedisProvider represents a redis session provider implementation. -type RedisProvider struct { - c *redis.Client - duration time.Duration - prefix string -} - -// Init initializes redis session provider. -// configs: network=tcp,addr=:6379,password=macaron,db=0,pool_size=100,idle_timeout=180,prefix=session; -func (p *RedisProvider) Init(maxlifetime int64, configs string) (err error) { - p.duration, err = time.ParseDuration(fmt.Sprintf("%ds", maxlifetime)) - if err != nil { - return err - } - - cfg, err := ini.Load([]byte(strings.Replace(configs, ",", "\n", -1))) - if err != nil { - return err - } - - opt := &redis.Options{ - Network: "tcp", - } - for k, v := range cfg.Section("").KeysHash() { - switch k { - case "network": - opt.Network = v - case "addr": - opt.Addr = v - case "password": - opt.Password = v - case "db": - opt.DB = com.StrTo(v).MustInt64() - case "pool_size": - opt.PoolSize = com.StrTo(v).MustInt() - case "idle_timeout": - opt.IdleTimeout, err = time.ParseDuration(v + "s") - if err != nil { - return fmt.Errorf("error parsing idle timeout: %v", err) - } - case "prefix": - p.prefix = v - default: - return fmt.Errorf("session/redis: unsupported option '%s'", k) - } - } - - p.c = redis.NewClient(opt) - return p.c.Ping().Err() -} - -// Read returns raw session store by session ID. -func (p *RedisProvider) Read(sid string) (session.RawStore, error) { - psid := p.prefix + sid - if !p.Exist(sid) { - if err := p.c.SetEx(psid, p.duration, "").Err(); err != nil { - return nil, err - } - } - - var kv map[interface{}]interface{} - kvs, err := p.c.Get(psid).Result() - if err != nil { - return nil, err - } - if len(kvs) == 0 { - kv = make(map[interface{}]interface{}) - } else { - kv, err = session.DecodeGob([]byte(kvs)) - if err != nil { - return nil, err - } - } - - return NewRedisStore(p.c, p.prefix, sid, p.duration, kv), nil -} - -// Exist returns true if session with given ID exists. -func (p *RedisProvider) Exist(sid string) bool { - has, err := p.c.Exists(p.prefix + sid).Result() - return err == nil && has -} - -// Destory deletes a session by session ID. -func (p *RedisProvider) Destory(sid string) error { - return p.c.Del(p.prefix + sid).Err() -} - -// Regenerate regenerates a session store from old session ID to new one. -func (p *RedisProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err error) { - poldsid := p.prefix + oldsid - psid := p.prefix + sid - - if p.Exist(sid) { - return nil, fmt.Errorf("new sid '%s' already exists", sid) - } else if !p.Exist(oldsid) { - // Make a fake old session. - if err = p.c.SetEx(poldsid, p.duration, "").Err(); err != nil { - return nil, err - } - } - - if err = p.c.Rename(poldsid, psid).Err(); err != nil { - return nil, err - } - - var kv map[interface{}]interface{} - kvs, err := p.c.Get(psid).Result() - if err != nil { - return nil, err - } - - if len(kvs) == 0 { - kv = make(map[interface{}]interface{}) - } else { - kv, err = session.DecodeGob([]byte(kvs)) - if err != nil { - return nil, err - } - } - - return NewRedisStore(p.c, p.prefix, sid, p.duration, kv), nil -} - -// Count counts and returns number of sessions. -func (p *RedisProvider) Count() int { - return int(p.c.DbSize().Val()) -} - -// GC calls GC to clean expired sessions. -func (_ *RedisProvider) GC() {} - -func init() { - session.Register("redis", &RedisProvider{}) -} diff --git a/vendor/github.com/go-macaron/session/redis/redis.goconvey b/vendor/github.com/go-macaron/session/redis/redis.goconvey deleted file mode 100644 index 8485e986e458a..0000000000000 --- a/vendor/github.com/go-macaron/session/redis/redis.goconvey +++ /dev/null @@ -1 +0,0 @@ -ignore \ No newline at end of file diff --git a/vendor/github.com/robfig/cron/.gitignore b/vendor/github.com/robfig/cron/.gitignore new file mode 100644 index 0000000000000..00268614f0456 --- /dev/null +++ b/vendor/github.com/robfig/cron/.gitignore @@ -0,0 +1,22 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe diff --git a/vendor/github.com/robfig/cron/LICENSE b/vendor/github.com/robfig/cron/LICENSE new file mode 100644 index 0000000000000..3a0f627ffeb53 --- /dev/null +++ b/vendor/github.com/robfig/cron/LICENSE @@ -0,0 +1,21 @@ +Copyright (C) 2012 Rob Figueiredo +All Rights Reserved. + +MIT LICENSE + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/robfig/cron/README.md b/vendor/github.com/robfig/cron/README.md new file mode 100644 index 0000000000000..4e0ae1c25f391 --- /dev/null +++ b/vendor/github.com/robfig/cron/README.md @@ -0,0 +1,6 @@ +[![GoDoc](http://godoc.org/github.com/robfig/cron?status.png)](http://godoc.org/github.com/robfig/cron) +[![Build Status](https://travis-ci.org/robfig/cron.svg?branch=master)](https://travis-ci.org/robfig/cron) + +# cron + +Documentation here: https://godoc.org/github.com/robfig/cron diff --git a/vendor/github.com/robfig/cron/constantdelay.go b/vendor/github.com/robfig/cron/constantdelay.go new file mode 100644 index 0000000000000..cd6e7b1be91a6 --- /dev/null +++ b/vendor/github.com/robfig/cron/constantdelay.go @@ -0,0 +1,27 @@ +package cron + +import "time" + +// ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes". +// It does not support jobs more frequent than once a second. +type ConstantDelaySchedule struct { + Delay time.Duration +} + +// Every returns a crontab Schedule that activates once every duration. +// Delays of less than a second are not supported (will round up to 1 second). +// Any fields less than a Second are truncated. +func Every(duration time.Duration) ConstantDelaySchedule { + if duration < time.Second { + duration = time.Second + } + return ConstantDelaySchedule{ + Delay: duration - time.Duration(duration.Nanoseconds())%time.Second, + } +} + +// Next returns the next time this should be run. +// This rounds so that the next activation time will be on the second. +func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time { + return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond) +} diff --git a/vendor/github.com/robfig/cron/cron.go b/vendor/github.com/robfig/cron/cron.go new file mode 100644 index 0000000000000..2318aeb2e7dff --- /dev/null +++ b/vendor/github.com/robfig/cron/cron.go @@ -0,0 +1,259 @@ +package cron + +import ( + "log" + "runtime" + "sort" + "time" +) + +// Cron keeps track of any number of entries, invoking the associated func as +// specified by the schedule. It may be started, stopped, and the entries may +// be inspected while running. +type Cron struct { + entries []*Entry + stop chan struct{} + add chan *Entry + snapshot chan []*Entry + running bool + ErrorLog *log.Logger + location *time.Location +} + +// Job is an interface for submitted cron jobs. +type Job interface { + Run() +} + +// The Schedule describes a job's duty cycle. +type Schedule interface { + // Return the next activation time, later than the given time. + // Next is invoked initially, and then each time the job is run. + Next(time.Time) time.Time +} + +// Entry consists of a schedule and the func to execute on that schedule. +type Entry struct { + // The schedule on which this job should be run. + Schedule Schedule + + // The next time the job will run. This is the zero time if Cron has not been + // started or this entry's schedule is unsatisfiable + Next time.Time + + // The last time this job was run. This is the zero time if the job has never + // been run. + Prev time.Time + + // The Job to run. + Job Job +} + +// byTime is a wrapper for sorting the entry array by time +// (with zero time at the end). +type byTime []*Entry + +func (s byTime) Len() int { return len(s) } +func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byTime) Less(i, j int) bool { + // Two zero times should return false. + // Otherwise, zero is "greater" than any other time. + // (To sort it at the end of the list.) + if s[i].Next.IsZero() { + return false + } + if s[j].Next.IsZero() { + return true + } + return s[i].Next.Before(s[j].Next) +} + +// New returns a new Cron job runner, in the Local time zone. +func New() *Cron { + return NewWithLocation(time.Now().Location()) +} + +// NewWithLocation returns a new Cron job runner. +func NewWithLocation(location *time.Location) *Cron { + return &Cron{ + entries: nil, + add: make(chan *Entry), + stop: make(chan struct{}), + snapshot: make(chan []*Entry), + running: false, + ErrorLog: nil, + location: location, + } +} + +// A wrapper that turns a func() into a cron.Job +type FuncJob func() + +func (f FuncJob) Run() { f() } + +// AddFunc adds a func to the Cron to be run on the given schedule. +func (c *Cron) AddFunc(spec string, cmd func()) error { + return c.AddJob(spec, FuncJob(cmd)) +} + +// AddJob adds a Job to the Cron to be run on the given schedule. +func (c *Cron) AddJob(spec string, cmd Job) error { + schedule, err := Parse(spec) + if err != nil { + return err + } + c.Schedule(schedule, cmd) + return nil +} + +// Schedule adds a Job to the Cron to be run on the given schedule. +func (c *Cron) Schedule(schedule Schedule, cmd Job) { + entry := &Entry{ + Schedule: schedule, + Job: cmd, + } + if !c.running { + c.entries = append(c.entries, entry) + return + } + + c.add <- entry +} + +// Entries returns a snapshot of the cron entries. +func (c *Cron) Entries() []*Entry { + if c.running { + c.snapshot <- nil + x := <-c.snapshot + return x + } + return c.entrySnapshot() +} + +// Location gets the time zone location +func (c *Cron) Location() *time.Location { + return c.location +} + +// Start the cron scheduler in its own go-routine, or no-op if already started. +func (c *Cron) Start() { + if c.running { + return + } + c.running = true + go c.run() +} + +// Run the cron scheduler, or no-op if already running. +func (c *Cron) Run() { + if c.running { + return + } + c.running = true + c.run() +} + +func (c *Cron) runWithRecovery(j Job) { + defer func() { + if r := recover(); r != nil { + const size = 64 << 10 + buf := make([]byte, size) + buf = buf[:runtime.Stack(buf, false)] + c.logf("cron: panic running job: %v\n%s", r, buf) + } + }() + j.Run() +} + +// Run the scheduler. this is private just due to the need to synchronize +// access to the 'running' state variable. +func (c *Cron) run() { + // Figure out the next activation times for each entry. + now := c.now() + for _, entry := range c.entries { + entry.Next = entry.Schedule.Next(now) + } + + for { + // Determine the next entry to run. + sort.Sort(byTime(c.entries)) + + var timer *time.Timer + if len(c.entries) == 0 || c.entries[0].Next.IsZero() { + // If there are no entries yet, just sleep - it still handles new entries + // and stop requests. + timer = time.NewTimer(100000 * time.Hour) + } else { + timer = time.NewTimer(c.entries[0].Next.Sub(now)) + } + + for { + select { + case now = <-timer.C: + now = now.In(c.location) + // Run every entry whose next time was less than now + for _, e := range c.entries { + if e.Next.After(now) || e.Next.IsZero() { + break + } + go c.runWithRecovery(e.Job) + e.Prev = e.Next + e.Next = e.Schedule.Next(now) + } + + case newEntry := <-c.add: + timer.Stop() + now = c.now() + newEntry.Next = newEntry.Schedule.Next(now) + c.entries = append(c.entries, newEntry) + + case <-c.snapshot: + c.snapshot <- c.entrySnapshot() + continue + + case <-c.stop: + timer.Stop() + return + } + + break + } + } +} + +// Logs an error to stderr or to the configured error log +func (c *Cron) logf(format string, args ...interface{}) { + if c.ErrorLog != nil { + c.ErrorLog.Printf(format, args...) + } else { + log.Printf(format, args...) + } +} + +// Stop stops the cron scheduler if it is running; otherwise it does nothing. +func (c *Cron) Stop() { + if !c.running { + return + } + c.stop <- struct{}{} + c.running = false +} + +// entrySnapshot returns a copy of the current cron entry list. +func (c *Cron) entrySnapshot() []*Entry { + entries := []*Entry{} + for _, e := range c.entries { + entries = append(entries, &Entry{ + Schedule: e.Schedule, + Next: e.Next, + Prev: e.Prev, + Job: e.Job, + }) + } + return entries +} + +// now returns current time in c location +func (c *Cron) now() time.Time { + return time.Now().In(c.location) +} diff --git a/vendor/github.com/robfig/cron/doc.go b/vendor/github.com/robfig/cron/doc.go new file mode 100644 index 0000000000000..1ce84f7bf4623 --- /dev/null +++ b/vendor/github.com/robfig/cron/doc.go @@ -0,0 +1,129 @@ +/* +Package cron implements a cron spec parser and job runner. + +Usage + +Callers may register Funcs to be invoked on a given schedule. Cron will run +them in their own goroutines. + + c := cron.New() + c.AddFunc("0 30 * * * *", func() { fmt.Println("Every hour on the half hour") }) + c.AddFunc("@hourly", func() { fmt.Println("Every hour") }) + c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") }) + c.Start() + .. + // Funcs are invoked in their own goroutine, asynchronously. + ... + // Funcs may also be added to a running Cron + c.AddFunc("@daily", func() { fmt.Println("Every day") }) + .. + // Inspect the cron job entries' next and previous run times. + inspect(c.Entries()) + .. + c.Stop() // Stop the scheduler (does not stop any jobs already running). + +CRON Expression Format + +A cron expression represents a set of times, using 6 space-separated fields. + + Field name | Mandatory? | Allowed values | Allowed special characters + ---------- | ---------- | -------------- | -------------------------- + Seconds | Yes | 0-59 | * / , - + Minutes | Yes | 0-59 | * / , - + Hours | Yes | 0-23 | * / , - + Day of month | Yes | 1-31 | * / , - ? + Month | Yes | 1-12 or JAN-DEC | * / , - + Day of week | Yes | 0-6 or SUN-SAT | * / , - ? + +Note: Month and Day-of-week field values are case insensitive. "SUN", "Sun", +and "sun" are equally accepted. + +Special Characters + +Asterisk ( * ) + +The asterisk indicates that the cron expression will match for all values of the +field; e.g., using an asterisk in the 5th field (month) would indicate every +month. + +Slash ( / ) + +Slashes are used to describe increments of ranges. For example 3-59/15 in the +1st field (minutes) would indicate the 3rd minute of the hour and every 15 +minutes thereafter. The form "*\/..." is equivalent to the form "first-last/...", +that is, an increment over the largest possible range of the field. The form +"N/..." is accepted as meaning "N-MAX/...", that is, starting at N, use the +increment until the end of that specific range. It does not wrap around. + +Comma ( , ) + +Commas are used to separate items of a list. For example, using "MON,WED,FRI" in +the 5th field (day of week) would mean Mondays, Wednesdays and Fridays. + +Hyphen ( - ) + +Hyphens are used to define ranges. For example, 9-17 would indicate every +hour between 9am and 5pm inclusive. + +Question mark ( ? ) + +Question mark may be used instead of '*' for leaving either day-of-month or +day-of-week blank. + +Predefined schedules + +You may use one of several pre-defined schedules in place of a cron expression. + + Entry | Description | Equivalent To + ----- | ----------- | ------------- + @yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * + @monthly | Run once a month, midnight, first of month | 0 0 0 1 * * + @weekly | Run once a week, midnight between Sat/Sun | 0 0 0 * * 0 + @daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * + @hourly | Run once an hour, beginning of hour | 0 0 * * * * + +Intervals + +You may also schedule a job to execute at fixed intervals, starting at the time it's added +or cron is run. This is supported by formatting the cron spec like this: + + @every + +where "duration" is a string accepted by time.ParseDuration +(http://golang.org/pkg/time/#ParseDuration). + +For example, "@every 1h30m10s" would indicate a schedule that activates after +1 hour, 30 minutes, 10 seconds, and then every interval after that. + +Note: The interval does not take the job runtime into account. For example, +if a job takes 3 minutes to run, and it is scheduled to run every 5 minutes, +it will have only 2 minutes of idle time between each run. + +Time zones + +All interpretation and scheduling is done in the machine's local time zone (as +provided by the Go time package (http://www.golang.org/pkg/time). + +Be aware that jobs scheduled during daylight-savings leap-ahead transitions will +not be run! + +Thread safety + +Since the Cron service runs concurrently with the calling code, some amount of +care must be taken to ensure proper synchronization. + +All cron methods are designed to be correctly synchronized as long as the caller +ensures that invocations have a clear happens-before ordering between them. + +Implementation + +Cron entries are stored in an array, sorted by their next activation time. Cron +sleeps until the next job is due to be run. + +Upon waking: + - it runs each entry that is active on that second + - it calculates the next run times for the jobs that were run + - it re-sorts the array of entries by next activation time. + - it goes to sleep until the soonest job. +*/ +package cron diff --git a/vendor/github.com/robfig/cron/parser.go b/vendor/github.com/robfig/cron/parser.go new file mode 100644 index 0000000000000..a5e83c0a8dcea --- /dev/null +++ b/vendor/github.com/robfig/cron/parser.go @@ -0,0 +1,380 @@ +package cron + +import ( + "fmt" + "math" + "strconv" + "strings" + "time" +) + +// Configuration options for creating a parser. Most options specify which +// fields should be included, while others enable features. If a field is not +// included the parser will assume a default value. These options do not change +// the order fields are parse in. +type ParseOption int + +const ( + Second ParseOption = 1 << iota // Seconds field, default 0 + Minute // Minutes field, default 0 + Hour // Hours field, default 0 + Dom // Day of month field, default * + Month // Month field, default * + Dow // Day of week field, default * + DowOptional // Optional day of week field, default * + Descriptor // Allow descriptors such as @monthly, @weekly, etc. +) + +var places = []ParseOption{ + Second, + Minute, + Hour, + Dom, + Month, + Dow, +} + +var defaults = []string{ + "0", + "0", + "0", + "*", + "*", + "*", +} + +// A custom Parser that can be configured. +type Parser struct { + options ParseOption + optionals int +} + +// Creates a custom Parser with custom options. +// +// // Standard parser without descriptors +// specParser := NewParser(Minute | Hour | Dom | Month | Dow) +// sched, err := specParser.Parse("0 0 15 */3 *") +// +// // Same as above, just excludes time fields +// subsParser := NewParser(Dom | Month | Dow) +// sched, err := specParser.Parse("15 */3 *") +// +// // Same as above, just makes Dow optional +// subsParser := NewParser(Dom | Month | DowOptional) +// sched, err := specParser.Parse("15 */3") +// +func NewParser(options ParseOption) Parser { + optionals := 0 + if options&DowOptional > 0 { + options |= Dow + optionals++ + } + return Parser{options, optionals} +} + +// Parse returns a new crontab schedule representing the given spec. +// It returns a descriptive error if the spec is not valid. +// It accepts crontab specs and features configured by NewParser. +func (p Parser) Parse(spec string) (Schedule, error) { + if len(spec) == 0 { + return nil, fmt.Errorf("Empty spec string") + } + if spec[0] == '@' && p.options&Descriptor > 0 { + return parseDescriptor(spec) + } + + // Figure out how many fields we need + max := 0 + for _, place := range places { + if p.options&place > 0 { + max++ + } + } + min := max - p.optionals + + // Split fields on whitespace + fields := strings.Fields(spec) + + // Validate number of fields + if count := len(fields); count < min || count > max { + if min == max { + return nil, fmt.Errorf("Expected exactly %d fields, found %d: %s", min, count, spec) + } + return nil, fmt.Errorf("Expected %d to %d fields, found %d: %s", min, max, count, spec) + } + + // Fill in missing fields + fields = expandFields(fields, p.options) + + var err error + field := func(field string, r bounds) uint64 { + if err != nil { + return 0 + } + var bits uint64 + bits, err = getField(field, r) + return bits + } + + var ( + second = field(fields[0], seconds) + minute = field(fields[1], minutes) + hour = field(fields[2], hours) + dayofmonth = field(fields[3], dom) + month = field(fields[4], months) + dayofweek = field(fields[5], dow) + ) + if err != nil { + return nil, err + } + + return &SpecSchedule{ + Second: second, + Minute: minute, + Hour: hour, + Dom: dayofmonth, + Month: month, + Dow: dayofweek, + }, nil +} + +func expandFields(fields []string, options ParseOption) []string { + n := 0 + count := len(fields) + expFields := make([]string, len(places)) + copy(expFields, defaults) + for i, place := range places { + if options&place > 0 { + expFields[i] = fields[n] + n++ + } + if n == count { + break + } + } + return expFields +} + +var standardParser = NewParser( + Minute | Hour | Dom | Month | Dow | Descriptor, +) + +// ParseStandard returns a new crontab schedule representing the given standardSpec +// (https://en.wikipedia.org/wiki/Cron). It differs from Parse requiring to always +// pass 5 entries representing: minute, hour, day of month, month and day of week, +// in that order. It returns a descriptive error if the spec is not valid. +// +// It accepts +// - Standard crontab specs, e.g. "* * * * ?" +// - Descriptors, e.g. "@midnight", "@every 1h30m" +func ParseStandard(standardSpec string) (Schedule, error) { + return standardParser.Parse(standardSpec) +} + +var defaultParser = NewParser( + Second | Minute | Hour | Dom | Month | DowOptional | Descriptor, +) + +// Parse returns a new crontab schedule representing the given spec. +// It returns a descriptive error if the spec is not valid. +// +// It accepts +// - Full crontab specs, e.g. "* * * * * ?" +// - Descriptors, e.g. "@midnight", "@every 1h30m" +func Parse(spec string) (Schedule, error) { + return defaultParser.Parse(spec) +} + +// getField returns an Int with the bits set representing all of the times that +// the field represents or error parsing field value. A "field" is a comma-separated +// list of "ranges". +func getField(field string, r bounds) (uint64, error) { + var bits uint64 + ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' }) + for _, expr := range ranges { + bit, err := getRange(expr, r) + if err != nil { + return bits, err + } + bits |= bit + } + return bits, nil +} + +// getRange returns the bits indicated by the given expression: +// number | number "-" number [ "/" number ] +// or error parsing range. +func getRange(expr string, r bounds) (uint64, error) { + var ( + start, end, step uint + rangeAndStep = strings.Split(expr, "/") + lowAndHigh = strings.Split(rangeAndStep[0], "-") + singleDigit = len(lowAndHigh) == 1 + err error + ) + + var extra uint64 + if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" { + start = r.min + end = r.max + extra = starBit + } else { + start, err = parseIntOrName(lowAndHigh[0], r.names) + if err != nil { + return 0, err + } + switch len(lowAndHigh) { + case 1: + end = start + case 2: + end, err = parseIntOrName(lowAndHigh[1], r.names) + if err != nil { + return 0, err + } + default: + return 0, fmt.Errorf("Too many hyphens: %s", expr) + } + } + + switch len(rangeAndStep) { + case 1: + step = 1 + case 2: + step, err = mustParseInt(rangeAndStep[1]) + if err != nil { + return 0, err + } + + // Special handling: "N/step" means "N-max/step". + if singleDigit { + end = r.max + } + default: + return 0, fmt.Errorf("Too many slashes: %s", expr) + } + + if start < r.min { + return 0, fmt.Errorf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr) + } + if end > r.max { + return 0, fmt.Errorf("End of range (%d) above maximum (%d): %s", end, r.max, expr) + } + if start > end { + return 0, fmt.Errorf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr) + } + if step == 0 { + return 0, fmt.Errorf("Step of range should be a positive number: %s", expr) + } + + return getBits(start, end, step) | extra, nil +} + +// parseIntOrName returns the (possibly-named) integer contained in expr. +func parseIntOrName(expr string, names map[string]uint) (uint, error) { + if names != nil { + if namedInt, ok := names[strings.ToLower(expr)]; ok { + return namedInt, nil + } + } + return mustParseInt(expr) +} + +// mustParseInt parses the given expression as an int or returns an error. +func mustParseInt(expr string) (uint, error) { + num, err := strconv.Atoi(expr) + if err != nil { + return 0, fmt.Errorf("Failed to parse int from %s: %s", expr, err) + } + if num < 0 { + return 0, fmt.Errorf("Negative number (%d) not allowed: %s", num, expr) + } + + return uint(num), nil +} + +// getBits sets all bits in the range [min, max], modulo the given step size. +func getBits(min, max, step uint) uint64 { + var bits uint64 + + // If step is 1, use shifts. + if step == 1 { + return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min) + } + + // Else, use a simple loop. + for i := min; i <= max; i += step { + bits |= 1 << i + } + return bits +} + +// all returns all bits within the given bounds. (plus the star bit) +func all(r bounds) uint64 { + return getBits(r.min, r.max, 1) | starBit +} + +// parseDescriptor returns a predefined schedule for the expression, or error if none matches. +func parseDescriptor(descriptor string) (Schedule, error) { + switch descriptor { + case "@yearly", "@annually": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: 1 << dom.min, + Month: 1 << months.min, + Dow: all(dow), + }, nil + + case "@monthly": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: 1 << dom.min, + Month: all(months), + Dow: all(dow), + }, nil + + case "@weekly": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: all(dom), + Month: all(months), + Dow: 1 << dow.min, + }, nil + + case "@daily", "@midnight": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: all(dom), + Month: all(months), + Dow: all(dow), + }, nil + + case "@hourly": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: all(hours), + Dom: all(dom), + Month: all(months), + Dow: all(dow), + }, nil + } + + const every = "@every " + if strings.HasPrefix(descriptor, every) { + duration, err := time.ParseDuration(descriptor[len(every):]) + if err != nil { + return nil, fmt.Errorf("Failed to parse duration %s: %s", descriptor, err) + } + return Every(duration), nil + } + + return nil, fmt.Errorf("Unrecognized descriptor: %s", descriptor) +} diff --git a/vendor/github.com/robfig/cron/spec.go b/vendor/github.com/robfig/cron/spec.go new file mode 100644 index 0000000000000..aac9a60b95474 --- /dev/null +++ b/vendor/github.com/robfig/cron/spec.go @@ -0,0 +1,158 @@ +package cron + +import "time" + +// SpecSchedule specifies a duty cycle (to the second granularity), based on a +// traditional crontab specification. It is computed initially and stored as bit sets. +type SpecSchedule struct { + Second, Minute, Hour, Dom, Month, Dow uint64 +} + +// bounds provides a range of acceptable values (plus a map of name to value). +type bounds struct { + min, max uint + names map[string]uint +} + +// The bounds for each field. +var ( + seconds = bounds{0, 59, nil} + minutes = bounds{0, 59, nil} + hours = bounds{0, 23, nil} + dom = bounds{1, 31, nil} + months = bounds{1, 12, map[string]uint{ + "jan": 1, + "feb": 2, + "mar": 3, + "apr": 4, + "may": 5, + "jun": 6, + "jul": 7, + "aug": 8, + "sep": 9, + "oct": 10, + "nov": 11, + "dec": 12, + }} + dow = bounds{0, 6, map[string]uint{ + "sun": 0, + "mon": 1, + "tue": 2, + "wed": 3, + "thu": 4, + "fri": 5, + "sat": 6, + }} +) + +const ( + // Set the top bit if a star was included in the expression. + starBit = 1 << 63 +) + +// Next returns the next time this schedule is activated, greater than the given +// time. If no time can be found to satisfy the schedule, return the zero time. +func (s *SpecSchedule) Next(t time.Time) time.Time { + // General approach: + // For Month, Day, Hour, Minute, Second: + // Check if the time value matches. If yes, continue to the next field. + // If the field doesn't match the schedule, then increment the field until it matches. + // While incrementing the field, a wrap-around brings it back to the beginning + // of the field list (since it is necessary to re-verify previous field + // values) + + // Start at the earliest possible time (the upcoming second). + t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond) + + // This flag indicates whether a field has been incremented. + added := false + + // If no time is found within five years, return zero. + yearLimit := t.Year() + 5 + +WRAP: + if t.Year() > yearLimit { + return time.Time{} + } + + // Find the first applicable month. + // If it's this month, then do nothing. + for 1< 0 + dowMatch bool = 1< 0 + ) + if s.Dom&starBit > 0 || s.Dow&starBit > 0 { + return domMatch && dowMatch + } + return domMatch || dowMatch +} diff --git a/vendor/modules.txt b/vendor/modules.txt index d9e43fe1f5f2d..19e66874848b7 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -77,9 +77,6 @@ github.com/go-macaron/gzip github.com/go-macaron/inject # github.com/go-macaron/session v0.0.0-20190131233854-0a0a789bf193 github.com/go-macaron/session -github.com/go-macaron/session/memcache -github.com/go-macaron/session/postgres -github.com/go-macaron/session/redis # github.com/go-sql-driver/mysql v1.4.1 github.com/go-sql-driver/mysql # github.com/go-stack/stack v1.8.0 @@ -178,6 +175,8 @@ github.com/prometheus/procfs/xfs github.com/prometheus/procfs/internal/util # github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be github.com/rainycape/unidecode +# github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 +github.com/robfig/cron # github.com/sergi/go-diff v1.0.0 github.com/sergi/go-diff/diffmatchpatch # github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 diff --git a/yarn.lock b/yarn.lock index 21b9db2e0d7b9..277e62192ca77 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10011,11 +10011,6 @@ jest@24.6.0: import-local "^2.0.0" jest-cli "^24.6.0" -jquery@3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca" - integrity sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg== - jquery@3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.0.tgz#8de513fa0fa4b2c7d2e48a530e26f0596936efdf"