Просмотр исходного кода

Merge branch 'master' into wizard

Torkel Ödegaard 9 лет назад
Родитель
Сommit
75649c49d5
100 измененных файлов с 2209 добавлено и 680 удалено
  1. 32 1
      CHANGELOG.md
  2. 2 2
      Godeps/Godeps.json
  3. 4 1
      Godeps/_workspace/src/github.com/go-ldap/ldap/.travis.yml
  4. 22 15
      Godeps/_workspace/src/github.com/go-ldap/ldap/README.md
  5. 104 0
      Godeps/_workspace/src/github.com/go-ldap/ldap/add.go
  6. 23 0
      Godeps/_workspace/src/github.com/go-ldap/ldap/client.go
  7. 7 2
      Godeps/_workspace/src/github.com/go-ldap/ldap/conn.go
  8. 32 0
      Godeps/_workspace/src/github.com/go-ldap/ldap/control.go
  9. 79 0
      Godeps/_workspace/src/github.com/go-ldap/ldap/del.go
  10. 6 6
      Godeps/_workspace/src/github.com/go-ldap/ldap/dn.go
  11. 33 33
      Godeps/_workspace/src/github.com/go-ldap/ldap/dn_test.go
  12. 137 0
      Godeps/_workspace/src/github.com/go-ldap/ldap/error.go
  13. 2 2
      Godeps/_workspace/src/github.com/go-ldap/ldap/example_test.go
  14. 239 35
      Godeps/_workspace/src/github.com/go-ldap/ldap/filter.go
  15. 194 28
      Godeps/_workspace/src/github.com/go-ldap/ldap/filter_test.go
  16. 2 119
      Godeps/_workspace/src/github.com/go-ldap/ldap/ldap.go
  17. 53 25
      Godeps/_workspace/src/github.com/go-ldap/ldap/ldap_test.go
  18. 57 4
      Godeps/_workspace/src/github.com/go-ldap/ldap/search.go
  19. 31 0
      Godeps/_workspace/src/github.com/go-ldap/ldap/search_test.go
  20. 1 0
      Gruntfile.js
  21. 4 1
      README.md
  22. 1 1
      appveyor.yml
  23. 4 4
      bower.json
  24. 10 6
      build.go
  25. 23 0
      conf/ldap.toml
  26. 5 6
      docs/mkdocs.yml
  27. 16 2
      docs/sources/datasources/cloudwatch.md
  28. 9 2
      docs/sources/installation/debian.md
  29. 21 2
      docs/sources/installation/rpm.md
  30. 1 1
      docs/sources/installation/windows.md
  31. 0 46
      docs/sources/plugins/app.md
  32. 24 0
      docs/sources/plugins/apps.md
  33. 12 2
      docs/sources/plugins/datasources.md
  34. 16 11
      docs/sources/plugins/development.md
  35. 21 0
      docs/sources/plugins/index.md
  36. 6 8
      docs/sources/plugins/installation.md
  37. 0 12
      docs/sources/plugins/overview.md
  38. 5 16
      docs/sources/plugins/panels.md
  39. 1 0
      docs/sources/versions.html_fragment
  40. 0 3
      karma.conf.js
  41. 2 1
      latest.json
  42. 9 10
      package.json
  43. 16 11
      packaging/publish/publish.sh
  44. 17 0
      pkg/api/api.go
  45. 1 0
      pkg/api/avatar/avatar.go
  46. 18 0
      pkg/api/dashboard.go
  47. 5 0
      pkg/api/dtos/models.go
  48. 13 0
      pkg/api/dtos/prefs.go
  49. 1 1
      pkg/api/frontendsettings.go
  50. 9 1
      pkg/api/index.go
  51. 73 0
      pkg/api/preferences.go
  52. 18 0
      pkg/api/user.go
  53. 16 9
      pkg/cmd/grafana-cli/commands/commands.go
  54. 25 20
      pkg/cmd/grafana-cli/commands/install_command.go
  55. 2 1
      pkg/cmd/grafana-cli/commands/ls_command.go
  56. 2 1
      pkg/cmd/grafana-cli/commands/remove_command.go
  57. 4 4
      pkg/cmd/grafana-cli/commands/upgrade_all_command.go
  58. 3 3
      pkg/cmd/grafana-cli/commands/upgrade_command.go
  59. 34 13
      pkg/cmd/grafana-cli/main.go
  60. 8 7
      pkg/cmd/grafana-cli/services/services.go
  61. 6 1
      pkg/login/ldap.go
  62. 3 2
      pkg/login/settings.go
  63. 4 0
      pkg/metrics/report_usage.go
  64. 5 0
      pkg/models/dashboards.go
  65. 53 0
      pkg/models/preferences.go
  66. 8 9
      pkg/models/stats.go
  67. 0 1
      pkg/models/user.go
  68. 21 0
      pkg/services/sqlstore/dashboard.go
  69. 18 6
      pkg/services/sqlstore/migrations/preferences_mig.go
  70. 96 0
      pkg/services/sqlstore/preferences.go
  71. 1 6
      pkg/services/sqlstore/stats.go
  72. 0 1
      pkg/services/sqlstore/user.go
  73. 44 0
      public/app/core/components/dashboard_selector.ts
  74. 1 9
      public/app/core/components/info_popover.ts
  75. 1 1
      public/app/core/components/navbar/navbar.html
  76. 0 4
      public/app/core/components/sidemenu/sidemenu.html
  77. 2 9
      public/app/core/components/sidemenu/sidemenu.ts
  78. 74 0
      public/app/core/components/switch.ts
  79. 4 0
      public/app/core/core.ts
  80. 114 0
      public/app/core/directives/dropdown_typeahead.js
  81. 1 1
      public/app/core/directives/plugin_component.ts
  82. 9 5
      public/app/core/routes/dashboard_loaders.js
  83. 7 3
      public/app/core/routes/routes.ts
  84. 0 47
      public/app/core/services/context_srv.js
  85. 73 0
      public/app/core/services/context_srv.ts
  86. 4 1
      public/app/core/services/popover_srv.ts
  87. 1 1
      public/app/core/time_series2.ts
  88. 8 29
      public/app/core/utils/emitter.ts
  89. 0 4
      public/app/features/admin/partials/stats.html
  90. 0 3
      public/app/features/all.js
  91. 4 0
      public/app/features/dashboard/dashboardCtrl.js
  92. 64 4
      public/app/features/dashboard/dashboardSrv.js
  93. 34 23
      public/app/features/dashboard/partials/settings.html
  94. 24 28
      public/app/features/dashboard/partials/shareModal.html
  95. 2 2
      public/app/features/dashboard/submenu/submenu.html
  96. 2 2
      public/app/features/dashboard/timepicker/timepicker.ts
  97. 5 0
      public/app/features/org/all.js
  98. 0 0
      public/app/features/org/change_password_ctrl.js
  99. 0 0
      public/app/features/org/partials/change_password.html
  100. 1 0
      public/app/features/org/partials/orgDetails.html

+ 32 - 1
CHANGELOG.md

@@ -1,4 +1,34 @@
-# 3.0.0 (unrelased master branch)
+# 3.0.0-beta3 (unreleased)
+
+### Enhancements
+* **InfluxDB**: Changed multi query encoding to work with InfluxDB 0.11 & 0.12, closes [#4533](https://github.com/grafana/grafana/issues/4533)
+
+### Bug fixes
+* **Postgres**: Fixed page render crash when using postgres, fixes [#4558](https://github.com/grafana/grafana/issues/4558)
+* **Table panel**: Fixed table panel bug when trying to show annotations in table panel, fixes [#4563](https://github.com/grafana/grafana/issues/4563)
+* **App Config**: Fixed app config issue showing content of other app config, fixes [#4575](https://github.com/grafana/grafana/issues/4575)
+* **Graph Panel**: Fixed legend option max not updating, fixes [#4601](https://github.com/grafana/grafana/issues/4601)
+* **Graph Panel**: Fixed issue where newly added graph panels shared same axes config, fixes [#4582](https://github.com/grafana/grafana/issues/4582)
+* **Graph Panel**: Fixed issue with axis labels overlapping Y-axis, fixes [#4626](https://github.com/grafana/grafana/issues/4626)
+* **InfluxDB**: Fixed issue with templating query containing template variable, fixes [#4602](https://github.com/grafana/grafana/issues/4602)
+* **Graph Panel**: Fixed issue with hiding series and stacking, fixes [#4557](https://github.com/grafana/grafana/issues/4557)
+* **Graph Panel**: Fixed issue with legend height in table mode with few series, affected iframe embedding as well, fixes [#4640](https://github.com/grafana/grafana/issues/4640)
+
+# 3.0.0-beta2 (2016-04-04)
+
+### New Features (introduces since 3.0-beta1)
+* **Preferences**: Set home dashboard on user and org level, closes [#1678](https://github.com/grafana/grafana/issues/1678)
+* **Preferences**: Set timezone on user and org level, closes [#3214](https://github.com/grafana/grafana/issues/3214), [#1200](https://github.com/grafana/grafana/issues/1200)
+* **Preferences**: Set theme on user and org level, closes [#3214](https://github.com/grafana/grafana/issues/3214), [#1917](https://github.com/grafana/grafana/issues/1917)
+
+### Bug fixes
+* **Dashboard**: Fixed dashboard panel layout for mobile devices, fixes [#4529](https://github.com/grafana/grafana/issues/4529)
+* **Table Panel**: Fixed issue with table panel sort, fixes [#4532](https://github.com/grafana/grafana/issues/4532)
+* **Page Load Crash**: A Datasource with null jsonData would make Grafana fail to load page, fixes [#4536](https://github.com/grafana/grafana/issues/4536)
+* **Metrics tab**: Fix for missing datasource name in datasource selector, fixes [#4541](https://github.com/grafana/grafana/issues/4540)
+* **Graph**: Fix legend in table mode with series on right-y axis, fixes [#4551](https://github.com/grafana/grafana/issues/4551), [#1145](https://github.com/grafana/grafana/issues/1145)
+
+# 3.0.0-beta1 (2016-03-31)
 
 ### New Features
 * **Playlists**: Playlists can now be persisted and started from urls, closes [#3655](https://github.com/grafana/grafana/issues/3655)
@@ -14,6 +44,7 @@
 * **Plugin API**: Both datasource and panel plugin api (and plugin.json schema) have been updated, requiring an update to plugins. See [plugin api](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) for more info.
 * **InfluxDB 0.8.x** The data source for the old version of influxdb (0.8.x) is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3523](https://github.com/grafana/grafana/issues/3523)
 * **KairosDB** The data source is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3524](https://github.com/grafana/grafana/issues/3524)
+* **Templating**: Templating value formats (glob/regex/pipe etc) are now handled automatically and not specified by the user, this makes variable values possible to reuse in many contexts. It can in some edge cases break existing dashboards that have template variables that do not reload on dashboard load. To fix any issue just go into template variable options and update the variable (so it's values are reloaded.).
 
 ### Enhancements
 * **LDAP**: Support for nested LDAP Groups, closes [#4401](https://github.com/grafana/grafana/issues/4401), [#3808](https://github.com/grafana/grafana/issues/3808)

+ 2 - 2
Godeps/Godeps.json

@@ -160,8 +160,8 @@
 		},
 		{
 			"ImportPath": "github.com/go-ldap/ldap",
-			"Comment": "v1-19-g83e6542",
-			"Rev": "83e65426fd1c06626e88aa8a085e5bfed0208e29"
+			"Comment": "v2.2.1",
+			"Rev": "07a7330929b9ee80495c88a4439657d89c7dbd87"
 		},
 		{
 			"ImportPath": "github.com/go-macaron/binding",

+ 4 - 1
Godeps/_workspace/src/github.com/go-ldap/ldap/.travis.yml

@@ -2,10 +2,13 @@ language: go
 go:
     - 1.2
     - 1.3
+    - 1.4
+    - 1.5
     - tip
+go_import_path: gopkg.in/ldap.v2
 install:
     - go get gopkg.in/asn1-ber.v1
-    - go get gopkg.in/ldap.v1
+    - go get gopkg.in/ldap.v2
     - go get code.google.com/p/go.tools/cmd/cover || go get golang.org/x/tools/cmd/cover
     - go build -v ./...
 script:

+ 22 - 15
Godeps/_workspace/src/github.com/go-ldap/ldap/README.md

@@ -1,8 +1,20 @@
-[![GoDoc](https://godoc.org/gopkg.in/ldap.v1?status.svg)](https://godoc.org/gopkg.in/ldap.v1) [![Build Status](https://travis-ci.org/go-ldap/ldap.svg)](https://travis-ci.org/go-ldap/ldap)
+[![GoDoc](https://godoc.org/gopkg.in/ldap.v2?status.svg)](https://godoc.org/gopkg.in/ldap.v2)
+[![Build Status](https://travis-ci.org/go-ldap/ldap.svg)](https://travis-ci.org/go-ldap/ldap)
 
 # Basic LDAP v3 functionality for the GO programming language.
 
-## Required Librarys: 
+## Install
+
+For the latest version use:
+
+    go get gopkg.in/ldap.v2
+
+Import the latest version with:
+
+    import "gopkg.in/ldap.v2"
+
+
+## Required Libraries:
 
  - gopkg.in/asn1-ber.v1
 
@@ -14,6 +26,9 @@
  - Compiling string filters to LDAP filters
  - Paging Search Results
  - Modify Requests / Responses
+ - Add Requests / Responses
+ - Delete Requests / Responses
+ - Better Unicode support
 
 ## Examples:
 
@@ -26,23 +41,15 @@
 
 ## TODO:
 
- - Add Requests / Responses
- - Delete Requests / Responses
- - Modify DN Requests / Responses
- - Compare Requests / Responses
- - Implement Tests / Benchmarks
+ - [x] Add Requests / Responses
+ - [x] Delete Requests / Responses
+ - [x] Modify DN Requests / Responses
+ - [ ] Compare Requests / Responses
+ - [ ] Implement Tests / Benchmarks
 
----
-This feature is disabled at the moment, because in some cases the "Search Request Done" packet will be handled before the last "Search Request Entry":
 
- - Mulitple internal goroutines to handle network traffic
-        Makes library goroutine safe
-        Can perform multiple search requests at the same time and return
-        the results to the proper goroutine. All requests are blocking requests,
-        so the goroutine does not need special handling
 
 ---
-
 The Go gopher was designed by Renee French. (http://reneefrench.blogspot.com/)
 The design is licensed under the Creative Commons 3.0 Attributions license.
 Read this article for more details: http://blog.golang.org/gopher

+ 104 - 0
Godeps/_workspace/src/github.com/go-ldap/ldap/add.go

@@ -0,0 +1,104 @@
+//
+// https://tools.ietf.org/html/rfc4511
+//
+// AddRequest ::= [APPLICATION 8] SEQUENCE {
+//      entry           LDAPDN,
+//      attributes      AttributeList }
+//
+// AttributeList ::= SEQUENCE OF attribute Attribute
+
+package ldap
+
+import (
+	"errors"
+	"log"
+
+	"gopkg.in/asn1-ber.v1"
+)
+
+type Attribute struct {
+	attrType string
+	attrVals []string
+}
+
+func (a *Attribute) encode() *ber.Packet {
+	seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attribute")
+	seq.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, a.attrType, "Type"))
+	set := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "AttributeValue")
+	for _, value := range a.attrVals {
+		set.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Vals"))
+	}
+	seq.AppendChild(set)
+	return seq
+}
+
+type AddRequest struct {
+	dn         string
+	attributes []Attribute
+}
+
+func (a AddRequest) encode() *ber.Packet {
+	request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationAddRequest, nil, "Add Request")
+	request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, a.dn, "DN"))
+	attributes := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes")
+	for _, attribute := range a.attributes {
+		attributes.AppendChild(attribute.encode())
+	}
+	request.AppendChild(attributes)
+	return request
+}
+
+func (a *AddRequest) Attribute(attrType string, attrVals []string) {
+	a.attributes = append(a.attributes, Attribute{attrType: attrType, attrVals: attrVals})
+}
+
+func NewAddRequest(dn string) *AddRequest {
+	return &AddRequest{
+		dn: dn,
+	}
+
+}
+
+func (l *Conn) Add(addRequest *AddRequest) error {
+	messageID := l.nextMessageID()
+	packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request")
+	packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "MessageID"))
+	packet.AppendChild(addRequest.encode())
+
+	l.Debug.PrintPacket(packet)
+
+	channel, err := l.sendMessage(packet)
+	if err != nil {
+		return err
+	}
+	if channel == nil {
+		return NewError(ErrorNetwork, errors.New("ldap: could not send message"))
+	}
+	defer l.finishMessage(messageID)
+
+	l.Debug.Printf("%d: waiting for response", messageID)
+	packet = <-channel
+	l.Debug.Printf("%d: got response %p", messageID, packet)
+	if packet == nil {
+		return NewError(ErrorNetwork, errors.New("ldap: could not retrieve message"))
+	}
+
+	if l.Debug {
+		if err := addLDAPDescriptions(packet); err != nil {
+			return err
+		}
+		ber.PrintPacket(packet)
+	}
+
+	if packet.Children[1].Tag == ApplicationAddResponse {
+		resultCode, resultDescription := getLDAPResultCode(packet)
+		if resultCode != 0 {
+			return NewError(resultCode, errors.New(resultDescription))
+		}
+	} else {
+		log.Printf("Unexpected Response: %d", packet.Children[1].Tag)
+	}
+
+	l.Debug.Printf("%d: returning", messageID)
+	return nil
+}

+ 23 - 0
Godeps/_workspace/src/github.com/go-ldap/ldap/client.go

@@ -0,0 +1,23 @@
+package ldap
+
+import "crypto/tls"
+
+// Client knows how to interact with an LDAP server
+type Client interface {
+	Start()
+	StartTLS(config *tls.Config) error
+	Close()
+
+	Bind(username, password string) error
+	SimpleBind(simpleBindRequest *SimpleBindRequest) (*SimpleBindResult, error)
+
+	Add(addRequest *AddRequest) error
+	Del(delRequest *DelRequest) error
+	Modify(modifyRequest *ModifyRequest) error
+
+	Compare(dn, attribute, value string) (bool, error)
+	PasswordModify(passwordModifyRequest *PasswordModifyRequest) (*PasswordModifyResult, error)
+
+	Search(searchRequest *SearchRequest) (*SearchResult, error)
+	SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error)
+}

+ 7 - 2
Godeps/_workspace/src/github.com/go-ldap/ldap/conn.go

@@ -8,11 +8,12 @@ import (
 	"crypto/tls"
 	"errors"
 	"fmt"
-	"gopkg.in/asn1-ber.v1"
 	"log"
 	"net"
 	"sync"
 	"time"
+
+	"gopkg.in/asn1-ber.v1"
 )
 
 const (
@@ -53,6 +54,8 @@ type Conn struct {
 	messageMutex        sync.Mutex
 }
 
+var _ Client = &Conn{}
+
 // DefaultTimeout is a package-level variable that sets the timeout value
 // used for the Dial and DialTLS methods.
 //
@@ -176,7 +179,7 @@ func (l *Conn) StartTLS(config *tls.Config) error {
 		ber.PrintPacket(packet)
 	}
 
-	if packet.Children[1].Children[0].Value.(int64) == 0 {
+	if resultCode, message := getLDAPResultCode(packet); resultCode == LDAPResultSuccess {
 		conn := tls.Client(l.conn, config)
 
 		if err := conn.Handshake(); err != nil {
@@ -186,6 +189,8 @@ func (l *Conn) StartTLS(config *tls.Config) error {
 
 		l.isTLS = true
 		l.conn = conn
+	} else {
+		return NewError(resultCode, fmt.Errorf("ldap: cannot StartTLS (%s)", message))
 	}
 	go l.reader()
 

+ 32 - 0
Godeps/_workspace/src/github.com/go-ldap/ldap/control.go

@@ -16,11 +16,13 @@ const (
 	ControlTypeBeheraPasswordPolicy   = "1.3.6.1.4.1.42.2.27.8.5.1"
 	ControlTypeVChuPasswordMustChange = "2.16.840.1.113730.3.4.4"
 	ControlTypeVChuPasswordWarning    = "2.16.840.1.113730.3.4.5"
+	ControlTypeManageDsaIT            = "2.16.840.1.113730.3.4.2"
 )
 
 var ControlTypeMap = map[string]string{
 	ControlTypePaging:               "Paging",
 	ControlTypeBeheraPasswordPolicy: "Password Policy - Behera Draft",
+	ControlTypeManageDsaIT:          "Manage DSA IT",
 }
 
 type Control interface {
@@ -165,6 +167,36 @@ func (c *ControlVChuPasswordWarning) String() string {
 		c.Expire)
 }
 
+type ControlManageDsaIT struct {
+	Criticality bool
+}
+
+func (c *ControlManageDsaIT) GetControlType() string {
+	return ControlTypeManageDsaIT
+}
+
+func (c *ControlManageDsaIT) Encode() *ber.Packet {
+	//FIXME
+	packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control")
+	packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeManageDsaIT, "Control Type ("+ControlTypeMap[ControlTypeManageDsaIT]+")"))
+	if c.Criticality {
+		packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality"))
+	}
+	return packet
+}
+
+func (c *ControlManageDsaIT) String() string {
+	return fmt.Sprintf(
+		"Control Type: %s (%q)  Criticality: %t",
+		ControlTypeMap[ControlTypeManageDsaIT],
+		ControlTypeManageDsaIT,
+		c.Criticality)
+}
+
+func NewControlManageDsaIT(Criticality bool) *ControlManageDsaIT {
+	return &ControlManageDsaIT{Criticality: Criticality}
+}
+
 func FindControl(controls []Control, controlType string) Control {
 	for _, c := range controls {
 		if c.GetControlType() == controlType {

+ 79 - 0
Godeps/_workspace/src/github.com/go-ldap/ldap/del.go

@@ -0,0 +1,79 @@
+//
+// https://tools.ietf.org/html/rfc4511
+//
+// DelRequest ::= [APPLICATION 10] LDAPDN
+
+package ldap
+
+import (
+	"errors"
+	"log"
+
+	"gopkg.in/asn1-ber.v1"
+)
+
+type DelRequest struct {
+	DN       string
+	Controls []Control
+}
+
+func (d DelRequest) encode() *ber.Packet {
+	request := ber.Encode(ber.ClassApplication, ber.TypePrimitive, ApplicationDelRequest, d.DN, "Del Request")
+	request.Data.Write([]byte(d.DN))
+	return request
+}
+
+func NewDelRequest(DN string,
+	Controls []Control) *DelRequest {
+	return &DelRequest{
+		DN:       DN,
+		Controls: Controls,
+	}
+}
+
+func (l *Conn) Del(delRequest *DelRequest) error {
+	messageID := l.nextMessageID()
+	packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request")
+	packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, messageID, "MessageID"))
+	packet.AppendChild(delRequest.encode())
+	if delRequest.Controls != nil {
+		packet.AppendChild(encodeControls(delRequest.Controls))
+	}
+
+	l.Debug.PrintPacket(packet)
+
+	channel, err := l.sendMessage(packet)
+	if err != nil {
+		return err
+	}
+	if channel == nil {
+		return NewError(ErrorNetwork, errors.New("ldap: could not send message"))
+	}
+	defer l.finishMessage(messageID)
+
+	l.Debug.Printf("%d: waiting for response", messageID)
+	packet = <-channel
+	l.Debug.Printf("%d: got response %p", messageID, packet)
+	if packet == nil {
+		return NewError(ErrorNetwork, errors.New("ldap: could not retrieve message"))
+	}
+
+	if l.Debug {
+		if err := addLDAPDescriptions(packet); err != nil {
+			return err
+		}
+		ber.PrintPacket(packet)
+	}
+
+	if packet.Children[1].Tag == ApplicationDelResponse {
+		resultCode, resultDescription := getLDAPResultCode(packet)
+		if resultCode != 0 {
+			return NewError(resultCode, errors.New(resultDescription))
+		}
+	} else {
+		log.Printf("Unexpected Response: %d", packet.Children[1].Tag)
+	}
+
+	l.Debug.Printf("%d: returning", messageID)
+	return nil
+}

+ 6 - 6
Godeps/_workspace/src/github.com/go-ldap/ldap/dn.go

@@ -47,17 +47,17 @@ package ldap
 
 import (
 	"bytes"
+	enchex "encoding/hex"
 	"errors"
 	"fmt"
 	"strings"
-	enchex "encoding/hex"
 
 	ber "gopkg.in/asn1-ber.v1"
 )
 
 type AttributeTypeAndValue struct {
-	Type       string
-	Value      string
+	Type  string
+	Value string
 }
 
 type RelativeDN struct {
@@ -71,7 +71,7 @@ type DN struct {
 func ParseDN(str string) (*DN, error) {
 	dn := new(DN)
 	dn.RDNs = make([]*RelativeDN, 0)
-	rdn := new (RelativeDN)
+	rdn := new(RelativeDN)
 	rdn.Attributes = make([]*AttributeTypeAndValue, 0)
 	buffer := bytes.Buffer{}
 	attribute := new(AttributeTypeAndValue)
@@ -115,7 +115,7 @@ func ParseDN(str string) (*DN, error) {
 				index := strings.IndexAny(str[i:], ",+")
 				data := str
 				if index > 0 {
-				  data = str[i:i+index]
+					data = str[i : i+index]
 				} else {
 					data = str[i:]
 				}
@@ -126,7 +126,7 @@ func ParseDN(str string) (*DN, error) {
 				}
 				packet := ber.DecodePacket(raw_ber)
 				buffer.WriteString(packet.Data.String())
-				i += len(data)-1
+				i += len(data) - 1
 			}
 		} else if char == ',' || char == '+' {
 			// We're done with this RDN or value, push it

+ 33 - 33
Godeps/_workspace/src/github.com/go-ldap/ldap/dn_test.go

@@ -1,38 +1,40 @@
-package ldap
+package ldap_test
 
 import (
 	"reflect"
 	"testing"
+
+	"gopkg.in/ldap.v2"
 )
 
 func TestSuccessfulDNParsing(t *testing.T) {
-	testcases := map[string]DN {
-		"": DN{[]*RelativeDN{}},
-		"cn=Jim\\2C \\22Hasse Hö\\22 Hansson!,dc=dummy,dc=com": DN{[]*RelativeDN{
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"cn", "Jim, \"Hasse Hö\" Hansson!"},}},
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"dc", "dummy"},}},
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"dc", "com"}, }},}},
-		"UID=jsmith,DC=example,DC=net": DN{[]*RelativeDN{
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"UID", "jsmith"},}},
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "example"},}},
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "net"}, }},}},
-		"OU=Sales+CN=J. Smith,DC=example,DC=net": DN{[]*RelativeDN{
-			&RelativeDN{[]*AttributeTypeAndValue{
-				&AttributeTypeAndValue{"OU", "Sales"},
-				&AttributeTypeAndValue{"CN", "J. Smith"},}},
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "example"},}},
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "net"}, }},}},
-		"1.3.6.1.4.1.1466.0=#04024869": DN{[]*RelativeDN{
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"},}},}},
-		"1.3.6.1.4.1.1466.0=#04024869,DC=net": DN{[]*RelativeDN{
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"},}},
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"DC", "net"}, }},}},
-		"CN=Lu\\C4\\8Di\\C4\\87": DN{[]*RelativeDN{
-			&RelativeDN{[]*AttributeTypeAndValue{&AttributeTypeAndValue{"CN", "Lučić"},}},}},
+	testcases := map[string]ldap.DN{
+		"": ldap.DN{[]*ldap.RelativeDN{}},
+		"cn=Jim\\2C \\22Hasse Hö\\22 Hansson!,dc=dummy,dc=com": ldap.DN{[]*ldap.RelativeDN{
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"cn", "Jim, \"Hasse Hö\" Hansson!"}}},
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"dc", "dummy"}}},
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"dc", "com"}}}}},
+		"UID=jsmith,DC=example,DC=net": ldap.DN{[]*ldap.RelativeDN{
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"UID", "jsmith"}}},
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "example"}}},
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "net"}}}}},
+		"OU=Sales+CN=J. Smith,DC=example,DC=net": ldap.DN{[]*ldap.RelativeDN{
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{
+				&ldap.AttributeTypeAndValue{"OU", "Sales"},
+				&ldap.AttributeTypeAndValue{"CN", "J. Smith"}}},
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "example"}}},
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "net"}}}}},
+		"1.3.6.1.4.1.1466.0=#04024869": ldap.DN{[]*ldap.RelativeDN{
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"}}}}},
+		"1.3.6.1.4.1.1466.0=#04024869,DC=net": ldap.DN{[]*ldap.RelativeDN{
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"1.3.6.1.4.1.1466.0", "Hi"}}},
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"DC", "net"}}}}},
+		"CN=Lu\\C4\\8Di\\C4\\87": ldap.DN{[]*ldap.RelativeDN{
+			&ldap.RelativeDN{[]*ldap.AttributeTypeAndValue{&ldap.AttributeTypeAndValue{"CN", "Lučić"}}}}},
 	}
 
 	for test, answer := range testcases {
-		dn, err := ParseDN(test)
+		dn, err := ldap.ParseDN(test)
 		if err != nil {
 			t.Errorf(err.Error())
 			continue
@@ -49,16 +51,16 @@ func TestSuccessfulDNParsing(t *testing.T) {
 }
 
 func TestErrorDNParsing(t *testing.T) {
-	testcases := map[string]string {
-		"*": "DN ended with incomplete type, value pair",
-		"cn=Jim\\0Test": "Failed to decode escaped character: encoding/hex: invalid byte: U+0054 'T'",
-		"cn=Jim\\0": "Got corrupted escaped character",
+	testcases := map[string]string{
+		"*":               "DN ended with incomplete type, value pair",
+		"cn=Jim\\0Test":   "Failed to decode escaped character: encoding/hex: invalid byte: U+0054 'T'",
+		"cn=Jim\\0":       "Got corrupted escaped character",
 		"DC=example,=net": "DN ended with incomplete type, value pair",
-		"1=#0402486": "Failed to decode BER encoding: encoding/hex: odd length hex string",
+		"1=#0402486":      "Failed to decode BER encoding: encoding/hex: odd length hex string",
 	}
 
 	for test, answer := range testcases {
-		_, err := ParseDN(test)
+		_, err := ldap.ParseDN(test)
 		if err == nil {
 			t.Errorf("Expected %s to fail parsing but succeeded\n", test)
 		} else if err.Error() != answer {
@@ -66,5 +68,3 @@ func TestErrorDNParsing(t *testing.T) {
 		}
 	}
 }
-
-

+ 137 - 0
Godeps/_workspace/src/github.com/go-ldap/ldap/error.go

@@ -0,0 +1,137 @@
+package ldap
+
+import (
+	"fmt"
+
+	"gopkg.in/asn1-ber.v1"
+)
+
+// LDAP Result Codes
+const (
+	LDAPResultSuccess                      = 0
+	LDAPResultOperationsError              = 1
+	LDAPResultProtocolError                = 2
+	LDAPResultTimeLimitExceeded            = 3
+	LDAPResultSizeLimitExceeded            = 4
+	LDAPResultCompareFalse                 = 5
+	LDAPResultCompareTrue                  = 6
+	LDAPResultAuthMethodNotSupported       = 7
+	LDAPResultStrongAuthRequired           = 8
+	LDAPResultReferral                     = 10
+	LDAPResultAdminLimitExceeded           = 11
+	LDAPResultUnavailableCriticalExtension = 12
+	LDAPResultConfidentialityRequired      = 13
+	LDAPResultSaslBindInProgress           = 14
+	LDAPResultNoSuchAttribute              = 16
+	LDAPResultUndefinedAttributeType       = 17
+	LDAPResultInappropriateMatching        = 18
+	LDAPResultConstraintViolation          = 19
+	LDAPResultAttributeOrValueExists       = 20
+	LDAPResultInvalidAttributeSyntax       = 21
+	LDAPResultNoSuchObject                 = 32
+	LDAPResultAliasProblem                 = 33
+	LDAPResultInvalidDNSyntax              = 34
+	LDAPResultAliasDereferencingProblem    = 36
+	LDAPResultInappropriateAuthentication  = 48
+	LDAPResultInvalidCredentials           = 49
+	LDAPResultInsufficientAccessRights     = 50
+	LDAPResultBusy                         = 51
+	LDAPResultUnavailable                  = 52
+	LDAPResultUnwillingToPerform           = 53
+	LDAPResultLoopDetect                   = 54
+	LDAPResultNamingViolation              = 64
+	LDAPResultObjectClassViolation         = 65
+	LDAPResultNotAllowedOnNonLeaf          = 66
+	LDAPResultNotAllowedOnRDN              = 67
+	LDAPResultEntryAlreadyExists           = 68
+	LDAPResultObjectClassModsProhibited    = 69
+	LDAPResultAffectsMultipleDSAs          = 71
+	LDAPResultOther                        = 80
+
+	ErrorNetwork            = 200
+	ErrorFilterCompile      = 201
+	ErrorFilterDecompile    = 202
+	ErrorDebugging          = 203
+	ErrorUnexpectedMessage  = 204
+	ErrorUnexpectedResponse = 205
+)
+
+var LDAPResultCodeMap = map[uint8]string{
+	LDAPResultSuccess:                      "Success",
+	LDAPResultOperationsError:              "Operations Error",
+	LDAPResultProtocolError:                "Protocol Error",
+	LDAPResultTimeLimitExceeded:            "Time Limit Exceeded",
+	LDAPResultSizeLimitExceeded:            "Size Limit Exceeded",
+	LDAPResultCompareFalse:                 "Compare False",
+	LDAPResultCompareTrue:                  "Compare True",
+	LDAPResultAuthMethodNotSupported:       "Auth Method Not Supported",
+	LDAPResultStrongAuthRequired:           "Strong Auth Required",
+	LDAPResultReferral:                     "Referral",
+	LDAPResultAdminLimitExceeded:           "Admin Limit Exceeded",
+	LDAPResultUnavailableCriticalExtension: "Unavailable Critical Extension",
+	LDAPResultConfidentialityRequired:      "Confidentiality Required",
+	LDAPResultSaslBindInProgress:           "Sasl Bind In Progress",
+	LDAPResultNoSuchAttribute:              "No Such Attribute",
+	LDAPResultUndefinedAttributeType:       "Undefined Attribute Type",
+	LDAPResultInappropriateMatching:        "Inappropriate Matching",
+	LDAPResultConstraintViolation:          "Constraint Violation",
+	LDAPResultAttributeOrValueExists:       "Attribute Or Value Exists",
+	LDAPResultInvalidAttributeSyntax:       "Invalid Attribute Syntax",
+	LDAPResultNoSuchObject:                 "No Such Object",
+	LDAPResultAliasProblem:                 "Alias Problem",
+	LDAPResultInvalidDNSyntax:              "Invalid DN Syntax",
+	LDAPResultAliasDereferencingProblem:    "Alias Dereferencing Problem",
+	LDAPResultInappropriateAuthentication:  "Inappropriate Authentication",
+	LDAPResultInvalidCredentials:           "Invalid Credentials",
+	LDAPResultInsufficientAccessRights:     "Insufficient Access Rights",
+	LDAPResultBusy:                         "Busy",
+	LDAPResultUnavailable:                  "Unavailable",
+	LDAPResultUnwillingToPerform:           "Unwilling To Perform",
+	LDAPResultLoopDetect:                   "Loop Detect",
+	LDAPResultNamingViolation:              "Naming Violation",
+	LDAPResultObjectClassViolation:         "Object Class Violation",
+	LDAPResultNotAllowedOnNonLeaf:          "Not Allowed On Non Leaf",
+	LDAPResultNotAllowedOnRDN:              "Not Allowed On RDN",
+	LDAPResultEntryAlreadyExists:           "Entry Already Exists",
+	LDAPResultObjectClassModsProhibited:    "Object Class Mods Prohibited",
+	LDAPResultAffectsMultipleDSAs:          "Affects Multiple DSAs",
+	LDAPResultOther:                        "Other",
+}
+
+func getLDAPResultCode(packet *ber.Packet) (code uint8, description string) {
+	if len(packet.Children) >= 2 {
+		response := packet.Children[1]
+		if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) >= 3 {
+			// Children[1].Children[2] is the diagnosticMessage which is guaranteed to exist as seen here: https://tools.ietf.org/html/rfc4511#section-4.1.9
+			return uint8(response.Children[0].Value.(int64)), response.Children[2].Value.(string)
+		}
+	}
+
+	return ErrorNetwork, "Invalid packet format"
+}
+
+type Error struct {
+	Err        error
+	ResultCode uint8
+}
+
+func (e *Error) Error() string {
+	return fmt.Sprintf("LDAP Result Code %d %q: %s", e.ResultCode, LDAPResultCodeMap[e.ResultCode], e.Err.Error())
+}
+
+func NewError(resultCode uint8, err error) error {
+	return &Error{ResultCode: resultCode, Err: err}
+}
+
+func IsErrorWithCode(err error, desiredResultCode uint8) bool {
+	if err == nil {
+		return false
+	}
+
+	serverError, ok := err.(*Error)
+	if !ok {
+		return false
+	}
+
+	return serverError.ResultCode == desiredResultCode
+}

+ 2 - 2
Godeps/_workspace/src/github.com/go-ldap/ldap/example_test.go

@@ -5,10 +5,10 @@ import (
 	"fmt"
 	"log"
 
-	"github.com/go-ldap/ldap"
+	"gopkg.in/ldap.v2"
 )
 
-// ExampleConn_Bind demonstrats how to bind a connection to an ldap user
+// ExampleConn_Bind demonstrates how to bind a connection to an ldap user
 // allowing access to restricted attrabutes that user has access to
 func ExampleConn_Bind() {
 	l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389))

+ 239 - 35
Godeps/_workspace/src/github.com/go-ldap/ldap/filter.go

@@ -5,9 +5,12 @@
 package ldap
 
 import (
+	"bytes"
+	hexpac "encoding/hex"
 	"errors"
 	"fmt"
 	"strings"
+	"unicode/utf8"
 
 	"gopkg.in/asn1-ber.v1"
 )
@@ -50,6 +53,20 @@ var FilterSubstringsMap = map[uint64]string{
 	FilterSubstringsFinal:   "Substrings Final",
 }
 
+const (
+	MatchingRuleAssertionMatchingRule = 1
+	MatchingRuleAssertionType         = 2
+	MatchingRuleAssertionMatchValue   = 3
+	MatchingRuleAssertionDNAttributes = 4
+)
+
+var MatchingRuleAssertionMap = map[uint64]string{
+	MatchingRuleAssertionMatchingRule: "Matching Rule Assertion Matching Rule",
+	MatchingRuleAssertionType:         "Matching Rule Assertion Type",
+	MatchingRuleAssertionMatchValue:   "Matching Rule Assertion Match Value",
+	MatchingRuleAssertionDNAttributes: "Matching Rule Assertion DN Attributes",
+}
+
 func CompileFilter(filter string) (*ber.Packet, error) {
 	if len(filter) == 0 || filter[0] != '(' {
 		return nil, NewError(ErrorFilterCompile, errors.New("ldap: filter does not start with an '('"))
@@ -108,7 +125,7 @@ func DecompileFilter(packet *ber.Packet) (ret string, err error) {
 			if i == 0 && child.Tag != FilterSubstringsInitial {
 				ret += "*"
 			}
-			ret += ber.DecodeString(child.Data.Bytes())
+			ret += EscapeFilter(ber.DecodeString(child.Data.Bytes()))
 			if child.Tag != FilterSubstringsFinal {
 				ret += "*"
 			}
@@ -116,22 +133,53 @@ func DecompileFilter(packet *ber.Packet) (ret string, err error) {
 	case FilterEqualityMatch:
 		ret += ber.DecodeString(packet.Children[0].Data.Bytes())
 		ret += "="
-		ret += ber.DecodeString(packet.Children[1].Data.Bytes())
+		ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes()))
 	case FilterGreaterOrEqual:
 		ret += ber.DecodeString(packet.Children[0].Data.Bytes())
 		ret += ">="
-		ret += ber.DecodeString(packet.Children[1].Data.Bytes())
+		ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes()))
 	case FilterLessOrEqual:
 		ret += ber.DecodeString(packet.Children[0].Data.Bytes())
 		ret += "<="
-		ret += ber.DecodeString(packet.Children[1].Data.Bytes())
+		ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes()))
 	case FilterPresent:
 		ret += ber.DecodeString(packet.Data.Bytes())
 		ret += "=*"
 	case FilterApproxMatch:
 		ret += ber.DecodeString(packet.Children[0].Data.Bytes())
 		ret += "~="
-		ret += ber.DecodeString(packet.Children[1].Data.Bytes())
+		ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes()))
+	case FilterExtensibleMatch:
+		attr := ""
+		dnAttributes := false
+		matchingRule := ""
+		value := ""
+
+		for _, child := range packet.Children {
+			switch child.Tag {
+			case MatchingRuleAssertionMatchingRule:
+				matchingRule = ber.DecodeString(child.Data.Bytes())
+			case MatchingRuleAssertionType:
+				attr = ber.DecodeString(child.Data.Bytes())
+			case MatchingRuleAssertionMatchValue:
+				value = ber.DecodeString(child.Data.Bytes())
+			case MatchingRuleAssertionDNAttributes:
+				dnAttributes = child.Value.(bool)
+			}
+		}
+
+		if len(attr) > 0 {
+			ret += attr
+		}
+		if dnAttributes {
+			ret += ":dn"
+		}
+		if len(matchingRule) > 0 {
+			ret += ":"
+			ret += matchingRule
+		}
+		ret += ":="
+		ret += EscapeFilter(value)
 	}
 
 	ret += ")"
@@ -155,58 +203,143 @@ func compileFilterSet(filter string, pos int, parent *ber.Packet) (int, error) {
 }
 
 func compileFilter(filter string, pos int) (*ber.Packet, int, error) {
-	var packet *ber.Packet
-	var err error
+	var (
+		packet *ber.Packet
+		err    error
+	)
 
 	defer func() {
 		if r := recover(); r != nil {
 			err = NewError(ErrorFilterCompile, errors.New("ldap: error compiling filter"))
 		}
 	}()
-
 	newPos := pos
-	switch filter[pos] {
+
+	currentRune, currentWidth := utf8.DecodeRuneInString(filter[newPos:])
+
+	switch currentRune {
+	case utf8.RuneError:
+		return nil, 0, NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", newPos))
 	case '(':
-		packet, newPos, err = compileFilter(filter, pos+1)
+		packet, newPos, err = compileFilter(filter, pos+currentWidth)
 		newPos++
 		return packet, newPos, err
 	case '&':
 		packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterAnd, nil, FilterMap[FilterAnd])
-		newPos, err = compileFilterSet(filter, pos+1, packet)
+		newPos, err = compileFilterSet(filter, pos+currentWidth, packet)
 		return packet, newPos, err
 	case '|':
 		packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterOr, nil, FilterMap[FilterOr])
-		newPos, err = compileFilterSet(filter, pos+1, packet)
+		newPos, err = compileFilterSet(filter, pos+currentWidth, packet)
 		return packet, newPos, err
 	case '!':
 		packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterNot, nil, FilterMap[FilterNot])
 		var child *ber.Packet
-		child, newPos, err = compileFilter(filter, pos+1)
+		child, newPos, err = compileFilter(filter, pos+currentWidth)
 		packet.AppendChild(child)
 		return packet, newPos, err
 	default:
+		READING_ATTR := 0
+		READING_EXTENSIBLE_MATCHING_RULE := 1
+		READING_CONDITION := 2
+
+		state := READING_ATTR
+
 		attribute := ""
+		extensibleDNAttributes := false
+		extensibleMatchingRule := ""
 		condition := ""
-		for newPos < len(filter) && filter[newPos] != ')' {
-			switch {
-			case packet != nil:
-				condition += fmt.Sprintf("%c", filter[newPos])
-			case filter[newPos] == '=':
-				packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterEqualityMatch, nil, FilterMap[FilterEqualityMatch])
-			case filter[newPos] == '>' && filter[newPos+1] == '=':
-				packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterGreaterOrEqual, nil, FilterMap[FilterGreaterOrEqual])
-				newPos++
-			case filter[newPos] == '<' && filter[newPos+1] == '=':
-				packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterLessOrEqual, nil, FilterMap[FilterLessOrEqual])
-				newPos++
-			case filter[newPos] == '~' && filter[newPos+1] == '=':
-				packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterApproxMatch, nil, FilterMap[FilterLessOrEqual])
-				newPos++
-			case packet == nil:
-				attribute += fmt.Sprintf("%c", filter[newPos])
-			}
-			newPos++
+
+		for newPos < len(filter) {
+			remainingFilter := filter[newPos:]
+			currentRune, currentWidth = utf8.DecodeRuneInString(remainingFilter)
+			if currentRune == ')' {
+				break
+			}
+			if currentRune == utf8.RuneError {
+				return packet, newPos, NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", newPos))
+			}
+
+			switch state {
+			case READING_ATTR:
+				switch {
+				// Extensible rule, with only DN-matching
+				case currentRune == ':' && strings.HasPrefix(remainingFilter, ":dn:="):
+					packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch])
+					extensibleDNAttributes = true
+					state = READING_CONDITION
+					newPos += 5
+
+				// Extensible rule, with DN-matching and a matching OID
+				case currentRune == ':' && strings.HasPrefix(remainingFilter, ":dn:"):
+					packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch])
+					extensibleDNAttributes = true
+					state = READING_EXTENSIBLE_MATCHING_RULE
+					newPos += 4
+
+				// Extensible rule, with attr only
+				case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="):
+					packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch])
+					state = READING_CONDITION
+					newPos += 2
+
+				// Extensible rule, with no DN attribute matching
+				case currentRune == ':':
+					packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch])
+					state = READING_EXTENSIBLE_MATCHING_RULE
+					newPos += 1
+
+				// Equality condition
+				case currentRune == '=':
+					packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterEqualityMatch, nil, FilterMap[FilterEqualityMatch])
+					state = READING_CONDITION
+					newPos += 1
+
+				// Greater-than or equal
+				case currentRune == '>' && strings.HasPrefix(remainingFilter, ">="):
+					packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterGreaterOrEqual, nil, FilterMap[FilterGreaterOrEqual])
+					state = READING_CONDITION
+					newPos += 2
+
+				// Less-than or equal
+				case currentRune == '<' && strings.HasPrefix(remainingFilter, "<="):
+					packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterLessOrEqual, nil, FilterMap[FilterLessOrEqual])
+					state = READING_CONDITION
+					newPos += 2
+
+				// Approx
+				case currentRune == '~' && strings.HasPrefix(remainingFilter, "~="):
+					packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterApproxMatch, nil, FilterMap[FilterApproxMatch])
+					state = READING_CONDITION
+					newPos += 2
+
+				// Still reading the attribute name
+				default:
+					attribute += fmt.Sprintf("%c", currentRune)
+					newPos += currentWidth
+				}
+
+			case READING_EXTENSIBLE_MATCHING_RULE:
+				switch {
+
+				// Matching rule OID is done
+				case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="):
+					state = READING_CONDITION
+					newPos += 2
+
+				// Still reading the matching rule oid
+				default:
+					extensibleMatchingRule += fmt.Sprintf("%c", currentRune)
+					newPos += currentWidth
+				}
+
+			case READING_CONDITION:
+				// append to the condition
+				condition += fmt.Sprintf("%c", currentRune)
+				newPos += currentWidth
+			}
 		}
+
 		if newPos == len(filter) {
 			err = NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter"))
 			return packet, newPos, err
@@ -217,6 +350,36 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) {
 		}
 
 		switch {
+		case packet.Tag == FilterExtensibleMatch:
+			// MatchingRuleAssertion ::= SEQUENCE {
+			//         matchingRule    [1] MatchingRuleID OPTIONAL,
+			//         type            [2] AttributeDescription OPTIONAL,
+			//         matchValue      [3] AssertionValue,
+			//         dnAttributes    [4] BOOLEAN DEFAULT FALSE
+			// }
+
+			// Include the matching rule oid, if specified
+			if len(extensibleMatchingRule) > 0 {
+				packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionMatchingRule, extensibleMatchingRule, MatchingRuleAssertionMap[MatchingRuleAssertionMatchingRule]))
+			}
+
+			// Include the attribute, if specified
+			if len(attribute) > 0 {
+				packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionType, attribute, MatchingRuleAssertionMap[MatchingRuleAssertionType]))
+			}
+
+			// Add the value (only required child)
+			encodedString, err := escapedStringToEncodedBytes(condition)
+			if err != nil {
+				return packet, newPos, err
+			}
+			packet.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionMatchValue, encodedString, MatchingRuleAssertionMap[MatchingRuleAssertionMatchValue]))
+
+			// Defaults to false, so only include in the sequence if true
+			if extensibleDNAttributes {
+				packet.AppendChild(ber.NewBoolean(ber.ClassContext, ber.TypePrimitive, MatchingRuleAssertionDNAttributes, extensibleDNAttributes, MatchingRuleAssertionMap[MatchingRuleAssertionDNAttributes]))
+			}
+
 		case packet.Tag == FilterEqualityMatch && condition == "*":
 			packet = ber.NewString(ber.ClassContext, ber.TypePrimitive, FilterPresent, attribute, FilterMap[FilterPresent])
 		case packet.Tag == FilterEqualityMatch && strings.Contains(condition, "*"):
@@ -238,15 +401,56 @@ func compileFilter(filter string, pos int) (*ber.Packet, int, error) {
 				default:
 					tag = FilterSubstringsAny
 				}
-				seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, tag, part, FilterSubstringsMap[uint64(tag)]))
+				encodedString, err := escapedStringToEncodedBytes(part)
+				if err != nil {
+					return packet, newPos, err
+				}
+				seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, tag, encodedString, FilterSubstringsMap[uint64(tag)]))
 			}
 			packet.AppendChild(seq)
 		default:
+			encodedString, err := escapedStringToEncodedBytes(condition)
+			if err != nil {
+				return packet, newPos, err
+			}
 			packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute"))
-			packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, condition, "Condition"))
+			packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, encodedString, "Condition"))
 		}
 
-		newPos++
+		newPos += currentWidth
 		return packet, newPos, err
 	}
 }
+
+// Convert from "ABC\xx\xx\xx" form to literal bytes for transport
+func escapedStringToEncodedBytes(escapedString string) (string, error) {
+	var buffer bytes.Buffer
+	i := 0
+	for i < len(escapedString) {
+		currentRune, currentWidth := utf8.DecodeRuneInString(escapedString[i:])
+		if currentRune == utf8.RuneError {
+			return "", NewError(ErrorFilterCompile, fmt.Errorf("ldap: error reading rune at position %d", i))
+		}
+
+		// Check for escaped hex characters and convert them to their literal value for transport.
+		if currentRune == '\\' {
+			// http://tools.ietf.org/search/rfc4515
+			// \ (%x5C) is not a valid character unless it is followed by two HEX characters due to not
+			// being a member of UTF1SUBSET.
+			if i+2 > len(escapedString) {
+				return "", NewError(ErrorFilterCompile, errors.New("ldap: missing characters for escape in filter"))
+			}
+			if escByte, decodeErr := hexpac.DecodeString(escapedString[i+1 : i+3]); decodeErr != nil {
+				return "", NewError(ErrorFilterCompile, errors.New("ldap: invalid characters for escape in filter"))
+			} else {
+				buffer.WriteByte(escByte[0])
+				i += 2 // +1 from end of loop, so 3 total for \xx.
+			}
+		} else {
+			buffer.WriteRune(currentRune)
+		}
+
+		i += currentWidth
+	}
+	return buffer.String(), nil
+}

+ 194 - 28
Godeps/_workspace/src/github.com/go-ldap/ldap/filter_test.go

@@ -1,54 +1,220 @@
-package ldap
+package ldap_test
 
 import (
+	"strings"
 	"testing"
 
 	"gopkg.in/asn1-ber.v1"
+	"gopkg.in/ldap.v2"
 )
 
 type compileTest struct {
-	filterStr  string
-	filterType int
+	filterStr string
+
+	expectedFilter string
+	expectedType   int
+	expectedErr    string
 }
 
 var testFilters = []compileTest{
-	compileTest{filterStr: "(&(sn=Miller)(givenName=Bob))", filterType: FilterAnd},
-	compileTest{filterStr: "(|(sn=Miller)(givenName=Bob))", filterType: FilterOr},
-	compileTest{filterStr: "(!(sn=Miller))", filterType: FilterNot},
-	compileTest{filterStr: "(sn=Miller)", filterType: FilterEqualityMatch},
-	compileTest{filterStr: "(sn=Mill*)", filterType: FilterSubstrings},
-	compileTest{filterStr: "(sn=*Mill)", filterType: FilterSubstrings},
-	compileTest{filterStr: "(sn=*Mill*)", filterType: FilterSubstrings},
-	compileTest{filterStr: "(sn=*i*le*)", filterType: FilterSubstrings},
-	compileTest{filterStr: "(sn=Mi*l*r)", filterType: FilterSubstrings},
-	compileTest{filterStr: "(sn=Mi*le*)", filterType: FilterSubstrings},
-	compileTest{filterStr: "(sn=*i*ler)", filterType: FilterSubstrings},
-	compileTest{filterStr: "(sn>=Miller)", filterType: FilterGreaterOrEqual},
-	compileTest{filterStr: "(sn<=Miller)", filterType: FilterLessOrEqual},
-	compileTest{filterStr: "(sn=*)", filterType: FilterPresent},
-	compileTest{filterStr: "(sn~=Miller)", filterType: FilterApproxMatch},
+	compileTest{
+		filterStr:      "(&(sn=Miller)(givenName=Bob))",
+		expectedFilter: "(&(sn=Miller)(givenName=Bob))",
+		expectedType:   ldap.FilterAnd,
+	},
+	compileTest{
+		filterStr:      "(|(sn=Miller)(givenName=Bob))",
+		expectedFilter: "(|(sn=Miller)(givenName=Bob))",
+		expectedType:   ldap.FilterOr,
+	},
+	compileTest{
+		filterStr:      "(!(sn=Miller))",
+		expectedFilter: "(!(sn=Miller))",
+		expectedType:   ldap.FilterNot,
+	},
+	compileTest{
+		filterStr:      "(sn=Miller)",
+		expectedFilter: "(sn=Miller)",
+		expectedType:   ldap.FilterEqualityMatch,
+	},
+	compileTest{
+		filterStr:      "(sn=Mill*)",
+		expectedFilter: "(sn=Mill*)",
+		expectedType:   ldap.FilterSubstrings,
+	},
+	compileTest{
+		filterStr:      "(sn=*Mill)",
+		expectedFilter: "(sn=*Mill)",
+		expectedType:   ldap.FilterSubstrings,
+	},
+	compileTest{
+		filterStr:      "(sn=*Mill*)",
+		expectedFilter: "(sn=*Mill*)",
+		expectedType:   ldap.FilterSubstrings,
+	},
+	compileTest{
+		filterStr:      "(sn=*i*le*)",
+		expectedFilter: "(sn=*i*le*)",
+		expectedType:   ldap.FilterSubstrings,
+	},
+	compileTest{
+		filterStr:      "(sn=Mi*l*r)",
+		expectedFilter: "(sn=Mi*l*r)",
+		expectedType:   ldap.FilterSubstrings,
+	},
+	// substring filters escape properly
+	compileTest{
+		filterStr:      `(sn=Mi*함*r)`,
+		expectedFilter: `(sn=Mi*\ed\95\a8*r)`,
+		expectedType:   ldap.FilterSubstrings,
+	},
+	// already escaped substring filters don't get double-escaped
+	compileTest{
+		filterStr:      `(sn=Mi*\ed\95\a8*r)`,
+		expectedFilter: `(sn=Mi*\ed\95\a8*r)`,
+		expectedType:   ldap.FilterSubstrings,
+	},
+	compileTest{
+		filterStr:      "(sn=Mi*le*)",
+		expectedFilter: "(sn=Mi*le*)",
+		expectedType:   ldap.FilterSubstrings,
+	},
+	compileTest{
+		filterStr:      "(sn=*i*ler)",
+		expectedFilter: "(sn=*i*ler)",
+		expectedType:   ldap.FilterSubstrings,
+	},
+	compileTest{
+		filterStr:      "(sn>=Miller)",
+		expectedFilter: "(sn>=Miller)",
+		expectedType:   ldap.FilterGreaterOrEqual,
+	},
+	compileTest{
+		filterStr:      "(sn<=Miller)",
+		expectedFilter: "(sn<=Miller)",
+		expectedType:   ldap.FilterLessOrEqual,
+	},
+	compileTest{
+		filterStr:      "(sn=*)",
+		expectedFilter: "(sn=*)",
+		expectedType:   ldap.FilterPresent,
+	},
+	compileTest{
+		filterStr:      "(sn~=Miller)",
+		expectedFilter: "(sn~=Miller)",
+		expectedType:   ldap.FilterApproxMatch,
+	},
+	compileTest{
+		filterStr:      `(objectGUID='\fc\fe\a3\ab\f9\90N\aaGm\d5I~\d12)`,
+		expectedFilter: `(objectGUID='\fc\fe\a3\ab\f9\90N\aaGm\d5I~\d12)`,
+		expectedType:   ldap.FilterEqualityMatch,
+	},
+	compileTest{
+		filterStr:      `(objectGUID=абвгдеёжзийклмнопрстуфхцчшщъыьэюя)`,
+		expectedFilter: `(objectGUID=\d0\b0\d0\b1\d0\b2\d0\b3\d0\b4\d0\b5\d1\91\d0\b6\d0\b7\d0\b8\d0\b9\d0\ba\d0\bb\d0\bc\d0\bd\d0\be\d0\bf\d1\80\d1\81\d1\82\d1\83\d1\84\d1\85\d1\86\d1\87\d1\88\d1\89\d1\8a\d1\8b\d1\8c\d1\8d\d1\8e\d1\8f)`,
+		expectedType:   ldap.FilterEqualityMatch,
+	},
+	compileTest{
+		filterStr:      `(objectGUID=함수목록)`,
+		expectedFilter: `(objectGUID=\ed\95\a8\ec\88\98\eb\aa\a9\eb\a1\9d)`,
+		expectedType:   ldap.FilterEqualityMatch,
+	},
+	compileTest{
+		filterStr:      `(objectGUID=`,
+		expectedFilter: ``,
+		expectedType:   0,
+		expectedErr:    "unexpected end of filter",
+	},
+	compileTest{
+		filterStr:      `(objectGUID=함수목록`,
+		expectedFilter: ``,
+		expectedType:   0,
+		expectedErr:    "unexpected end of filter",
+	},
+	compileTest{
+		filterStr:      `(&(objectclass=inetorgperson)(cn=中文))`,
+		expectedFilter: `(&(objectclass=inetorgperson)(cn=\e4\b8\ad\e6\96\87))`,
+		expectedType:   0,
+	},
+	// attr extension
+	compileTest{
+		filterStr:      `(memberOf:=foo)`,
+		expectedFilter: `(memberOf:=foo)`,
+		expectedType:   ldap.FilterExtensibleMatch,
+	},
+	// attr+named matching rule extension
+	compileTest{
+		filterStr:      `(memberOf:test:=foo)`,
+		expectedFilter: `(memberOf:test:=foo)`,
+		expectedType:   ldap.FilterExtensibleMatch,
+	},
+	// attr+oid matching rule extension
+	compileTest{
+		filterStr:      `(cn:1.2.3.4.5:=Fred Flintstone)`,
+		expectedFilter: `(cn:1.2.3.4.5:=Fred Flintstone)`,
+		expectedType:   ldap.FilterExtensibleMatch,
+	},
+	// attr+dn+oid matching rule extension
+	compileTest{
+		filterStr:      `(sn:dn:2.4.6.8.10:=Barney Rubble)`,
+		expectedFilter: `(sn:dn:2.4.6.8.10:=Barney Rubble)`,
+		expectedType:   ldap.FilterExtensibleMatch,
+	},
+	// attr+dn extension
+	compileTest{
+		filterStr:      `(o:dn:=Ace Industry)`,
+		expectedFilter: `(o:dn:=Ace Industry)`,
+		expectedType:   ldap.FilterExtensibleMatch,
+	},
+	// dn extension
+	compileTest{
+		filterStr:      `(:dn:2.4.6.8.10:=Dino)`,
+		expectedFilter: `(:dn:2.4.6.8.10:=Dino)`,
+		expectedType:   ldap.FilterExtensibleMatch,
+	},
+	compileTest{
+		filterStr:      `(memberOf:1.2.840.113556.1.4.1941:=CN=User1,OU=blah,DC=mydomain,DC=net)`,
+		expectedFilter: `(memberOf:1.2.840.113556.1.4.1941:=CN=User1,OU=blah,DC=mydomain,DC=net)`,
+		expectedType:   ldap.FilterExtensibleMatch,
+	},
+
 	// compileTest{ filterStr: "()", filterType: FilterExtensibleMatch },
 }
 
+var testInvalidFilters = []string{
+	`(objectGUID=\zz)`,
+	`(objectGUID=\a)`,
+}
+
 func TestFilter(t *testing.T) {
 	// Test Compiler and Decompiler
 	for _, i := range testFilters {
-		filter, err := CompileFilter(i.filterStr)
+		filter, err := ldap.CompileFilter(i.filterStr)
 		if err != nil {
-			t.Errorf("Problem compiling %s - %s", i.filterStr, err.Error())
-		} else if filter.Tag != ber.Tag(i.filterType) {
-			t.Errorf("%q Expected %q got %q", i.filterStr, FilterMap[uint64(i.filterType)], FilterMap[uint64(filter.Tag)])
+			if i.expectedErr == "" || !strings.Contains(err.Error(), i.expectedErr) {
+				t.Errorf("Problem compiling '%s' - '%v' (expected error to contain '%v')", i.filterStr, err, i.expectedErr)
+			}
+		} else if filter.Tag != ber.Tag(i.expectedType) {
+			t.Errorf("%q Expected %q got %q", i.filterStr, ldap.FilterMap[uint64(i.expectedType)], ldap.FilterMap[uint64(filter.Tag)])
 		} else {
-			o, err := DecompileFilter(filter)
+			o, err := ldap.DecompileFilter(filter)
 			if err != nil {
 				t.Errorf("Problem compiling %s - %s", i.filterStr, err.Error())
-			} else if i.filterStr != o {
-				t.Errorf("%q expected, got %q", i.filterStr, o)
+			} else if i.expectedFilter != o {
+				t.Errorf("%q expected, got %q", i.expectedFilter, o)
 			}
 		}
 	}
 }
 
+func TestInvalidFilter(t *testing.T) {
+	for _, filterStr := range testInvalidFilters {
+		if _, err := ldap.CompileFilter(filterStr); err == nil {
+			t.Errorf("Problem compiling %s - expected err", filterStr)
+		}
+	}
+}
+
 func BenchmarkFilterCompile(b *testing.B) {
 	b.StopTimer()
 	filters := make([]string, len(testFilters))
@@ -61,7 +227,7 @@ func BenchmarkFilterCompile(b *testing.B) {
 	maxIdx := len(filters)
 	b.StartTimer()
 	for i := 0; i < b.N; i++ {
-		CompileFilter(filters[i%maxIdx])
+		ldap.CompileFilter(filters[i%maxIdx])
 	}
 }
 
@@ -71,12 +237,12 @@ func BenchmarkFilterDecompile(b *testing.B) {
 
 	// Test Compiler and Decompiler
 	for idx, i := range testFilters {
-		filters[idx], _ = CompileFilter(i.filterStr)
+		filters[idx], _ = ldap.CompileFilter(i.filterStr)
 	}
 
 	maxIdx := len(filters)
 	b.StartTimer()
 	for i := 0; i < b.N; i++ {
-		DecompileFilter(filters[i%maxIdx])
+		ldap.DecompileFilter(filters[i%maxIdx])
 	}
 }

+ 2 - 119
Godeps/_workspace/src/github.com/go-ldap/ldap/ldap.go

@@ -6,7 +6,6 @@ package ldap
 
 import (
 	"errors"
-	"fmt"
 	"io/ioutil"
 	"os"
 
@@ -60,98 +59,6 @@ var ApplicationMap = map[uint8]string{
 	ApplicationExtendedResponse:      "Extended Response",
 }
 
-// LDAP Result Codes
-const (
-	LDAPResultSuccess                      = 0
-	LDAPResultOperationsError              = 1
-	LDAPResultProtocolError                = 2
-	LDAPResultTimeLimitExceeded            = 3
-	LDAPResultSizeLimitExceeded            = 4
-	LDAPResultCompareFalse                 = 5
-	LDAPResultCompareTrue                  = 6
-	LDAPResultAuthMethodNotSupported       = 7
-	LDAPResultStrongAuthRequired           = 8
-	LDAPResultReferral                     = 10
-	LDAPResultAdminLimitExceeded           = 11
-	LDAPResultUnavailableCriticalExtension = 12
-	LDAPResultConfidentialityRequired      = 13
-	LDAPResultSaslBindInProgress           = 14
-	LDAPResultNoSuchAttribute              = 16
-	LDAPResultUndefinedAttributeType       = 17
-	LDAPResultInappropriateMatching        = 18
-	LDAPResultConstraintViolation          = 19
-	LDAPResultAttributeOrValueExists       = 20
-	LDAPResultInvalidAttributeSyntax       = 21
-	LDAPResultNoSuchObject                 = 32
-	LDAPResultAliasProblem                 = 33
-	LDAPResultInvalidDNSyntax              = 34
-	LDAPResultAliasDereferencingProblem    = 36
-	LDAPResultInappropriateAuthentication  = 48
-	LDAPResultInvalidCredentials           = 49
-	LDAPResultInsufficientAccessRights     = 50
-	LDAPResultBusy                         = 51
-	LDAPResultUnavailable                  = 52
-	LDAPResultUnwillingToPerform           = 53
-	LDAPResultLoopDetect                   = 54
-	LDAPResultNamingViolation              = 64
-	LDAPResultObjectClassViolation         = 65
-	LDAPResultNotAllowedOnNonLeaf          = 66
-	LDAPResultNotAllowedOnRDN              = 67
-	LDAPResultEntryAlreadyExists           = 68
-	LDAPResultObjectClassModsProhibited    = 69
-	LDAPResultAffectsMultipleDSAs          = 71
-	LDAPResultOther                        = 80
-
-	ErrorNetwork            = 200
-	ErrorFilterCompile      = 201
-	ErrorFilterDecompile    = 202
-	ErrorDebugging          = 203
-	ErrorUnexpectedMessage  = 204
-	ErrorUnexpectedResponse = 205
-)
-
-var LDAPResultCodeMap = map[uint8]string{
-	LDAPResultSuccess:                      "Success",
-	LDAPResultOperationsError:              "Operations Error",
-	LDAPResultProtocolError:                "Protocol Error",
-	LDAPResultTimeLimitExceeded:            "Time Limit Exceeded",
-	LDAPResultSizeLimitExceeded:            "Size Limit Exceeded",
-	LDAPResultCompareFalse:                 "Compare False",
-	LDAPResultCompareTrue:                  "Compare True",
-	LDAPResultAuthMethodNotSupported:       "Auth Method Not Supported",
-	LDAPResultStrongAuthRequired:           "Strong Auth Required",
-	LDAPResultReferral:                     "Referral",
-	LDAPResultAdminLimitExceeded:           "Admin Limit Exceeded",
-	LDAPResultUnavailableCriticalExtension: "Unavailable Critical Extension",
-	LDAPResultConfidentialityRequired:      "Confidentiality Required",
-	LDAPResultSaslBindInProgress:           "Sasl Bind In Progress",
-	LDAPResultNoSuchAttribute:              "No Such Attribute",
-	LDAPResultUndefinedAttributeType:       "Undefined Attribute Type",
-	LDAPResultInappropriateMatching:        "Inappropriate Matching",
-	LDAPResultConstraintViolation:          "Constraint Violation",
-	LDAPResultAttributeOrValueExists:       "Attribute Or Value Exists",
-	LDAPResultInvalidAttributeSyntax:       "Invalid Attribute Syntax",
-	LDAPResultNoSuchObject:                 "No Such Object",
-	LDAPResultAliasProblem:                 "Alias Problem",
-	LDAPResultInvalidDNSyntax:              "Invalid DN Syntax",
-	LDAPResultAliasDereferencingProblem:    "Alias Dereferencing Problem",
-	LDAPResultInappropriateAuthentication:  "Inappropriate Authentication",
-	LDAPResultInvalidCredentials:           "Invalid Credentials",
-	LDAPResultInsufficientAccessRights:     "Insufficient Access Rights",
-	LDAPResultBusy:                         "Busy",
-	LDAPResultUnavailable:                  "Unavailable",
-	LDAPResultUnwillingToPerform:           "Unwilling To Perform",
-	LDAPResultLoopDetect:                   "Loop Detect",
-	LDAPResultNamingViolation:              "Naming Violation",
-	LDAPResultObjectClassViolation:         "Object Class Violation",
-	LDAPResultNotAllowedOnNonLeaf:          "Not Allowed On Non Leaf",
-	LDAPResultNotAllowedOnRDN:              "Not Allowed On RDN",
-	LDAPResultEntryAlreadyExists:           "Entry Already Exists",
-	LDAPResultObjectClassModsProhibited:    "Object Class Mods Prohibited",
-	LDAPResultAffectsMultipleDSAs:          "Affects Multiple DSAs",
-	LDAPResultOther:                        "Other",
-}
-
 // Ldap Behera Password Policy Draft 10 (https://tools.ietf.org/html/draft-behera-ldap-password-policy-10)
 const (
 	BeheraPasswordExpired             = 0
@@ -318,8 +225,8 @@ func addRequestDescriptions(packet *ber.Packet) {
 }
 
 func addDefaultLDAPResponseDescriptions(packet *ber.Packet) {
-	resultCode := packet.Children[1].Children[0].Value.(int64)
-	packet.Children[1].Children[0].Description = "Result Code (" + LDAPResultCodeMap[uint8(resultCode)] + ")"
+	resultCode, _ := getLDAPResultCode(packet)
+	packet.Children[1].Children[0].Description = "Result Code (" + LDAPResultCodeMap[resultCode] + ")"
 	packet.Children[1].Children[1].Description = "Matched DN"
 	packet.Children[1].Children[2].Description = "Error Message"
 	if len(packet.Children[1].Children) > 3 {
@@ -343,30 +250,6 @@ func DebugBinaryFile(fileName string) error {
 	return nil
 }
 
-type Error struct {
-	Err        error
-	ResultCode uint8
-}
-
-func (e *Error) Error() string {
-	return fmt.Sprintf("LDAP Result Code %d %q: %s", e.ResultCode, LDAPResultCodeMap[e.ResultCode], e.Err.Error())
-}
-
-func NewError(resultCode uint8, err error) error {
-	return &Error{ResultCode: resultCode, Err: err}
-}
-
-func getLDAPResultCode(packet *ber.Packet) (code uint8, description string) {
-	if len(packet.Children) >= 2 {
-		response := packet.Children[1]
-		if response.ClassType == ber.ClassApplication && response.TagType == ber.TypeConstructed && len(response.Children) >= 3 {
-			return uint8(response.Children[0].Value.(int64)), response.Children[2].Value.(string)
-		}
-	}
-
-	return ErrorNetwork, "Invalid packet format"
-}
-
 var hex = "0123456789abcdef"
 
 func mustEscape(c byte) bool {

+ 53 - 25
Godeps/_workspace/src/github.com/go-ldap/ldap/ldap_test.go

@@ -1,9 +1,11 @@
-package ldap
+package ldap_test
 
 import (
 	"crypto/tls"
 	"fmt"
 	"testing"
+
+	"gopkg.in/ldap.v2"
 )
 
 var ldapServer = "ldap.itd.umich.edu"
@@ -21,7 +23,7 @@ var attributes = []string{
 
 func TestDial(t *testing.T) {
 	fmt.Printf("TestDial: starting...\n")
-	l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
+	l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
 	if err != nil {
 		t.Errorf(err.Error())
 		return
@@ -32,7 +34,7 @@ func TestDial(t *testing.T) {
 
 func TestDialTLS(t *testing.T) {
 	fmt.Printf("TestDialTLS: starting...\n")
-	l, err := DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapTLSPort), &tls.Config{InsecureSkipVerify: true})
+	l, err := ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapTLSPort), &tls.Config{InsecureSkipVerify: true})
 	if err != nil {
 		t.Errorf(err.Error())
 		return
@@ -43,7 +45,7 @@ func TestDialTLS(t *testing.T) {
 
 func TestStartTLS(t *testing.T) {
 	fmt.Printf("TestStartTLS: starting...\n")
-	l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
+	l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
 	if err != nil {
 		t.Errorf(err.Error())
 		return
@@ -58,16 +60,16 @@ func TestStartTLS(t *testing.T) {
 
 func TestSearch(t *testing.T) {
 	fmt.Printf("TestSearch: starting...\n")
-	l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
+	l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
 	if err != nil {
 		t.Errorf(err.Error())
 		return
 	}
 	defer l.Close()
 
-	searchRequest := NewSearchRequest(
+	searchRequest := ldap.NewSearchRequest(
 		baseDN,
-		ScopeWholeSubtree, DerefAlways, 0, 0, false,
+		ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false,
 		filter[0],
 		attributes,
 		nil)
@@ -83,16 +85,16 @@ func TestSearch(t *testing.T) {
 
 func TestSearchStartTLS(t *testing.T) {
 	fmt.Printf("TestSearchStartTLS: starting...\n")
-	l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
+	l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
 	if err != nil {
 		t.Errorf(err.Error())
 		return
 	}
 	defer l.Close()
 
-	searchRequest := NewSearchRequest(
+	searchRequest := ldap.NewSearchRequest(
 		baseDN,
-		ScopeWholeSubtree, DerefAlways, 0, 0, false,
+		ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false,
 		filter[0],
 		attributes,
 		nil)
@@ -123,7 +125,7 @@ func TestSearchStartTLS(t *testing.T) {
 
 func TestSearchWithPaging(t *testing.T) {
 	fmt.Printf("TestSearchWithPaging: starting...\n")
-	l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
+	l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
 	if err != nil {
 		t.Errorf(err.Error())
 		return
@@ -136,9 +138,9 @@ func TestSearchWithPaging(t *testing.T) {
 		return
 	}
 
-	searchRequest := NewSearchRequest(
+	searchRequest := ldap.NewSearchRequest(
 		baseDN,
-		ScopeWholeSubtree, DerefAlways, 0, 0, false,
+		ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false,
 		filter[2],
 		attributes,
 		nil)
@@ -149,12 +151,38 @@ func TestSearchWithPaging(t *testing.T) {
 	}
 
 	fmt.Printf("TestSearchWithPaging: %s -> num of entries = %d\n", searchRequest.Filter, len(sr.Entries))
+
+	searchRequest = ldap.NewSearchRequest(
+		baseDN,
+		ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false,
+		filter[2],
+		attributes,
+		[]ldap.Control{ldap.NewControlPaging(5)})
+	sr, err = l.SearchWithPaging(searchRequest, 5)
+	if err != nil {
+		t.Errorf(err.Error())
+		return
+	}
+
+	fmt.Printf("TestSearchWithPaging: %s -> num of entries = %d\n", searchRequest.Filter, len(sr.Entries))
+
+	searchRequest = ldap.NewSearchRequest(
+		baseDN,
+		ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false,
+		filter[2],
+		attributes,
+		[]ldap.Control{ldap.NewControlPaging(500)})
+	sr, err = l.SearchWithPaging(searchRequest, 5)
+	if err == nil {
+		t.Errorf("expected an error when paging size in control in search request doesn't match size given in call, got none")
+		return
+	}
 }
 
-func searchGoroutine(t *testing.T, l *Conn, results chan *SearchResult, i int) {
-	searchRequest := NewSearchRequest(
+func searchGoroutine(t *testing.T, l *ldap.Conn, results chan *ldap.SearchResult, i int) {
+	searchRequest := ldap.NewSearchRequest(
 		baseDN,
-		ScopeWholeSubtree, DerefAlways, 0, 0, false,
+		ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false,
 		filter[i],
 		attributes,
 		nil)
@@ -169,17 +197,17 @@ func searchGoroutine(t *testing.T, l *Conn, results chan *SearchResult, i int) {
 
 func testMultiGoroutineSearch(t *testing.T, TLS bool, startTLS bool) {
 	fmt.Printf("TestMultiGoroutineSearch: starting...\n")
-	var l *Conn
+	var l *ldap.Conn
 	var err error
 	if TLS {
-		l, err = DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapTLSPort), &tls.Config{InsecureSkipVerify: true})
+		l, err = ldap.DialTLS("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapTLSPort), &tls.Config{InsecureSkipVerify: true})
 		if err != nil {
 			t.Errorf(err.Error())
 			return
 		}
 		defer l.Close()
 	} else {
-		l, err = Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
+		l, err = ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
 		if err != nil {
 			t.Errorf(err.Error())
 			return
@@ -195,9 +223,9 @@ func testMultiGoroutineSearch(t *testing.T, TLS bool, startTLS bool) {
 		}
 	}
 
-	results := make([]chan *SearchResult, len(filter))
+	results := make([]chan *ldap.SearchResult, len(filter))
 	for i := range filter {
-		results[i] = make(chan *SearchResult)
+		results[i] = make(chan *ldap.SearchResult)
 		go searchGoroutine(t, l, results[i], i)
 	}
 	for i := range filter {
@@ -217,17 +245,17 @@ func TestMultiGoroutineSearch(t *testing.T) {
 }
 
 func TestEscapeFilter(t *testing.T) {
-	if got, want := EscapeFilter("a\x00b(c)d*e\\f"), `a\00b\28c\29d\2ae\5cf`; got != want {
+	if got, want := ldap.EscapeFilter("a\x00b(c)d*e\\f"), `a\00b\28c\29d\2ae\5cf`; got != want {
 		t.Errorf("Got %s, expected %s", want, got)
 	}
-	if got, want := EscapeFilter("Lučić"), `Lu\c4\8di\c4\87`; got != want {
+	if got, want := ldap.EscapeFilter("Lučić"), `Lu\c4\8di\c4\87`; got != want {
 		t.Errorf("Got %s, expected %s", want, got)
 	}
 }
 
 func TestCompare(t *testing.T) {
 	fmt.Printf("TestCompare: starting...\n")
-	l, err := Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
+	l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", ldapServer, ldapPort))
 	if err != nil {
 		t.Fatal(err.Error())
 	}
@@ -243,5 +271,5 @@ func TestCompare(t *testing.T) {
 		return
 	}
 
-	fmt.Printf("TestCompare: -> num of entries = %d\n", sr)
+	fmt.Printf("TestCompare: -> %v\n", sr)
 }

+ 57 - 4
Godeps/_workspace/src/github.com/go-ldap/ldap/search.go

@@ -62,6 +62,7 @@ package ldap
 import (
 	"errors"
 	"fmt"
+	"sort"
 	"strings"
 
 	"gopkg.in/asn1-ber.v1"
@@ -93,6 +94,26 @@ var DerefMap = map[int]string{
 	DerefAlways:         "DerefAlways",
 }
 
+// NewEntry returns an Entry object with the specified distinguished name and attribute key-value pairs.
+// The map of attributes is accessed in alphabetical order of the keys in order to ensure that, for the
+// same input map of attributes, the output entry will contain the same order of attributes
+func NewEntry(dn string, attributes map[string][]string) *Entry {
+	var attributeNames []string
+	for attributeName := range attributes {
+		attributeNames = append(attributeNames, attributeName)
+	}
+	sort.Strings(attributeNames)
+
+	var encodedAttributes []*EntryAttribute
+	for _, attributeName := range attributeNames {
+		encodedAttributes = append(encodedAttributes, NewEntryAttribute(attributeName, attributes[attributeName]))
+	}
+	return &Entry{
+		DN:         dn,
+		Attributes: encodedAttributes,
+	}
+}
+
 type Entry struct {
 	DN         string
 	Attributes []*EntryAttribute
@@ -146,6 +167,19 @@ func (e *Entry) PrettyPrint(indent int) {
 	}
 }
 
+// NewEntryAttribute returns a new EntryAttribute with the desired key-value pair
+func NewEntryAttribute(name string, values []string) *EntryAttribute {
+	var bytes [][]byte
+	for _, value := range values {
+		bytes = append(bytes, []byte(value))
+	}
+	return &EntryAttribute{
+		Name:       name,
+		Values:     values,
+		ByteValues: bytes,
+	}
+}
+
 type EntryAttribute struct {
 	Name       string
 	Values     []string
@@ -234,13 +268,32 @@ func NewSearchRequest(
 	}
 }
 
+// SearchWithPaging accepts a search request and desired page size in order to execute LDAP queries to fulfill the
+// search request. All paged LDAP query responses will be buffered and the final result will be returned atomically.
+// The following four cases are possible given the arguments:
+//  - given SearchRequest missing a control of type ControlTypePaging: we will add one with the desired paging size
+//  - given SearchRequest contains a control of type ControlTypePaging that isn't actually a ControlPaging: fail without issuing any queries
+//  - given SearchRequest contains a control of type ControlTypePaging with pagingSize equal to the size requested: no change to the search request
+//  - given SearchRequest contains a control of type ControlTypePaging with pagingSize not equal to the size requested: fail without issuing any queries
+// A requested pagingSize of 0 is interpreted as no limit by LDAP servers.
 func (l *Conn) SearchWithPaging(searchRequest *SearchRequest, pagingSize uint32) (*SearchResult, error) {
-	if searchRequest.Controls == nil {
-		searchRequest.Controls = make([]Control, 0)
+	var pagingControl *ControlPaging
+
+	control := FindControl(searchRequest.Controls, ControlTypePaging)
+	if control == nil {
+		pagingControl = NewControlPaging(pagingSize)
+		searchRequest.Controls = append(searchRequest.Controls, pagingControl)
+	} else {
+		castControl, ok := control.(*ControlPaging)
+		if !ok {
+			return nil, fmt.Errorf("Expected paging control to be of type *ControlPaging, got %v", control)
+		}
+		if castControl.PagingSize != pagingSize {
+			return nil, fmt.Errorf("Paging size given in search request (%d) conflicts with size given in search call (%d)", castControl.PagingSize, pagingSize)
+		}
+		pagingControl = castControl
 	}
 
-	pagingControl := NewControlPaging(pagingSize)
-	searchRequest.Controls = append(searchRequest.Controls, pagingControl)
 	searchResult := new(SearchResult)
 	for {
 		result, err := l.Search(searchRequest)

+ 31 - 0
Godeps/_workspace/src/github.com/go-ldap/ldap/search_test.go

@@ -0,0 +1,31 @@
+package ldap
+
+import (
+	"reflect"
+	"testing"
+)
+
+// TestNewEntry tests that repeated calls to NewEntry return the same value with the same input
+func TestNewEntry(t *testing.T) {
+	dn := "testDN"
+	attributes := map[string][]string{
+		"alpha":   {"value"},
+		"beta":    {"value"},
+		"gamma":   {"value"},
+		"delta":   {"value"},
+		"epsilon": {"value"},
+	}
+	exectedEntry := NewEntry(dn, attributes)
+
+	iteration := 0
+	for {
+		if iteration == 100 {
+			break
+		}
+		testEntry := NewEntry(dn, attributes)
+		if !reflect.DeepEqual(exectedEntry, testEntry) {
+			t.Fatalf("consequent calls to NewEntry did not yield the same result:\n\texpected:\n\t%s\n\tgot:\n\t%s\n", exectedEntry, testEntry)
+		}
+		iteration = iteration + 1
+	}
+}

+ 1 - 0
Gruntfile.js

@@ -18,6 +18,7 @@ module.exports = function (grunt) {
   }
 
   config.pkg.version = grunt.option('pkgVer') || config.pkg.version;
+  console.log('Version', config.pkg.version);
 
   // load plugins
   require('load-grunt-tasks')(grunt);

+ 4 - 1
README.md

@@ -1,8 +1,10 @@
-[Grafana](http://grafana.org) [![Circle CI](https://circleci.com/gh/grafana/grafana.svg?style=svg)](https://circleci.com/gh/grafana/grafana) [![Coverage Status](https://coveralls.io/repos/grafana/grafana/badge.png)](https://coveralls.io/r/grafana/grafana) [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/grafana/grafana?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[Grafana](http://grafana.org) [![Circle CI](https://circleci.com/gh/grafana/grafana.svg?style=svg)](https://circleci.com/gh/grafana/grafana) [![Coverage Status](https://coveralls.io/repos/grafana/grafana/badge.png)](https://coveralls.io/r/grafana/grafana)
 ================
 [Website](http://grafana.org) |
 [Twitter](https://twitter.com/grafana) |
 [IRC](https://webchat.freenode.net/?channels=grafana) |
+![](https://brandfolder.com/api/favicon/icon?size=16&domain=www.slack.com)
+[Slack](http://slack.raintank.io) |
 [Email](mailto:contact@grafana.org)
 
 Grafana is an open source, feature rich metrics dashboard and graph editor for
@@ -77,6 +79,7 @@ the latest master builds [here](http://grafana.org/download/builds)
 
 - Go 1.5
 - NodeJS
+- [Godep](https://github.com/tools/godep)
 
 ### Get Code
 

+ 1 - 1
appveyor.yml

@@ -5,7 +5,7 @@ os: Windows Server 2012 R2
 clone_folder: c:\gopath\src\github.com\grafana\grafana
 
 environment:
-  nodejs_version: "4"
+  nodejs_version: "5"
   GOPATH: c:\gopath
 
 install:

+ 4 - 4
bower.json

@@ -14,10 +14,10 @@
   ],
   "dependencies": {
     "jquery": "~2.1.4",
-    "angular": "~1.5.1",
-    "angular-route": "~1.5.1",
-    "angular-mocks": "~1.5.1",
-    "angular-sanitize": "~1.5.1",
+    "angular": "~1.5.3",
+    "angular-route": "~1.5.3",
+    "angular-mocks": "~1.5.3",
+    "angular-sanitize": "~1.5.3",
     "angular-bindonce": "~0.3.3"
   }
 }

+ 10 - 6
build.go

@@ -73,8 +73,7 @@ func main() {
 			grunt("test")
 
 		case "package":
-			//verifyGitRepoIsClean()
-			grunt("release")
+			grunt("release", fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration))
 			createLinuxPackages()
 
 		case "pkg-rpm":
@@ -100,12 +99,12 @@ func main() {
 func makeLatestDistCopies() {
 	rpmIteration := "-1"
 	if linuxPackageIteration != "" {
-		rpmIteration = "-" + linuxPackageIteration
+		rpmIteration = linuxPackageIteration
 	}
 
-	runError("cp", "dist/grafana_"+version+"_amd64.deb", "dist/grafana_latest_amd64.deb")
-	runError("cp", "dist/grafana-"+linuxPackageVersion+rpmIteration+".x86_64.rpm", "dist/grafana-latest-1.x86_64.rpm")
-	runError("cp", "dist/grafana-"+version+".linux-x64.tar.gz", "dist/grafana-latest.linux-x64.tar.gz")
+	runError("cp", fmt.Sprintf("dist/grafana_%v-%v_amd64.deb", linuxPackageVersion, linuxPackageIteration), "dist/grafana_latest_amd64.deb")
+	runError("cp", fmt.Sprintf("dist/grafana-%v-%v.x86_64.rpm", linuxPackageVersion, rpmIteration), "dist/grafana-latest-1.x86_64.rpm")
+	runError("cp", fmt.Sprintf("dist/grafana-%v-%v.linux-x64.tar.gz", linuxPackageVersion, linuxPackageIteration), "dist/grafana-latest.linux-x64.tar.gz")
 }
 
 func readVersionFromPackageJson() {
@@ -133,6 +132,11 @@ func readVersionFromPackageJson() {
 	if len(parts) > 1 {
 		linuxPackageVersion = parts[0]
 		linuxPackageIteration = parts[1]
+		if linuxPackageIteration != "" {
+			// add timestamp to iteration
+			linuxPackageIteration = fmt.Sprintf("%s%v", linuxPackageIteration, time.Now().Unix())
+		}
+		log.Println(fmt.Sprintf("teration %v", linuxPackageIteration))
 	}
 }
 

+ 23 - 0
conf/ldap.toml

@@ -28,8 +28,31 @@ search_base_dns = ["dc=grafana,dc=org"]
 # This is done by enabling group_search_filter below. You must also set member_of= "cn"
 # in [servers.attributes] below.
 
+# Users with nested/recursive group membership and an LDAP server that supports LDAP_MATCHING_RULE_IN_CHAIN
+# can set group_search_filter, group_search_filter_user_attribute, group_search_base_dns and member_of
+# below in such a way that the user's recursive group membership is considered.
+#
+# Nested Groups + Active Directory (AD) Example:
+#
+#   AD groups store the Distinguished Names (DNs) of members, so your filter must
+#   recursively search your groups for the authenticating user's DN. For example:
+#
+#     group_search_filter = "(member:1.2.840.113556.1.4.1941:=%s)"
+#     group_search_filter_user_attribute = "distinguishedName"
+#     group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
+#
+#     [servers.attributes]
+#     ...
+#     member_of = "distinguishedName"
+
 ## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available)
 # group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))"
+## Group search filter user attribute defines what user attribute gets substituted for %s in group_search_filter.
+## Defaults to the value of username in [server.attributes]
+## Valid options are any of your values in [servers.attributes]
+## If you are using nested groups you probably want to set this and member_of in
+## [servers.attributes] to "distinguishedName"
+# group_search_filter_user_attribute = "distinguishedName"
 ## An array of the base DNs to search through for groups. Typically uses ou=groups
 # group_search_base_dns = ["ou=groups,dc=grafana,dc=org"]
 

+ 5 - 6
docs/mkdocs.yml

@@ -86,13 +86,12 @@ pages:
 - ['http_api/snapshot.md', 'API', 'Snapshot API']
 - ['http_api/other.md', 'API', 'Other API']
 
-- ['plugins/overview.md', 'Plugins', 'Overview']
+- ['plugins/index.md', 'Plugins', 'Overview']
 - ['plugins/installation.md', 'Plugins', 'Installation']
-- ['plugins/app.md', 'Plugins', 'App plugins']
-- ['plugins/datasources.md', 'Plugins', 'Datasource plugins']
-- ['plugins/panels.md', 'Plugins', 'Panel plugins']
-- ['plugins/development.md', 'Plugins', 'Plugin development']
-- ['plugins/plugin.json.md', 'Plugins', 'Plugin json']
+- ['plugins/development.md', 'Plugins', 'Development']
+- ['plugins/apps.md', 'Plugins', 'Apps']
+- ['plugins/datasources.md', 'Plugins', 'Datasources']
+- ['plugins/panels.md', 'Plugins', 'Panels']
 
 - ['tutorials/index.md', 'Tutorials', 'Tutorials']
 - ['tutorials/hubot_howto.md', 'Tutorials', 'How To integrate Hubot and Grafana']

+ 16 - 2
docs/sources/datasources/cloudwatch.md

@@ -69,8 +69,22 @@ Name | Description
 
 For details about the metrics CloudWatch provides, please refer to the [CloudWatch documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html).
 
-The `ec2_instance_attribute` query take `filters` in JSON format.  
-You can specify [pre-defined filters of ec2:DescribeInstances](http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html).  
+## Example templated Queries
+
+Example dimension queries which will return list of resources for individual AWS Services:
+
+Service | Query
+------- | -----
+EBS | `dimension_values(us-east-1,AWS/ELB,RequestCount,LoadBalancerName)`
+ElastiCache | `dimension_values(us-east-1,AWS/ElastiCache,CPUUtilization,CacheClusterId)`
+RedShift | `dimension_values(us-east-1,AWS/Redshift,CPUUtilization,ClusterIdentifier)`
+RDS | `dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier)`
+S3 | `dimension_values(us-east-1,AWS/S3,BucketSizeBytes,BucketName)`
+
+## ec2_instance_attribute JSON filters
+
+The `ec2_instance_attribute` query take `filters` in JSON format.
+You can specify [pre-defined filters of ec2:DescribeInstances](http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html).
 Specify like `{ filter_name1: [ filter_value1 ], filter_name2: [ filter_value2 ] }`
 
 Example `ec2_instance_attribute()` query

+ 9 - 2
docs/sources/installation/debian.md

@@ -10,14 +10,21 @@ page_keywords: grafana, installation, debian, ubuntu, guide
 
 Description | Download
 ------------ | -------------
-.deb for Debian-based Linux | [grafana_2.6.0_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb)
+Stable .deb for Debian-based Linux | [grafana_2.6.0_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb)
+Beta .deb for Debian-based Linux |   [grafana_3.0.0-beta21459801392_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.0-beta21459801392_amd64.deb)
 
-## Install
+## Install Stable
 
     $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb
     $ sudo apt-get install -y adduser libfontconfig
     $ sudo dpkg -i grafana_2.6.0_amd64.deb
 
+## Install 3.0 Beta
+
+    $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.0-beta21459801392_amd64.deb
+    $ sudo apt-get install -y adduser libfontconfig
+    $ sudo dpkg -i grafana_3.0.0-beta21459801392_amd64.deb
+
 ## APT Repository
 
 Add the following line to your `/etc/apt/sources.list` file.

+ 21 - 2
docs/sources/installation/rpm.md

@@ -10,9 +10,10 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide
 
 Description | Download
 ------------ | -------------
-.RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [grafana-2.6.0-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.6.0-1.x86_64.rpm)
+Stable .RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [grafana-2.6.0-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.6.0-1.x86_64.rpm)
+Beta .RPM for CentOS / Fedor / OpenSuse / Redhat Linux | [grafana-3.0.0-beta21459801392.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.0-beta21459801392.x86_64.rpm)
 
-## Install from package file
+## Install Stable Release from package file
 
 You can install Grafana using Yum directly.
 
@@ -29,6 +30,24 @@ Or install manually using `rpm`.
 
     $ sudo rpm -i --nodeps grafana-2.6.0-1.x86_64.rpm
 
+## Install Beta Release from package file
+
+You can install Grafana using Yum directly.
+
+    $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.0-beta21459801392.x86_64.rpm
+
+Or install manually using `rpm`.
+
+#### On CentOS / Fedora / Redhat:
+
+    $ sudo yum install initscripts fontconfig
+    $ sudo rpm -Uvh grafana-3.0.0-beta21459801392.x86_64.rpm
+
+#### On OpenSuse:
+
+    $ sudo rpm -i --nodeps grafana-3.0.0-beta21459801392.x86_64.rpm
+
+
 ## Install via YUM Repository
 
 Add the following to a new file at `/etc/yum.repos.d/grafana.repo`

+ 1 - 1
docs/sources/installation/windows.md

@@ -10,7 +10,7 @@ page_keywords: grafana, installation, windows guide
 
 Description | Download
 ------------ | -------------
-Zip package for Windows | [grafana.2.5.0.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-2.5.0.windows-x64.zip)
+Stable Zip package for Windows | [grafana.2.5.0.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-2.5.0.windows-x64.zip)
 
 ## Configure
 

+ 0 - 46
docs/sources/plugins/app.md

@@ -1,46 +0,0 @@
----
-page_title: App plugin
-page_description: App plugin for Grafana
-page_keywords: grafana, plugins, documentation
----
-
- > Our goal is not to have a very extensive documentation but rather have actual code that people can look at. An example implementation of an app can be found in this [example app repo](https://github.com/grafana/example-app)
-
-# Apps
-
-App plugins is a new kind of grafana plugin that can bundle datasource and panel plugins within one package. It also enable the plugin author to create custom pages within grafana. The custom pages enables the plugin author to include things like documentation, sign up forms or controlling other services using HTTP requests.
-
-Datasource and panel plugins will show up like normal plugins. The app pages will be available in the main menu.
-
-<img class="no-shadow" src="/img/v3/app-in-main-menu.png">
-
-## Enabling app plugins
-After installing an app it have to be enabled before it show up as an datasource or panel. You can do that on the app page in the config tab.
-
-## README.md
-
-The readme file in the mounted folder will show up in the overview tab on the app page.
-
-## Module exports
-```javascript
-export {
-  ExampleAppConfigCtrl as ConfigCtrl,
-  StreamPageCtrl,
-  LogsPageCtrl
-};
-```
-The only required export is the ConfigCtrl. Both StreamPageCtrl and LogsPageCtrl are custom pages defined in plugin.json
-
-## Custom pages
-Custom pages are defined in the plugin.json like this.
-```json
-"pages": [
-  { "name": "Live stream", "component": "StreamPageCtrl", "role": "Editor"},
-  { "name": "Log view", "component": "LogsPageCtrl", "role": "Viewer"}
-]
-```
-The component field have to match one of the components exported in the module.js in the root of the plugin.
-
-## Bundled plugins
-
-When Grafana starts it will scan all directories within an app plugin and load folders containing a plugin.json as an plugin.

+ 24 - 0
docs/sources/plugins/apps.md

@@ -0,0 +1,24 @@
+---
+page_title: App plugin
+page_description: App plugin for Grafana
+page_keywords: grafana, plugins, documentation
+---
+
+
+# Apps
+
+App plugins is a new kind of grafana plugin that can bundle datasource and panel plugins within one package. It also enable the plugin author to create custom pages within grafana. The custom pages enables the plugin author to include things like documentation, sign up forms or controlling other services using HTTP requests.
+
+Datasource and panel plugins will show up like normal plugins. The app pages will be available in the main menu.
+
+<img class="no-shadow" src="/img/v3/app-in-main-menu.png">
+
+## Enabling app plugins
+After installing an app it have to be enabled before it show up as an datasource or panel. You can do that on the app page in the config tab.
+
+### Develop your own App
+
+> Our goal is not to have a very extensive documentation but rather have actual
+> code that people can look at. An example implementation of an app can be found
+> in this [example app repo](https://github.com/grafana/example-app)
+

+ 12 - 2
docs/sources/plugins/datasources.md

@@ -4,11 +4,18 @@ page_description: Datasource plugins for Grafana
 page_keywords: grafana, plugins, documentation
 ---
 
- > Our goal is not to have a very extensive documentation but rather have actual code that people can look at. An example implementation of a datasource can be found in this [example datasource repo](https://github.com/grafana/simple-json-datasource)
 
 # Datasources
 
-Datasource plugins enables people to develop plugins for any database that communicates over http. Its up to the plugin to transform the data into time series data so that any grafana panel can then show it.
+Datasource plugins enables people to develop plugins for any database that
+communicates over http. Its up to the plugin to transform the data into
+time series data so that any grafana panel can then show it.
+
+## Datasource development
+
+> Our goal is not to have a very extensive documentation but rather have actual
+> code that people can look at. An example implementation of a datasource can be
+> found in this [example datasource repo](https://github.com/grafana/simple-json-datasource)
 
 To interact with the rest of grafana the plugins module file can export 5 different components.
 
@@ -19,11 +26,14 @@ To interact with the rest of grafana the plugins module file can export 5 differ
 - AnnotationsQueryCtrl
 
 ## Plugin json
+
 There are two datasource specific settings for the plugin.json
+
 ```javascript
 "metrics": true,
 "annotations": false,
 ```
+
 These settings indicates what kind of data the plugin can deliver. At least one of them have to be true
 
 ## Datasource

+ 16 - 11
docs/sources/plugins/developing_plugins.md → docs/sources/plugins/development.md

@@ -1,5 +1,5 @@
 ---
-page_title: Plugin development
+page_title: Plugin development guide
 page_description: Plugin development for Grafana
 page_keywords: grafana, plugins, documentation, development
 ---
@@ -10,8 +10,8 @@ From grafana 3.0 it's very easy to develop your own plugins and share them with
 
 ## Short version
 
-1. [Setup grafana](https://github.com/grafana/grafana/blob/master/DEVELOPMENT.md)
-2. Clone an example plugin into ```data/plugins```
+1. [Setup grafana](http://docs.grafana.org/project/building_from_source/)
+2. Clone an example plugin into ```/var/lib/grafana/plugins```  or `data/plugins` (relative to grafana git repo if your running development version from source dir)
 3. Code away!
 
 ## What languages?
@@ -26,22 +26,27 @@ All our example plugins have build scripted configured.
 
 ## module.(js|ts)
 
-This is the entry point for every plugin. This is the place where you should export your plugin implementation. Depending on what kind of plugin you are developing you will be expected to export different things. You can find whats expected for [datasource](http://docs.grafana.org/v3.0/plugins/datasources/), [panels](http://docs.grafana.org/v3.0/plugins/panels/) and [apps](http://docs.grafana.org/v3.0/plugins/app/)
-plugins in the documentation.
+This is the entry point for every plugin. This is the place where you should export
+your plugin implementation. Depending on what kind of plugin you are developing you
+will be expected to export different things. You can find what's expected for [datasource](./datasources.md), [panels](./panels.md)
+and [apps](./apps.md) plugins in the documentation.
 
 ## Start developing your plugin
 There are two ways that you can start developing a Grafana plugin.
-1. Setup a Grafana development environment. [(described here)](https://github.com/grafana/grafana/blob/master/DEVELOPMENT.md)  and place your plugin in the ```data/plugins``` folder.
-2. Install Grafana and place your plugin the plugins directory which is set in your [config file](http://docs.grafana.org/installation/configuration/)
+
+1. Setup a Grafana development environment. [(described here)](http://docs.grafana.org/project/building_from_source/) and place your plugin in the ```data/plugins``` folder.
+2. Install Grafana and place your plugin in the plugins directory which is set in your [config file](../installation/configuration.md). By default this is `/var/lib/grafana/plugins` on Linux systems.
+3. Place your plugin directory anywhere you like and specify it grafana.ini.
 
 We encourage people to setup the full Grafana environment so that you can get inspiration from the rest of grafana code base.
 
-When Grafana starts it will scan the plugin folders and mount every folder that contains a plugin.json file unless the folder contains a subfolder named dist. In that case grafana will mount the dist folder instead.
-This makes it possible to have both built and src content in the same plugin folder.
+When Grafana starts it will scan the plugin folders and mount every folder that contains a plugin.json file unless
+the folder contains a subfolder named dist. In that case grafana will mount the dist folder instead.
+This makes it possible to have both built and src content in the same plugin git repo.
 
-## Boilerplate
+## Examples
 We currently have three different examples that you can fork/download to get started developing your grafana plugin.
 
- - [simple-json-datasource](https://github.com/grafana/simple-json-datasource) (small datasource plugin for quering json data from backends)
+ - [simple-json-datasource](https://github.com/grafana/simple-json-datasource) (small datasource plugin for querying json data from backends)
  - [piechart-panel](https://github.com/grafana/piechart-panel)
  - [example-app](https://github.com/grafana/example-app)

+ 21 - 0
docs/sources/plugins/index.md

@@ -0,0 +1,21 @@
+---
+page_title: Plugin overview
+page_description: Plugins for Grafana
+page_keywords: grafana, plugins, documentation
+---
+
+# Plugins
+
+From Grafana 3.0 not only datasource plugins are supported but also panel plugins and apps.
+Having panels as plugins make it easy to create and add any kind of panel, to show your data
+or improve your favorite dashboards. Apps is something new in Grafana that enables
+bundling of datasources, panels, dashboards and Grafana pages into a cohesive experience.
+
+Grafana already have a strong community of contributors and plugin developers.
+By making it easier to develop and install plugins we hope that the community
+can grow even stronger and develop new plugins that we would never think about.
+
+You can discover available plugins on [Grafana.net](http://grafana.net)
+
+
+

+ 6 - 8
docs/sources/plugins/installation.md

@@ -4,14 +4,12 @@ page_description: Plugin installation for Grafana
 page_keywords: grafana, plugins, documentation
 ---
 
-# Plugins
-
-## Installing plugins
+# Installing plugins
 
 The easiest way to install plugins is by using the CLI tool grafana-cli which is bundled with grafana. Before any modification take place after modifying plugins, grafana-server needs to be restarted.
 
 ### Grafana plugin directory
-On Linux systems the grafana-cli will assume that the grafana plugin directory is "/var/lib/grafana/plugins". It's possible to override the directory which grafana-cli will operate on by specifing the --path flag. On Windows systems this parameter have to be specified for every call.
+On Linux systems the grafana-cli will assume that the grafana plugin directory is `/var/lib/grafana/plugins`. It's possible to override the directory which grafana-cli will operate on by specifying the --path flag. On Windows systems this parameter have to be specified for every call.
 
 ### Grafana-cli commands
 
@@ -30,14 +28,14 @@ List installed plugins
 grafana-cli plugins ls
 ```
 
-Upgrade all installed plugins
+Update all installed plugins
 ```
-grafana-cli plugins upgrade-all
+grafana-cli plugins update-all
 ```
 
-Upgrade one plugin
+Update one plugin
 ```
-grafana-cli plugins upgrade <plugin-id>
+grafana-cli plugins update <plugin-id>
 ```
 
 Remove one plugin

+ 0 - 12
docs/sources/plugins/overview.md

@@ -1,12 +0,0 @@
----
-page_title: Plugin overview
-page_description: Plugins for Grafana
-page_keywords: grafana, plugins, documentation
----
-
-# Plugins
-
-From Grafana 3.0 not only datasource plugins are supported but also panel plugins and apps. Having panels as plugins make it easy to create and add any kind of panel, to show your data or improve your favorite dashboards. Apps is something new in Grafana that enables bundling of datasources, panels that belongs together.
-
-Grafana already have a strong community of contributors and plugin developers. By making it easier to develop and install plugins we hope that the community can grow even stronger and develop new plugins that we would never think about.
-

+ 5 - 16
docs/sources/plugins/panels.md

@@ -4,26 +4,15 @@ page_description: Panel plugins for Grafana
 page_keywords: grafana, plugins, documentation
 ---
 
- > Our goal is not to have a very extensive documentation but rather have actual code that people can look at. An example implementation of a datasource can be found in the grafana repo under /examples/panel-boilerplate-es5
 
 # Panels
 
-To interact with the rest of grafana the panel plugin need to export a class in the module.js.
-This class have to inherit from sdk.PanelCtrl or sdk.MetricsPanelCtrl and be exported as PanelCtrl.
+Panels are the main building blocks of dashboards.
 
-```javascript
-  return {
-    PanelCtrl: BoilerPlatePanelCtrl
-  };
-```
+## Panel development
 
-This class will be instantiated once for every panel of its kind in a dashboard and treated as an AngularJs controller.
+Examples
 
-## MetricsPanelCtrl or PanelCtrl
-
-MetricsPanelCtrl inherits from PanelCtrl and adds some common features for datasource usage. So if your Panel will be working with a datasource you should inherit from MetricsPanelCtrl. If don't need to access any datasource then you should inherit from PanelCtrl instead.
-
-## Implementing a MetricsPanelCtrl
-
-If you choose to inherit from MetricsPanelCtrl you should implement a function called refreshData that will take a datasource as in parameter when its time to get new data. Its recommended that the refreshData function calls the issueQueries in the base class but its not mandatory. An examples of such implementation can be found in our [example panel](https://github.com/grafana/grafana/blob/master/examples/panel-boilerplate-es5/module.js#L27-L38)
+- [clock-panel](https://github.com/grafana/clock-panel)
+- [singlestat-panel](https://github.com/grafana/grafana/blob/master/public/app/plugins/panel/singlestat/module.ts)
 

+ 1 - 0
docs/sources/versions.html_fragment

@@ -1,3 +1,4 @@
+<li><a class='version' href='/v3.0'>Version v3.0</a></li>
 <li><a class='version' href='/v2.6'>Version v2.6</a></li>
 <li><a class='version' href='/v2.5'>Version v2.5</a></li>
 <li><a class='version' href='/v2.1'>Version v2.1</a></li>

+ 0 - 3
karma.conf.js

@@ -8,10 +8,7 @@ module.exports = function(config) {
 
     // list of files / patterns to load in the browser
     files: [
-      'vendor/npm/es5-shim/es5-shim.js',
-      'vendor/npm/es5-shim/es5-sham.js',
       'vendor/npm/es6-shim/es6-shim.js',
-      'vendor/npm/es6-promise/dist/es6-promise.js',
       'vendor/npm/systemjs/dist/system.src.js',
       'test/test-main.js',
 

+ 2 - 1
latest.json

@@ -1,3 +1,4 @@
 {
-	"version": "2.1.1"
+	"stable": "2.6.0",
+	"testing": "3.0.0"
 }

+ 9 - 10
package.json

@@ -4,13 +4,13 @@
     "company": "Coding Instinct AB"
   },
   "name": "grafana",
-  "version": "3.0.0-pre1",
+  "version": "3.0.0-beta3",
   "repository": {
     "type": "git",
     "url": "http://github.com/grafana/grafana.git"
   },
   "devDependencies": {
-    "angular2": "2.0.0-beta.0",
+    "zone.js": "^0.6.6",
     "autoprefixer": "^6.3.3",
     "es6-promise": "^3.0.2",
     "es6-shim": "^0.35.0",
@@ -31,18 +31,18 @@
     "grunt-contrib-watch": "^0.6.1",
     "grunt-filerev": "^0.2.1",
     "grunt-git-describe": "~2.3.2",
-    "grunt-karma": "~0.12.1",
+    "grunt-karma": "~0.12.2",
     "grunt-ng-annotate": "^1.0.1",
     "grunt-notify": "^0.4.3",
     "grunt-postcss": "^0.8.0",
     "grunt-sass": "^1.1.0",
     "grunt-string-replace": "~1.2.1",
-    "grunt-systemjs-builder": "^0.2.5",
+    "grunt-systemjs-builder": "^0.2.6",
     "grunt-tslint": "^3.0.2",
     "grunt-typescript": "^0.8.0",
     "grunt-usemin": "3.0.0",
     "jshint-stylish": "~2.1.0",
-    "karma": "~0.13.15",
+    "karma": "0.13.22",
     "karma-chrome-launcher": "~0.2.2",
     "karma-coverage": "0.5.3",
     "karma-coveralls": "1.1.2",
@@ -53,10 +53,9 @@
     "mocha": "2.3.4",
     "phantomjs-prebuilt": "^2.1.3",
     "reflect-metadata": "0.1.2",
-    "rxjs": "5.0.0-beta.2",
+    "rxjs": "5.0.0-beta.4",
     "sass-lint": "^1.5.0",
-    "systemjs": "0.19.20",
-    "zone.js": "0.5.10"
+    "systemjs": "0.19.24"
   },
   "engines": {
     "node": "0.4.x",
@@ -68,7 +67,7 @@
   },
   "license": "Apache-2.0",
   "dependencies": {
-    "es5-shim": "^4.4.1",
+    "eventemitter3": "^1.2.0",
     "grunt-jscs": "~1.5.x",
     "grunt-sass-lint": "^0.1.0",
     "grunt-sync": "^0.4.1",
@@ -76,7 +75,7 @@
     "lodash": "^2.4.1",
     "remarkable": "^1.6.2",
     "sinon": "1.16.1",
-    "systemjs-builder": "^0.15.7",
+    "systemjs-builder": "^0.15.13",
     "tether": "^1.2.0",
     "tether-drop": "^1.4.2",
     "tslint": "^3.4.0",

+ 16 - 11
packaging/publish/publish.sh

@@ -1,17 +1,22 @@
 #! /usr/bin/env bash
 
-version=2.6.0
+deb_ver=3.0.0-beta31460381868
+rpm_ver=3.0.0-beta31460381868
 
-wget https://grafanarel.s3.amazonaws.com/builds/grafana_${version}_amd64.deb
+#rpm_ver=3.0.0-1
 
-package_cloud push grafana/stable/debian/jessie grafana_${version}_amd64.deb
-package_cloud push grafana/stable/debian/wheezy grafana_${version}_amd64.deb
-package_cloud push grafana/testing/debian/jessie grafana_${version}_amd64.deb
-package_cloud push grafana/testing/debian/wheezy grafana_${version}_amd64.deb
+#wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb
 
-wget https://grafanarel.s3.amazonaws.com/builds/grafana-${version}-1.x86_64.rpm
+#package_cloud push grafana/stable/debian/jessie grafana_${deb_ver}_amd64.deb
+#package_cloud push grafana/stable/debian/wheezy grafana_${deb_ver}_amd64.deb
 
-package_cloud push grafana/testing/el/6 grafana-${version}-1.x86_64.rpm
-package_cloud push grafana/testing/el/7 grafana-${version}-1.x86_64.rpm
-package_cloud push grafana/stable/el/7 grafana-${version}-1.x86_64.rpm
-package_cloud push grafana/stable/el/6 grafana-${version}-1.x86_64.rpm
+#package_cloud push grafana/testing/debian/jessie grafana_${deb_ver}_amd64.deb
+#package_cloud push grafana/testing/debian/wheezy grafana_${deb_ver}_amd64.deb
+
+#wget https://grafanarel.s3.amazonaws.com/builds/grafana-${rpm_ver}.x86_64.rpm
+
+#package_cloud push grafana/testing/el/6 grafana-${rpm_ver}.x86_64.rpm
+#package_cloud push grafana/testing/el/7 grafana-${rpm_ver}.x86_64.rpm
+
+# package_cloud push grafana/stable/el/7 grafana-${version}-1.x86_64.rpm
+# package_cloud push grafana/stable/el/6 grafana-${version}-1.x86_64.rpm

+ 17 - 0
pkg/api/api.go

@@ -29,6 +29,8 @@ func Register(r *macaron.Macaron) {
 
 	// authed views
 	r.Get("/profile/", reqSignedIn, Index)
+	r.Get("/profile/password", reqSignedIn, Index)
+	r.Get("/profile/switch-org/:id", reqSignedIn, ChangeActiveOrgAndRedirectToHome)
 	r.Get("/org/", reqSignedIn, Index)
 	r.Get("/org/new", reqSignedIn, Index)
 	r.Get("/datasources/", reqSignedIn, Index)
@@ -45,6 +47,8 @@ func Register(r *macaron.Macaron) {
 	r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, Index)
 	r.Get("/admin/stats", reqGrafanaAdmin, Index)
 
+	r.Get("/styleguide", reqSignedIn, Index)
+
 	r.Get("/plugins", reqSignedIn, Index)
 	r.Get("/plugins/:id/edit", reqSignedIn, Index)
 	r.Get("/plugins/:id/page/:page", reqSignedIn, Index)
@@ -94,10 +98,15 @@ func Register(r *macaron.Macaron) {
 			r.Put("/", bind(m.UpdateUserCommand{}), wrap(UpdateSignedInUser))
 			r.Post("/using/:id", wrap(UserSetUsingOrg))
 			r.Get("/orgs", wrap(GetSignedInUserOrgList))
+
 			r.Post("/stars/dashboard/:id", wrap(StarDashboard))
 			r.Delete("/stars/dashboard/:id", wrap(UnstarDashboard))
+
 			r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword))
 			r.Get("/quotas", wrap(GetUserQuotas))
+
+			r.Get("/preferences", wrap(GetUserPreferences))
+			r.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateUserPreferences))
 		})
 
 		// users (admin permission required)
@@ -128,6 +137,9 @@ func Register(r *macaron.Macaron) {
 			r.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite))
 			r.Patch("/invites/:code/revoke", wrap(RevokeInvite))
 
+			// prefs
+			r.Get("/preferences", wrap(GetOrgPreferences))
+			r.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateOrgPreferences))
 		}, reqOrgAdmin)
 
 		// create new org
@@ -162,6 +174,11 @@ func Register(r *macaron.Macaron) {
 			r.Delete("/:id", wrap(DeleteApiKey))
 		}, reqOrgAdmin)
 
+		// Preferences
+		r.Group("/preferences", func() {
+			r.Post("/set-home-dash", bind(m.SavePreferencesCommand{}), wrap(SetHomeDashboard))
+		})
+
 		// Data sources
 		r.Group("/datasources", func() {
 			r.Get("/", GetDataSources)

+ 1 - 0
pkg/api/avatar/avatar.go

@@ -116,6 +116,7 @@ func (this *service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	if avatar.Expired() {
 		if err := avatar.Update(); err != nil {
 			log.Trace("avatar update error: %v", err)
+			avatar = this.notFound
 		}
 	}
 

+ 18 - 0
pkg/api/dashboard.go

@@ -159,6 +159,24 @@ func canEditDashboard(role m.RoleType) bool {
 }
 
 func GetHomeDashboard(c *middleware.Context) {
+	prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
+	if err := bus.Dispatch(&prefsQuery); err != nil {
+		c.JsonApiErr(500, "Failed to get preferences", err)
+	}
+
+	if prefsQuery.Result.HomeDashboardId != 0 {
+		slugQuery := m.GetDashboardSlugByIdQuery{Id: prefsQuery.Result.HomeDashboardId}
+		err := bus.Dispatch(&slugQuery)
+		if err != nil {
+			c.JsonApiErr(500, "Failed to get slug from database", err)
+			return
+		}
+
+		dashRedirect := dtos.DashboardRedirect{RedirectUri: "db/" + slugQuery.Result}
+		c.JSON(200, &dashRedirect)
+		return
+	}
+
 	filePath := path.Join(setting.StaticRootPath, "dashboards/home.json")
 	file, err := os.Open(filePath)
 	if err != nil {

+ 5 - 0
pkg/api/dtos/models.go

@@ -33,6 +33,7 @@ type CurrentUser struct {
 	OrgRole        m.RoleType `json:"orgRole"`
 	IsGrafanaAdmin bool       `json:"isGrafanaAdmin"`
 	GravatarUrl    string     `json:"gravatarUrl"`
+	Timezone       string     `json:"timezone"`
 }
 
 type DashboardMeta struct {
@@ -57,6 +58,10 @@ type DashboardFullWithMeta struct {
 	Dashboard *simplejson.Json `json:"dashboard"`
 }
 
+type DashboardRedirect struct {
+	RedirectUri string `json:"redirectUri"`
+}
+
 type DataSource struct {
 	Id                int64            `json:"id"`
 	OrgId             int64            `json:"orgId"`

+ 13 - 0
pkg/api/dtos/prefs.go

@@ -0,0 +1,13 @@
+package dtos
+
+type Prefs struct {
+	Theme           string `json:"theme"`
+	HomeDashboardId int64  `json:"homeDashboardId"`
+	Timezone        string `json:"timezone"`
+}
+
+type UpdatePrefsCmd struct {
+	Theme           string `json:"theme"`
+	HomeDashboardId int64  `json:"homeDashboardId"`
+	Timezone        string `json:"timezone"`
+}

+ 1 - 1
pkg/api/frontendsettings.go

@@ -59,7 +59,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
 			defaultDatasource = ds.Name
 		}
 
-		if len(ds.JsonData.MustMap()) > 0 {
+		if ds.JsonData != nil {
 			dsMap["jsonData"] = ds.JsonData
 		}
 

+ 9 - 1
pkg/api/index.go

@@ -2,6 +2,7 @@ package api
 
 import (
 	"github.com/grafana/grafana/pkg/api/dtos"
+	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/plugins"
@@ -14,6 +15,12 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 		return nil, err
 	}
 
+	prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
+	if err := bus.Dispatch(&prefsQuery); err != nil {
+		return nil, err
+	}
+	prefs := prefsQuery.Result
+
 	var data = dtos.IndexViewData{
 		User: &dtos.CurrentUser{
 			Id:             c.UserId,
@@ -21,12 +28,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
 			Login:          c.Login,
 			Email:          c.Email,
 			Name:           c.Name,
-			LightTheme:     c.Theme == "light",
 			OrgId:          c.OrgId,
 			OrgName:        c.OrgName,
 			OrgRole:        c.OrgRole,
 			GravatarUrl:    dtos.GetGravatarUrl(c.Email),
 			IsGrafanaAdmin: c.IsGrafanaAdmin,
+			LightTheme:     prefs.Theme == "light",
+			Timezone:       prefs.Timezone,
 		},
 		Settings:           settings,
 		AppUrl:             setting.AppUrl,

+ 73 - 0
pkg/api/preferences.go

@@ -0,0 +1,73 @@
+package api
+
+import (
+	"github.com/grafana/grafana/pkg/api/dtos"
+	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/middleware"
+	m "github.com/grafana/grafana/pkg/models"
+)
+
+// POST /api/preferences/set-home-dash
+func SetHomeDashboard(c *middleware.Context, cmd m.SavePreferencesCommand) Response {
+
+	cmd.UserId = c.UserId
+	cmd.OrgId = c.OrgId
+
+	if err := bus.Dispatch(&cmd); err != nil {
+		return ApiError(500, "Failed to set home dashboard", err)
+	}
+
+	return ApiSuccess("Home dashboard set")
+}
+
+// GET /api/user/preferences
+func GetUserPreferences(c *middleware.Context) Response {
+	return getPreferencesFor(c.OrgId, c.UserId)
+}
+
+func getPreferencesFor(orgId int64, userId int64) Response {
+	prefsQuery := m.GetPreferencesQuery{UserId: userId, OrgId: orgId}
+
+	if err := bus.Dispatch(&prefsQuery); err != nil {
+		return ApiError(500, "Failed to get preferences", err)
+	}
+
+	dto := dtos.Prefs{
+		Theme:           prefsQuery.Result.Theme,
+		HomeDashboardId: prefsQuery.Result.HomeDashboardId,
+		Timezone:        prefsQuery.Result.Timezone,
+	}
+
+	return Json(200, &dto)
+}
+
+// PUT /api/user/preferences
+func UpdateUserPreferences(c *middleware.Context, dtoCmd dtos.UpdatePrefsCmd) Response {
+	return updatePreferencesFor(c.OrgId, c.UserId, &dtoCmd)
+}
+
+func updatePreferencesFor(orgId int64, userId int64, dtoCmd *dtos.UpdatePrefsCmd) Response {
+	saveCmd := m.SavePreferencesCommand{
+		UserId:          userId,
+		OrgId:           orgId,
+		Theme:           dtoCmd.Theme,
+		Timezone:        dtoCmd.Timezone,
+		HomeDashboardId: dtoCmd.HomeDashboardId,
+	}
+
+	if err := bus.Dispatch(&saveCmd); err != nil {
+		return ApiError(500, "Failed to save preferences", err)
+	}
+
+	return ApiSuccess("Preferences updated")
+}
+
+// GET /api/org/preferences
+func GetOrgPreferences(c *middleware.Context) Response {
+	return getPreferencesFor(c.OrgId, 0)
+}
+
+// PUT /api/org/preferences
+func UpdateOrgPreferences(c *middleware.Context, dtoCmd dtos.UpdatePrefsCmd) Response {
+	return updatePreferencesFor(c.OrgId, 0, &dtoCmd)
+}

+ 18 - 0
pkg/api/user.go

@@ -4,6 +4,7 @@ import (
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/util"
 )
 
@@ -109,6 +110,23 @@ func UserSetUsingOrg(c *middleware.Context) Response {
 	return ApiSuccess("Active organization changed")
 }
 
+// GET /profile/switch-org/:id
+func ChangeActiveOrgAndRedirectToHome(c *middleware.Context) {
+	orgId := c.ParamsInt64(":id")
+
+	if !validateUsingOrg(c.UserId, orgId) {
+		NotFoundHandler(c)
+	}
+
+	cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgId}
+
+	if err := bus.Dispatch(&cmd); err != nil {
+		NotFoundHandler(c)
+	}
+
+	c.Redirect(setting.AppSubUrl + "/")
+}
+
 func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) Response {
 	userQuery := m.GetUserByIdQuery{Id: c.UserId}
 

+ 16 - 9
pkg/cmd/grafana-cli/commands/commands.go

@@ -1,9 +1,10 @@
 package commands
 
 import (
+	"os"
+
 	"github.com/codegangsta/cli"
 	"github.com/grafana/grafana/pkg/cmd/grafana-cli/log"
-	"os"
 )
 
 func runCommand(command func(commandLine CommandLine) error) func(context *cli.Context) {
@@ -25,27 +26,33 @@ func runCommand(command func(commandLine CommandLine) error) func(context *cli.C
 var pluginCommands = []cli.Command{
 	{
 		Name:   "install",
-		Usage:  "install <plugin name>",
+		Usage:  "install <plugin id>",
 		Action: runCommand(installCommand),
 	}, {
 		Name:   "list-remote",
 		Usage:  "list remote available plugins",
 		Action: runCommand(listremoteCommand),
 	}, {
-		Name:   "upgrade",
-		Usage:  "upgrade <plugin name>",
-		Action: runCommand(upgradeCommand),
+		Name:    "update",
+		Usage:   "update <plugin id>",
+		Aliases: []string{"upgrade"},
+		Action:  runCommand(upgradeCommand),
 	}, {
-		Name:   "upgrade-all",
-		Usage:  "upgrades all your installed plugins",
-		Action: runCommand(upgradeAllCommand),
+		Name:    "update-all",
+		Aliases: []string{"upgrade-all"},
+		Usage:   "update all your installed plugins",
+		Action:  runCommand(upgradeAllCommand),
 	}, {
 		Name:   "ls",
 		Usage:  "list all installed plugins",
 		Action: runCommand(lsCommand),
+	}, {
+		Name:   "uninstall",
+		Usage:  "uninstall <plugin id>",
+		Action: runCommand(removeCommand),
 	}, {
 		Name:   "remove",
-		Usage:  "remove <plugin name>",
+		Usage:  "remove <plugin id>",
 		Action: runCommand(removeCommand),
 	},
 }

+ 25 - 20
pkg/cmd/grafana-cli/commands/install_command.go

@@ -5,10 +5,6 @@ import (
 	"bytes"
 	"errors"
 	"fmt"
-	"github.com/fatih/color"
-	"github.com/grafana/grafana/pkg/cmd/grafana-cli/log"
-	m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
-	s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
 	"io"
 	"io/ioutil"
 	"net/http"
@@ -16,6 +12,11 @@ import (
 	"path"
 	"regexp"
 	"strings"
+
+	"github.com/fatih/color"
+	"github.com/grafana/grafana/pkg/cmd/grafana-cli/log"
+	m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
+	s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
 )
 
 func validateInput(c CommandLine, pluginFolder string) error {
@@ -24,17 +25,16 @@ func validateInput(c CommandLine, pluginFolder string) error {
 		return errors.New("please specify plugin to install")
 	}
 
-	pluginDir := c.GlobalString("path")
-	if pluginDir == "" {
-		return errors.New("missing path flag")
+	pluginsDir := c.GlobalString("pluginsDir")
+	if pluginsDir == "" {
+		return errors.New("missing pluginsDir flag")
 	}
 
-	fileInfo, err := os.Stat(pluginDir)
+	fileInfo, err := os.Stat(pluginsDir)
 	if err != nil {
-		if err = os.MkdirAll(pluginDir, os.ModePerm); err != nil {
-			return errors.New("path is not a directory")
+		if err = os.MkdirAll(pluginsDir, os.ModePerm); err != nil {
+			return errors.New(fmt.Sprintf("pluginsDir (%s) is not a directory", pluginsDir))
 		}
-
 		return nil
 	}
 
@@ -46,7 +46,7 @@ func validateInput(c CommandLine, pluginFolder string) error {
 }
 
 func installCommand(c CommandLine) error {
-	pluginFolder := c.GlobalString("path")
+	pluginFolder := c.GlobalString("pluginsDir")
 	if err := validateInput(c, pluginFolder); err != nil {
 		return err
 	}
@@ -59,7 +59,7 @@ func installCommand(c CommandLine) error {
 
 func InstallPlugin(pluginName, version string, c CommandLine) error {
 	plugin, err := s.GetPlugin(pluginName, c.GlobalString("repo"))
-	pluginFolder := c.GlobalString("path")
+	pluginFolder := c.GlobalString("pluginsDir")
 	if err != nil {
 		return err
 	}
@@ -120,6 +120,7 @@ func RemoveGitBuildFromName(pluginName, filename string) string {
 }
 
 var retryCount = 0
+var permissionsDeniedMessage = "Could not create %s. Permission denied. Make sure you have write access to plugindir"
 
 func downloadFile(pluginName, filePath, url string) (err error) {
 	defer func() {
@@ -153,16 +154,16 @@ func downloadFile(pluginName, filePath, url string) (err error) {
 		newFile := path.Join(filePath, RemoveGitBuildFromName(pluginName, zf.Name))
 
 		if zf.FileInfo().IsDir() {
-			os.Mkdir(newFile, 0777)
+			err := os.Mkdir(newFile, 0777)
+			if PermissionsError(err) {
+				return fmt.Errorf(permissionsDeniedMessage, newFile)
+			}
 		} else {
 			dst, err := os.Create(newFile)
-			if err != nil {
-				if strings.Contains(err.Error(), "permission denied") {
-					return fmt.Errorf(
-						"Could not create file %s. permission deined. Make sure you have write access to plugindir",
-						newFile)
-				}
+			if PermissionsError(err) {
+				return fmt.Errorf(permissionsDeniedMessage, newFile)
 			}
+
 			defer dst.Close()
 			src, err := zf.Open()
 			if err != nil {
@@ -176,3 +177,7 @@ func downloadFile(pluginName, filePath, url string) (err error) {
 
 	return nil
 }
+
+func PermissionsError(err error) bool {
+	return err != nil && strings.Contains(err.Error(), "permission denied")
+}

+ 2 - 1
pkg/cmd/grafana-cli/commands/ls_command.go

@@ -3,6 +3,7 @@ package commands
 import (
 	"errors"
 	"fmt"
+
 	"github.com/fatih/color"
 	"github.com/grafana/grafana/pkg/cmd/grafana-cli/log"
 	m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
@@ -31,7 +32,7 @@ var validateLsCommand = func(pluginDir string) error {
 }
 
 func lsCommand(c CommandLine) error {
-	pluginDir := c.GlobalString("path")
+	pluginDir := c.GlobalString("pluginsDir")
 	if err := validateLsCommand(pluginDir); err != nil {
 		return err
 	}

+ 2 - 1
pkg/cmd/grafana-cli/commands/remove_command.go

@@ -2,6 +2,7 @@ package commands
 
 import (
 	"errors"
+
 	"github.com/grafana/grafana/pkg/cmd/grafana-cli/log"
 	m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
 	services "github.com/grafana/grafana/pkg/cmd/grafana-cli/services"
@@ -11,7 +12,7 @@ var getPluginss func(path string) []m.InstalledPlugin = services.GetLocalPlugins
 var removePlugin func(pluginPath, id string) error = services.RemoveInstalledPlugin
 
 func removeCommand(c CommandLine) error {
-	pluginPath := c.GlobalString("path")
+	pluginPath := c.GlobalString("pluginsDir")
 	localPlugins := getPluginss(pluginPath)
 
 	log.Info("remove!\n")

+ 4 - 4
pkg/cmd/grafana-cli/commands/upgrade_all_command.go

@@ -28,9 +28,9 @@ func ShouldUpgrade(installed string, remote m.Plugin) bool {
 }
 
 func upgradeAllCommand(c CommandLine) error {
-	pluginDir := c.GlobalString("path")
+	pluginsDir := c.GlobalString("pluginsDir")
 
-	localPlugins := s.GetLocalPlugins(pluginDir)
+	localPlugins := s.GetLocalPlugins(pluginsDir)
 
 	remotePlugins, err := s.ListAllPlugins(c.GlobalString("repo"))
 
@@ -51,9 +51,9 @@ func upgradeAllCommand(c CommandLine) error {
 	}
 
 	for _, p := range pluginsToUpgrade {
-		log.Infof("Upgrading %v \n", p.Id)
+		log.Infof("Updating %v \n", p.Id)
 
-		s.RemoveInstalledPlugin(pluginDir, p.Id)
+		s.RemoveInstalledPlugin(pluginsDir, p.Id)
 		InstallPlugin(p.Id, "", c)
 	}
 

+ 3 - 3
pkg/cmd/grafana-cli/commands/upgrade_command.go

@@ -5,10 +5,10 @@ import (
 )
 
 func upgradeCommand(c CommandLine) error {
-	pluginDir := c.GlobalString("path")
+	pluginsDir := c.GlobalString("pluginsDir")
 	pluginName := c.Args().First()
 
-	localPlugin, err := s.ReadPlugin(pluginDir, pluginName)
+	localPlugin, err := s.ReadPlugin(pluginsDir, pluginName)
 
 	if err != nil {
 		return err
@@ -23,7 +23,7 @@ func upgradeCommand(c CommandLine) error {
 	for _, v := range remotePlugins.Plugins {
 		if localPlugin.Id == v.Id {
 			if ShouldUpgrade(localPlugin.Info.Version, v) {
-				s.RemoveInstalledPlugin(pluginDir, pluginName)
+				s.RemoveInstalledPlugin(pluginsDir, pluginName)
 				return InstallPlugin(localPlugin.Id, "", c)
 			}
 		}

+ 34 - 13
pkg/cmd/grafana-cli/main.go

@@ -2,22 +2,42 @@ package main
 
 import (
 	"fmt"
+	"os"
+	"runtime"
+
 	"github.com/codegangsta/cli"
 	"github.com/grafana/grafana/pkg/cmd/grafana-cli/commands"
 	"github.com/grafana/grafana/pkg/cmd/grafana-cli/log"
-	"os"
-	"runtime"
 )
 
 var version = "master"
 
-func getGrafanaPluginPath() string {
-	os := runtime.GOOS
-	if os == "windows" {
-		return "C:\\opt\\grafana\\plugins"
-	} else {
-		return "/var/lib/grafana/plugins"
+func getGrafanaPluginDir() string {
+	currentOS := runtime.GOOS
+	defaultNix := "/var/lib/grafana/plugins"
+
+	if currentOS == "windows" {
+		return "..\\data\\plugins"
+	}
+
+	pwd, err := os.Getwd()
+
+	if err != nil {
+		log.Error("Could not get current path. using default")
+		return defaultNix
+	}
+
+	if isDevenvironment(pwd) {
+		return "../data/plugins"
 	}
+
+	return defaultNix
+}
+
+func isDevenvironment(pwd string) bool {
+	// if ../conf/default.ini exists, grafana is not installed as package
+	_, err := os.Stat("../conf/default.ini")
+	return err != nil
 }
 
 func main() {
@@ -25,20 +45,21 @@ func main() {
 
 	app := cli.NewApp()
 	app.Name = "Grafana cli"
-	app.Author = "raintank"
+	app.Usage = ""
+	app.Author = "Grafana Project"
 	app.Email = "https://github.com/grafana/grafana"
 	app.Version = version
 	app.Flags = []cli.Flag{
 		cli.StringFlag{
-			Name:   "path",
-			Usage:  "path to the grafana installation",
-			Value:  getGrafanaPluginPath(),
+			Name:   "pluginsDir",
+			Usage:  "path to the grafana plugin directory",
+			Value:  getGrafanaPluginDir(),
 			EnvVar: "GF_PLUGIN_DIR",
 		},
 		cli.StringFlag{
 			Name:   "repo",
 			Usage:  "url to the plugin repository",
-			Value:  "https://grafana-net.raintank.io/api/plugins",
+			Value:  "https://grafana.net/api/plugins",
 			EnvVar: "GF_PLUGIN_REPO",
 		},
 		cli.BoolFlag{

+ 8 - 7
pkg/cmd/grafana-cli/services/services.go

@@ -4,24 +4,27 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"path"
+
 	"github.com/franela/goreq"
 	"github.com/grafana/grafana/pkg/cmd/grafana-cli/log"
 	m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models"
-	"path"
 )
 
 var IoHelper m.IoUtil = IoUtilImp{}
 
 func ListAllPlugins(repoUrl string) (m.PluginRepo, error) {
 	fullUrl := repoUrl + "/repo"
-	res, _ := goreq.Request{Uri: fullUrl, MaxRedirects: 3}.Do()
-
+	res, err := goreq.Request{Uri: fullUrl, MaxRedirects: 3}.Do()
+	if err != nil {
+		return m.PluginRepo{}, err
+	}
 	if res.StatusCode != 200 {
 		return m.PluginRepo{}, fmt.Errorf("Could not access %s statuscode %v", fullUrl, res.StatusCode)
 	}
 
 	var resp m.PluginRepo
-	err := res.Body.FromJsonTo(&resp)
+	err = res.Body.FromJsonTo(&resp)
 	if err != nil {
 		return m.PluginRepo{}, errors.New("Could not load plugin data")
 	}
@@ -66,9 +69,7 @@ func RemoveInstalledPlugin(pluginPath, id string) error {
 }
 
 func GetPlugin(pluginId, repoUrl string) (m.Plugin, error) {
-	resp, err := ListAllPlugins(repoUrl)
-	if err != nil {
-	}
+	resp, _ := ListAllPlugins(repoUrl)
 
 	for _, i := range resp.Plugins {
 		if i.Id == pluginId {

+ 6 - 1
pkg/login/ldap.go

@@ -318,7 +318,12 @@ func (a *ldapAuther) searchForUser(username string) (*ldapUserInfo, error) {
 		// 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 {
-			filter := strings.Replace(a.server.GroupSearchFilter, "%s", username, -1)
+			var filter_replace string
+			filter_replace = getLdapAttr(a.server.GroupSearchFilterUserAttribute, searchResult)
+			if a.server.GroupSearchFilterUserAttribute == "" {
+				filter_replace = getLdapAttr(a.server.Attr.Username, searchResult)
+			}
+			filter := strings.Replace(a.server.GroupSearchFilter, "%s", filter_replace, -1)
 
 			if ldapCfg.VerboseLogging {
 				log.Info("LDAP: Searching for user's groups: %s", filter)

+ 3 - 2
pkg/login/settings.go

@@ -27,8 +27,9 @@ type LdapServerConf struct {
 	SearchFilter  string   `toml:"search_filter"`
 	SearchBaseDNs []string `toml:"search_base_dns"`
 
-	GroupSearchFilter  string   `toml:"group_search_filter"`
-	GroupSearchBaseDNs []string `toml:"group_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"`
 }

+ 4 - 0
pkg/metrics/report_usage.go

@@ -10,6 +10,7 @@ import (
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/log"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/setting"
 )
 
@@ -56,6 +57,9 @@ func sendUsageStats() {
 	metrics["stats.users.count"] = statsQuery.Result.UserCount
 	metrics["stats.orgs.count"] = statsQuery.Result.OrgCount
 	metrics["stats.playlist.count"] = statsQuery.Result.PlaylistCount
+	metrics["stats.plugins.apps.count"] = len(plugins.Apps)
+	metrics["stats.plugins.panels.count"] = len(plugins.Panels)
+	metrics["stats.plugins.datasources.count"] = len(plugins.DataSources)
 
 	dsStats := m.GetDataSourceStatsQuery{}
 	if err := bus.Dispatch(&dsStats); err != nil {

+ 5 - 0
pkg/models/dashboards.go

@@ -148,3 +148,8 @@ type GetDashboardsQuery struct {
 	DashboardIds []int64
 	Result       *[]Dashboard
 }
+
+type GetDashboardSlugByIdQuery struct {
+	Id     int64
+	Result string
+}

+ 53 - 0
pkg/models/preferences.go

@@ -0,0 +1,53 @@
+package models
+
+import (
+	"errors"
+	"time"
+)
+
+// Typed errors
+var (
+	ErrPreferencesNotFound = errors.New("Preferences not found")
+)
+
+type Preferences struct {
+	Id              int64
+	OrgId           int64
+	UserId          int64
+	Version         int
+	HomeDashboardId int64
+	Timezone        string
+	Theme           string
+	Created         time.Time
+	Updated         time.Time
+}
+
+// ---------------------
+// QUERIES
+
+type GetPreferencesQuery struct {
+	Id     int64
+	OrgId  int64
+	UserId int64
+
+	Result *Preferences
+}
+
+type GetPreferencesWithDefaultsQuery struct {
+	Id     int64
+	OrgId  int64
+	UserId int64
+
+	Result *Preferences
+}
+
+// ---------------------
+// COMMANDS
+type SavePreferencesCommand struct {
+	UserId int64
+	OrgId  int64
+
+	HomeDashboardId int64  `json:"homeDashboardId"`
+	Timezone        string `json:"timezone"`
+	Theme           string `json:"theme"`
+}

+ 8 - 9
pkg/models/stats.go

@@ -21,15 +21,14 @@ type GetDataSourceStatsQuery struct {
 }
 
 type AdminStats struct {
-	UserCount         int `json:"user_count"`
-	OrgCount          int `json:"org_count"`
-	DashboardCount    int `json:"dashboard_count"`
-	DbSnapshotCount   int `json:"db_snapshot_count"`
-	DbTagCount        int `json:"db_tag_count"`
-	DataSourceCount   int `json:"data_source_count"`
-	PlaylistCount     int `json:"playlist_count"`
-	StarredDbCount    int `json:"starred_db_count"`
-	GrafanaAdminCount int `json:"grafana_admin_count"`
+	UserCount       int `json:"user_count"`
+	OrgCount        int `json:"org_count"`
+	DashboardCount  int `json:"dashboard_count"`
+	DbSnapshotCount int `json:"db_snapshot_count"`
+	DbTagCount      int `json:"db_tag_count"`
+	DataSourceCount int `json:"data_source_count"`
+	PlaylistCount   int `json:"playlist_count"`
+	StarredDbCount  int `json:"starred_db_count"`
 }
 
 type GetAdminStatsQuery struct {

+ 0 - 1
pkg/models/user.go

@@ -136,7 +136,6 @@ type SignedInUser struct {
 	Login          string
 	Name           string
 	Email          string
-	Theme          string
 	ApiKeyId       int64
 	IsGrafanaAdmin bool
 }

+ 21 - 0
pkg/services/sqlstore/dashboard.go

@@ -18,6 +18,7 @@ func init() {
 	bus.AddHandler("sql", DeleteDashboard)
 	bus.AddHandler("sql", SearchDashboards)
 	bus.AddHandler("sql", GetDashboardTags)
+	bus.AddHandler("sql", GetDashboardSlugById)
 }
 
 func SaveDashboard(cmd *m.SaveDashboardCommand) error {
@@ -255,3 +256,23 @@ func GetDashboards(query *m.GetDashboardsQuery) error {
 
 	return nil
 }
+
+type DashboardSlugDTO struct {
+	Slug string
+}
+
+func GetDashboardSlugById(query *m.GetDashboardSlugByIdQuery) error {
+	var rawSql = `SELECT slug from dashboard WHERE Id=?`
+	var slug = DashboardSlugDTO{}
+
+	exists, err := x.Sql(rawSql, query.Id).Get(&slug)
+
+	if err != nil {
+		return err
+	} else if exists == false {
+		return m.ErrDashboardNotFound
+	}
+
+	query.Result = slug.Slug
+	return nil
+}

+ 18 - 6
pkg/services/sqlstore/migrations/preferences_mig.go

@@ -4,17 +4,29 @@ import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
 
 func addPreferencesMigrations(mg *Migrator) {
 
-	preferencesV1 := Table{
+	mg.AddMigration("drop preferences table v2", NewDropTableMigration("preferences"))
+
+	preferencesV2 := Table{
 		Name: "preferences",
 		Columns: []*Column{
 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
-			{Name: "pref_id", Type: DB_Int, Nullable: false},
-			{Name: "pref_type", Type: DB_NVarchar, Length: 255, Nullable: false},
-			{Name: "pref_data", Type: DB_Text, Nullable: false},
+			{Name: "org_id", Type: DB_BigInt, Nullable: false},
+			{Name: "user_id", Type: DB_BigInt, Nullable: false},
+			{Name: "version", Type: DB_Int, Nullable: false},
+			{Name: "home_dashboard_id", Type: DB_BigInt, Nullable: false},
+			{Name: "timezone", Type: DB_NVarchar, Length: 50, Nullable: false},
+			{Name: "theme", Type: DB_NVarchar, Length: 20, Nullable: false},
+			{Name: "created", Type: DB_DateTime, Nullable: false},
+			{Name: "updated", Type: DB_DateTime, Nullable: false},
+		},
+		Indices: []*Index{
+			{Cols: []string{"org_id"}},
+			{Cols: []string{"user_id"}},
 		},
 	}
 
-	// create table
-	mg.AddMigration("create preferences table v1", NewAddTableMigration(preferencesV1))
+	mg.AddMigration("drop preferences table v3", NewDropTableMigration("preferences"))
 
+	// create table
+	mg.AddMigration("create preferences table v3", NewAddTableMigration(preferencesV2))
 }

+ 96 - 0
pkg/services/sqlstore/preferences.go

@@ -0,0 +1,96 @@
+package sqlstore
+
+import (
+	"time"
+
+	"github.com/grafana/grafana/pkg/bus"
+	m "github.com/grafana/grafana/pkg/models"
+)
+
+func init() {
+	bus.AddHandler("sql", GetPreferences)
+	bus.AddHandler("sql", GetPreferencesWithDefaults)
+	bus.AddHandler("sql", SavePreferences)
+}
+
+func GetPreferencesWithDefaults(query *m.GetPreferencesWithDefaultsQuery) error {
+
+	prefs := make([]*m.Preferences, 0)
+	filter := "(org_id=? AND user_id=?) OR (org_id=? AND user_id=0)"
+	err := x.Where(filter, query.OrgId, query.UserId, query.OrgId).
+		OrderBy("user_id ASC").
+		Find(&prefs)
+
+	if err != nil {
+		return err
+	}
+
+	res := &m.Preferences{
+		Theme:           "dark",
+		Timezone:        "browser",
+		HomeDashboardId: 0,
+	}
+
+	for _, p := range prefs {
+		if p.Theme != "" {
+			res.Theme = p.Theme
+		}
+		if p.Timezone != "" {
+			res.Timezone = p.Timezone
+		}
+		if p.HomeDashboardId != 0 {
+			res.HomeDashboardId = p.HomeDashboardId
+		}
+	}
+
+	query.Result = res
+	return nil
+}
+
+func GetPreferences(query *m.GetPreferencesQuery) error {
+
+	var prefs m.Preferences
+	exists, err := x.Where("org_id=? AND user_id=?", query.OrgId, query.UserId).Get(&prefs)
+
+	if err != nil {
+		return err
+	}
+
+	if exists {
+		query.Result = &prefs
+	} else {
+		query.Result = new(m.Preferences)
+	}
+
+	return nil
+}
+
+func SavePreferences(cmd *m.SavePreferencesCommand) error {
+	return inTransaction2(func(sess *session) error {
+
+		var prefs m.Preferences
+		exists, err := sess.Where("org_id=? AND user_id=?", cmd.OrgId, cmd.UserId).Get(&prefs)
+
+		if !exists {
+			prefs = m.Preferences{
+				UserId:          cmd.UserId,
+				OrgId:           cmd.OrgId,
+				HomeDashboardId: cmd.HomeDashboardId,
+				Timezone:        cmd.Timezone,
+				Theme:           cmd.Theme,
+				Created:         time.Now(),
+				Updated:         time.Now(),
+			}
+			_, err = sess.Insert(&prefs)
+			return err
+		} else {
+			prefs.HomeDashboardId = cmd.HomeDashboardId
+			prefs.Timezone = cmd.Timezone
+			prefs.Theme = cmd.Theme
+			prefs.Updated = time.Now()
+			prefs.Version += 1
+			_, err := sess.Id(prefs.Id).AllCols().Update(&prefs)
+			return err
+		}
+	})
+}

+ 1 - 6
pkg/services/sqlstore/stats.go

@@ -85,12 +85,7 @@ func GetAdminStats(query *m.GetAdminStatsQuery) error {
       (
         SELECT COUNT(DISTINCT ` + dialect.Quote("dashboard_id") + ` )
         FROM ` + dialect.Quote("star") + `
-      ) AS starred_db_count,
-      (
-        SELECT COUNT(*)
-        FROM ` + dialect.Quote("user") + `
-        WHERE ` + dialect.Quote("is_admin") + ` = 1
-      ) AS grafana_admin_count
+      ) AS starred_db_count
       `
 
 	var stats m.AdminStats

+ 0 - 1
pkg/services/sqlstore/user.go

@@ -273,7 +273,6 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error {
 	                u.email        as email,
 	                u.login        as login,
 									u.name         as name,
-									u.theme        as theme,
 	                org.name       as org_name,
 	                org_user.role  as org_role,
 	                org.id         as org_id

+ 44 - 0
public/app/core/components/dashboard_selector.ts

@@ -0,0 +1,44 @@
+///<reference path="../../headers/common.d.ts" />
+
+import config from 'app/core/config';
+import _ from 'lodash';
+import $ from 'jquery';
+import coreModule from 'app/core/core_module';
+
+var template = `
+<select class="gf-form-input" ng-model="ctrl.model" ng-options="f.value as f.text for f in ctrl.options"></select>
+`;
+
+export class DashboardSelectorCtrl {
+  model: any;
+  options: any;
+
+  /** @ngInject */
+  constructor(private backendSrv) {
+  }
+
+  $onInit() {
+    this.options = [{value: 0, text: 'Default'}];
+
+    return this.backendSrv.search({starred: true}).then(res => {
+      res.forEach(dash => {
+        this.options.push({value: dash.id, text: dash.title});
+      });
+    });
+  }
+}
+
+export function dashboardSelector() {
+  return {
+    restrict: 'E',
+    controller: DashboardSelectorCtrl,
+    bindToController: true,
+    controllerAs: 'ctrl',
+    template: template,
+    scope: {
+      model: '='
+    }
+  };
+}
+
+coreModule.directive('dashboardSelector', dashboardSelector);

+ 1 - 9
public/app/core/components/info_popover.ts

@@ -18,7 +18,7 @@ export function infoPopover() {
 
       var offset = attrs.offset || '0 -10px';
       var position = attrs.position || 'right middle';
-      var classes = 'drop-help';
+      var classes = 'drop-help drop-hide-out-of-bounds';
       if (attrs.wide) {
         classes += ' drop-wide';
       }
@@ -40,14 +40,6 @@ export function infoPopover() {
           }
         });
 
-      // inputElem.on('focus.popover', function() {
-      //   drop.open();
-      // });
-      //
-      // inputElem.on('blur.popover', function() {
-      //   close();
-      // });
-
         scope.$on('$destroy', function() {
           drop.destroy();
         });

+ 1 - 1
public/app/core/components/navbar/navbar.html

@@ -10,7 +10,7 @@
 
 	<a href="{{ctrl.titleUrl}}" class="navbar-page-btn" ng-show="ctrl.title">
 		<i class="{{ctrl.icon}}" ng-show="ctrl.icon"></i>
-		<img src="{{ctrl.iconUrl}}" ng-show="ctrl.iconUrl"></i>
+		<img ng-src="{{ctrl.iconUrl}}" ng-show="ctrl.iconUrl"></i>
 		{{ctrl.title}}
 	</a>
 

+ 0 - 4
public/app/core/components/sidemenu/sidemenu.html

@@ -21,10 +21,6 @@
 					<i class="{{::menuItem.icon}}" ng-show="::menuItem.icon"></i>
 					{{::menuItem.text}}
 				</a>
-				<a ng-click="menuItem.click()" ng-show="::menuItem.click">
-					<i class="{{::menuItem.icon}}"></i>
-					{{::menuItem.text}}
-				</a>
 			</li>
 		</ul>
 	</li>

+ 2 - 9
public/app/core/components/sidemenu/sidemenu.ts

@@ -72,9 +72,8 @@ export class SideMenuCtrl {
        this.orgMenu.push({
          text: "Switch to " + org.name,
          icon: "fa fa-fw fa-random",
-         click: () => {
-           this.switchOrg(org.orgId);
-         }
+         url: this.getUrl('/profile/switch-org/' + org.orgId),
+         target: '_self'
        });
      });
 
@@ -83,12 +82,6 @@ export class SideMenuCtrl {
      }
    });
  }
-
- switchOrg(orgId) {
-   this.backendSrv.post('/api/user/using/' + orgId).then(() => {
-     window.location.href = `${config.appSubUrl}/`;
-   });
- };
 }
 
 export function sideMenuDirective() {

+ 74 - 0
public/app/core/components/switch.ts

@@ -0,0 +1,74 @@
+///<reference path="../../headers/common.d.ts" />
+
+import config from 'app/core/config';
+import _ from 'lodash';
+import $ from 'jquery';
+import coreModule from 'app/core/core_module';
+import Drop from 'tether-drop';
+
+var template = `
+<label for="check-{{ctrl.id}}" class="gf-form-label {{ctrl.labelClass}} pointer">{{ctrl.label}}</label>
+<div class="gf-form-switch {{ctrl.switchClass}}" ng-if="ctrl.show">
+  <input id="check-{{ctrl.id}}" type="checkbox" ng-model="ctrl.checked" ng-change="ctrl.internalOnChange()">
+  <label for="check-{{ctrl.id}}" data-on="Yes" data-off="No"></label>
+</div>
+`;
+
+export class SwitchCtrl {
+  onChange: any;
+  checked: any;
+  show: any;
+  id: any;
+
+  /** @ngInject */
+  constructor($scope, private $timeout) {
+    this.show = true;
+    this.id = $scope.$id;
+  }
+
+  internalOnChange() {
+    return new Promise(resolve => {
+      this.$timeout(() => {
+        this.onChange();
+        resolve();
+      });
+    });
+  }
+
+}
+
+export function switchDirective() {
+  return {
+    restrict: 'E',
+    controller: SwitchCtrl,
+    controllerAs: 'ctrl',
+    bindToController: true,
+    scope: {
+      checked: "=",
+      label: "@",
+      labelClass: "@",
+      tooltip: "@",
+      switchClass: "@",
+      onChange: "&",
+    },
+    template: template,
+    link: (scope, elem) => {
+      if (scope.ctrl.tooltip) {
+        var drop = new Drop({
+          target: elem[0],
+          content: scope.ctrl.tooltip,
+          position: "right middle",
+          classes: 'drop-help',
+          openOn: 'hover',
+          hoverOpenDelay: 400,
+        });
+
+        scope.$on('$destroy', function() {
+          drop.destroy();
+        });
+      }
+    }
+  };
+}
+
+coreModule.directive('gfFormSwitch', switchDirective);

+ 4 - 0
public/app/core/core.ts

@@ -31,6 +31,8 @@ import {arrayJoin} from './directives/array_join';
 import {liveSrv} from './live/live_srv';
 import {Emitter} from './utils/emitter';
 import {layoutSelector} from './components/layout_selector/layout_selector';
+import {switchDirective} from './components/switch';
+import {dashboardSelector} from './components/dashboard_selector';
 import 'app/core/controllers/all';
 import 'app/core/services/all';
 import 'app/core/routes/routes';
@@ -49,7 +51,9 @@ export {
   colorPicker,
   liveSrv,
   layoutSelector,
+  switchDirective,
   infoPopover,
   Emitter,
   appEvents,
+  dashboardSelector,
 };

+ 114 - 0
public/app/core/directives/dropdown_typeahead.js

@@ -119,4 +119,118 @@ function (_, $, coreModule) {
       }
     };
   });
+
+  coreModule.default.directive('dropdownTypeahead2', function($compile) {
+
+    var inputTemplate = '<input type="text"'+
+      ' class="gf-form-input"' +
+      ' spellcheck="false" style="display:none"></input>';
+
+    var buttonTemplate = '<a class="gf-form-input dropdown-toggle"' +
+      ' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
+      ' data-placement="top"><i class="fa fa-plus"></i></a>';
+
+    return {
+      scope: {
+        menuItems: "=dropdownTypeahead2",
+        dropdownTypeaheadOnSelect: "&dropdownTypeaheadOnSelect",
+        model: '=ngModel'
+      },
+      link: function($scope, elem, attrs) {
+        var $input = $(inputTemplate);
+        var $button = $(buttonTemplate);
+        $input.appendTo(elem);
+        $button.appendTo(elem);
+
+        if (attrs.linkText) {
+          $button.html(attrs.linkText);
+        }
+
+        if (attrs.ngModel) {
+          $scope.$watch('model', function(newValue) {
+            _.each($scope.menuItems, function(item) {
+              _.each(item.submenu, function(subItem) {
+                if (subItem.value === newValue) {
+                  $button.html(subItem.text);
+                }
+              });
+            });
+          });
+        }
+
+        var typeaheadValues = _.reduce($scope.menuItems, function(memo, value, index) {
+          if (!value.submenu) {
+            value.click = 'menuItemSelected(' + index + ')';
+            memo.push(value.text);
+          } else {
+            _.each(value.submenu, function(item, subIndex) {
+              item.click = 'menuItemSelected(' + index + ',' + subIndex + ')';
+              memo.push(value.text + ' ' + item.text);
+            });
+          }
+          return memo;
+        }, []);
+
+        $scope.menuItemSelected = function(index, subIndex) {
+          var menuItem = $scope.menuItems[index];
+          var payload = {$item: menuItem};
+          if (menuItem.submenu && subIndex !== void 0) {
+            payload.$subItem = menuItem.submenu[subIndex];
+          }
+          $scope.dropdownTypeaheadOnSelect(payload);
+        };
+
+        $input.attr('data-provide', 'typeahead');
+        $input.typeahead({
+          source: typeaheadValues,
+          minLength: 1,
+          items: 10,
+          updater: function (value) {
+            var result = {};
+            _.each($scope.menuItems, function(menuItem) {
+              _.each(menuItem.submenu, function(submenuItem) {
+                if (value === (menuItem.text + ' ' + submenuItem.text)) {
+                  result.$subItem = submenuItem;
+                  result.$item = menuItem;
+                }
+              });
+            });
+
+            if (result.$item) {
+              $scope.$apply(function() {
+                $scope.dropdownTypeaheadOnSelect(result);
+              });
+            }
+
+            $input.trigger('blur');
+            return '';
+          }
+        });
+
+        $button.click(function() {
+          $button.hide();
+          $input.show();
+          $input.focus();
+        });
+
+        $input.keyup(function() {
+          elem.toggleClass('open', $input.val() === '');
+        });
+
+        $input.blur(function() {
+          $input.hide();
+          $input.val('');
+          $button.show();
+          $button.focus();
+          // clicking the function dropdown menu wont
+          // work if you remove class at once
+          setTimeout(function() {
+            elem.removeClass('open');
+          }, 200);
+        });
+
+        $compile(elem.contents())($scope);
+      }
+    };
+  });
 });

+ 1 - 1
public/app/core/directives/plugin_component.ts

@@ -169,7 +169,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
         return System.import(model.module).then(function(appModule) {
           return {
             baseUrl: model.baseUrl,
-            name: 'app-config-' + model.pluginId,
+            name: 'app-config-' + model.id,
             bindings: {appModel: "=", appEditCtrl: "="},
             attrs: {"app-model": "ctrl.model", "app-edit-ctrl": "ctrl"},
             Component: appModule.ConfigCtrl,

+ 9 - 5
public/app/core/routes/dashboard_loaders.js

@@ -4,13 +4,17 @@ define([
 function (coreModule) {
   "use strict";
 
-  coreModule.default.controller('LoadDashboardCtrl', function($scope, $routeParams, dashboardLoaderSrv, backendSrv) {
+  coreModule.default.controller('LoadDashboardCtrl', function($scope, $routeParams, dashboardLoaderSrv, backendSrv, $location) {
 
     if (!$routeParams.slug) {
-      backendSrv.get('/api/dashboards/home').then(function(result) {
-        var meta = result.meta;
-        meta.canSave = meta.canShare = meta.canStar = false;
-        $scope.initDashboard(result, $scope);
+      backendSrv.get('/api/dashboards/home').then(function(homeDash) {
+        if (homeDash.redirectUri) {
+          $location.path('dashboard/' + homeDash.redirectUri);
+        } else {
+          var meta = homeDash.meta;
+          meta.canSave = meta.canShare = meta.canStar = false;
+          $scope.initDashboard(homeDash, $scope);
+        }
       });
       return;
     }

+ 7 - 3
public/app/core/routes/routes.ts

@@ -88,16 +88,20 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
     resolve: loadOrgBundle,
   })
   .when('/profile', {
-    templateUrl: 'public/app/features/profile/partials/profile.html',
+    templateUrl: 'public/app/features/org/partials/profile.html',
     controller : 'ProfileCtrl',
+    controllerAs: 'ctrl',
+    resolve: loadOrgBundle,
   })
   .when('/profile/password', {
-    templateUrl: 'public/app/features/profile/partials/password.html',
+    templateUrl: 'public/app/features/org/partials/change_password.html',
     controller : 'ChangePasswordCtrl',
+    resolve: loadOrgBundle,
   })
   .when('/profile/select-org', {
-    templateUrl: 'public/app/features/profile/partials/select_org.html',
+    templateUrl: 'public/app/features/org/partials/select_org.html',
     controller : 'SelectOrgCtrl',
+    resolve: loadOrgBundle,
   })
   // ADMIN
   .when('/admin', {

+ 0 - 47
public/app/core/services/context_srv.js

@@ -1,47 +0,0 @@
-define([
-  'angular',
-  'lodash',
-  '../core_module',
-  'app/core/store',
-  'app/core/config',
-],
-function (angular, _, coreModule, store, config) {
-  'use strict';
-
-  coreModule.default.service('contextSrv', function() {
-
-    function User() {
-      if (config.bootData.user) {
-        _.extend(this, config.bootData.user);
-      }
-    }
-
-    this.hasRole = function(role) {
-      return this.user.orgRole === role;
-    };
-
-    this.setPinnedState = function(val) {
-      this.pinned = val;
-      store.set('grafana.sidemenu.pinned', val);
-    };
-
-    this.toggleSideMenu = function() {
-      this.sidemenu = !this.sidemenu;
-      if (!this.sidemenu) {
-        this.setPinnedState(false);
-      }
-    };
-
-    this.pinned = store.getBool('grafana.sidemenu.pinned', false);
-    if (this.pinned) {
-      this.sidemenu = true;
-    }
-
-    this.version = config.buildInfo.version;
-    this.lightTheme = false;
-    this.user = new User();
-    this.isSignedIn = this.user.isSignedIn;
-    this.isGrafanaAdmin = this.user.isGrafanaAdmin;
-    this.isEditor = this.hasRole('Editor') || this.hasRole('Admin');
-  });
-});

+ 73 - 0
public/app/core/services/context_srv.ts

@@ -0,0 +1,73 @@
+///<reference path="../../headers/common.d.ts" />
+
+import config from 'app/core/config';
+import _ from 'lodash';
+import coreModule from 'app/core/core_module';
+import store from 'app/core/store';
+
+export class User {
+  isGrafanaAdmin: any;
+  isSignedIn: any;
+  orgRole: any;
+
+  constructor() {
+    if (config.bootData.user) {
+      _.extend(this, config.bootData.user);
+    }
+  }
+}
+
+export class ContextSrv {
+  pinned: any;
+  version: any;
+  user: User;
+  isSignedIn: any;
+  isGrafanaAdmin: any;
+  isEditor: any;
+  sidemenu: any;
+  lightTheme: any;
+
+  constructor() {
+    this.pinned = store.getBool('grafana.sidemenu.pinned', false);
+    if (this.pinned) {
+      this.sidemenu = true;
+    }
+
+    if (!config.buildInfo) {
+      config.buildInfo = {};
+    }
+    if (!config.bootData) {
+      config.bootData = {user: {}, settings: {}};
+    }
+
+    this.version = config.buildInfo.version;
+    this.lightTheme = false;
+    this.user = new User();
+    this.isSignedIn = this.user.isSignedIn;
+    this.isGrafanaAdmin = this.user.isGrafanaAdmin;
+    this.isEditor = this.hasRole('Editor') || this.hasRole('Admin');
+  }
+
+  hasRole(role) {
+    return this.user.orgRole === role;
+  }
+
+  setPinnedState(val) {
+    this.pinned = val;
+    store.set('grafana.sidemenu.pinned', val);
+  }
+
+  toggleSideMenu() {
+    this.sidemenu = !this.sidemenu;
+    if (!this.sidemenu) {
+      this.setPinnedState(false);
+    }
+  }
+}
+
+var contextSrv = new ContextSrv();
+export {contextSrv};
+
+coreModule.factory('contextSrv', function() {
+  return contextSrv;
+});

+ 4 - 1
public/app/core/services/popover_srv.ts

@@ -46,9 +46,12 @@ function popoverSrv($compile, $rootScope) {
     drop.on('close', () => {
       popoverScope.dismiss({fromDropClose: true});
       destroyDrop();
+      if (options.onClose) {
+        options.onClose();
+      }
     });
 
-    drop.open();
+    setTimeout(() => { drop.open(); }, 10);
   };
 }
 

+ 1 - 1
public/app/core/time_series2.ts

@@ -170,7 +170,7 @@ export default class TimeSeries {
   }
 
   isMsResolutionNeeded() {
-    for (var i = 0; i<this.datapoints.length; i++) {
+    for (var i = 0; i < this.datapoints.length; i++) {
       if (this.datapoints[i][0] !== null) {
         var timestamp = this.datapoints[i][0].toString();
         if (timestamp.length === 13 && (timestamp % 1000) !== 0) {

+ 8 - 29
public/app/core/utils/emitter.ts

@@ -1,6 +1,6 @@
 ///<reference path="../../headers/common.d.ts" />
 
-import {Subject} from 'vendor/npm/rxjs/Subject';
+import EventEmitter from 'eventemitter3';
 
 var hasOwnProp = {}.hasOwnProperty;
 
@@ -9,48 +9,27 @@ function createName(name) {
 }
 
 export class Emitter {
-  subjects: any;
+  emitter: any;
 
   constructor() {
-    this.subjects = {};
+    this.emitter = new EventEmitter();
   }
 
   emit(name, data?) {
-    var fnName = createName(name);
-    this.subjects[fnName] || (this.subjects[fnName] = new Subject());
-    this.subjects[fnName].next(data);
+    this.emitter.emit(name, data);
   }
 
   on(name, handler, scope?) {
-    var fnName = createName(name);
-    this.subjects[fnName] || (this.subjects[fnName] = new Subject());
-    var subscription = this.subjects[fnName].subscribe(handler);
+    this.emitter.on(name, handler);
 
     if (scope) {
       scope.$on('$destroy', function() {
-        subscription.unsubscribe();
+        this.emitter.off(name, handler);
       });
     }
-
-    return subscription;
-  };
-
-  off(name, handler) {
-    var fnName = createName(name);
-    if (this.subjects[fnName]) {
-      this.subjects[fnName].dispose();
-      delete this.subjects[fnName];
-    }
   }
 
-  dispose() {
-    var subjects = this.subjects;
-    for (var prop in subjects) {
-      if (hasOwnProp.call(subjects, prop)) {
-        subjects[prop].dispose();
-      }
-    }
-
-    this.subjects = {};
+  off(name, handler) {
+    this.emitter.off(name, handler);
   }
 }

+ 0 - 4
public/app/features/admin/partials/stats.html

@@ -22,10 +22,6 @@
 				<td>Total users</td>
 				<td>{{ctrl.stats.user_count}}</td>
 			</tr>
-			<tr>
-				<td>Total grafana admins</td>
-				<td>{{ctrl.stats.grafana_admin_count}}</td>
-			</tr>
 			<tr>
 				<td>Total organizations</td>
 				<td>{{ctrl.stats.org_count}}</td>

+ 0 - 3
public/app/features/all.js

@@ -7,8 +7,5 @@ define([
   './playlist/all',
   './snapshot/all',
   './panel/all',
-  './profile/profileCtrl',
-  './profile/changePasswordCtrl',
-  './profile/selectOrgCtrl',
   './styleguide/styleguide',
 ], function () {});

+ 4 - 0
public/app/features/dashboard/dashboardCtrl.js

@@ -134,6 +134,10 @@ function (angular, $, config, moment) {
       });
     };
 
+    $scope.timezoneChanged = function() {
+      $rootScope.$broadcast("refresh");
+    };
+
     $scope.formatDate = function(date) {
       return moment(date).format('MMM Do YYYY, h:mm:ss a');
     };

+ 64 - 4
public/app/features/dashboard/dashboardSrv.js

@@ -9,7 +9,7 @@ function (angular, $, _, moment) {
 
   var module = angular.module('grafana.services');
 
-  module.factory('dashboardSrv', function()  {
+  module.factory('dashboardSrv', function(contextSrv)  {
 
     function DashboardModel (data, meta) {
       if (!data) {
@@ -25,7 +25,7 @@ function (angular, $, _, moment) {
       this.originalTitle = this.title;
       this.tags = data.tags || [];
       this.style = data.style || "dark";
-      this.timezone = data.timezone || 'browser';
+      this.timezone = data.timezone || '';
       this.editable = data.editable !== false;
       this.hideControls = data.hideControls || false;
       this.sharedCrosshair = data.sharedCrosshair || false;
@@ -208,11 +208,19 @@ function (angular, $, _, moment) {
       });
     };
 
+    p.isTimezoneUtc = function() {
+      return this.getTimezone() === 'utc';
+    };
+
+    p.getTimezone = function() {
+      return this.timezone ? this.timezone : contextSrv.user.timezone;
+    };
+
     p._updateSchema = function(old) {
       var i, j, k;
       var oldVersion = this.schemaVersion;
       var panelUpgrades = [];
-      this.schemaVersion = 11;
+      this.schemaVersion = 12;
 
       if (oldVersion === this.schemaVersion) {
         return;
@@ -401,11 +409,63 @@ function (angular, $, _, moment) {
         });
       }
 
-      if (oldVersion < 11) {
+      if (oldVersion < 12) {
         // update template variables
         _.each(this.templating.list, function(templateVariable) {
           if (templateVariable.refresh) { templateVariable.refresh = 1; }
           if (!templateVariable.refresh) { templateVariable.refresh = 0; }
+          if (templateVariable.hideVariable) {
+            templateVariable.hide = 2;
+          } else if (templateVariable.hideLabel) {
+            templateVariable.hide = 1;
+          } else {
+            templateVariable.hide = 0;
+          }
+        });
+      }
+
+      if (oldVersion < 12) {
+        // update graph yaxes changes
+        panelUpgrades.push(function(panel) {
+          if (panel.type !== 'graph') { return; }
+          if (!panel.grid) { return; }
+
+          if (!panel.yaxes) {
+            panel.yaxes = [
+              {
+                show: panel['y-axis'],
+                min: panel.grid.leftMin,
+                max: panel.grid.leftMax,
+                logBase: panel.grid.leftLogBase,
+                format: panel.y_formats[0],
+                label: panel.leftYAxisLabel,
+              },
+              {
+                show: panel['y-axis'],
+                min: panel.grid.rightMin,
+                max: panel.grid.rightMax,
+                logBase: panel.grid.rightLogBase,
+                format: panel.y_formats[1],
+                label: panel.rightYAxisLabel,
+              }
+            ];
+
+            panel.xaxis = {
+              show: panel['x-axis'],
+            };
+
+            delete panel.grid.leftMin;
+            delete panel.grid.leftMax;
+            delete panel.grid.leftLogBase;
+            delete panel.grid.rightMin;
+            delete panel.grid.rightMax;
+            delete panel.grid.rightLogBase;
+            delete panel.y_formats;
+            delete panel.leftYAxisLabel;
+            delete panel.rightYAxisLabel;
+            delete panel['y-axis'];
+            delete panel['x-axis'];
+          }
         });
       }
 

+ 34 - 23
public/app/features/dashboard/partials/settings.html

@@ -19,10 +19,11 @@
 <div class="tabbed-view-body">
 	<div ng-if="editor.index == 0">
 
-		<div class="gf-form-group">
+		<div class="gf-form-group section">
+      <h5 class="section-heading">Details</h5>
 			<div class="gf-form">
 				<label class="gf-form-label width-7">Title</label>
-				<input type="text" class="gf-form-input max-width-25" ng-model='dashboard.title'></input>
+				<input type="text" class="gf-form-input width-25" ng-model='dashboard.title'></input>
 			</div>
 			<div class="gf-form">
 				<label class="gf-form-label width-7">Tags<tip>Press enter to a add tag</tip></label>
@@ -33,25 +34,35 @@
 			<div class="gf-form">
 				<label class="gf-form-label width-7">Timezone</label>
 				<div class="gf-form-select-wrapper">
-					<select ng-model="dashboard.timezone" class='gf-form-input' ng-options="f for f in ['browser','utc']"></select>
+					<select ng-model="dashboard.timezone" class='gf-form-input' ng-options="f.value as f.text for f in [{value: '', text: 'Default'}, {value: 'browser', text: 'Local browser time'},{value: 'utc', text: 'UTC'}]" ng-change="timezoneChanged()"></select>
 				</div>
 			</div>
 		</div>
 
-		<h5 class="section-heading">On/Off Toggles</h5>
-		<div class="gf-form-group">
-			<div class="gf-form-inline">
-				<div class="gf-form">
-					<editor-checkbox text="Editable" model="dashboard.editable"></editor-checkbox>
-				</div>
-				<div class="gf-form">
-					<editor-checkbox text="Hide Controls (CTRL+H)" model="dashboard.hideControls"></editor-checkbox>
-				</div>
-				<div class="gf-form">
-					<editor-checkbox text="Shared Crosshair (CTRL+O)" model="dashboard.sharedCrosshair"></editor-checkbox>
-				</div>
-			</div>
-		</div>
+    <div class="section">
+      <h5 class="section-heading">Toggles</h5>
+      <div class="gf-form-group">
+        <gf-form-switch class="gf-form"
+                        label="Editable"
+                        tooltip="Uncheck, then save and reload to disable all dashboard editing"
+                        checked="dashboard.editable"
+                        label-class="width-10">
+        </gf-form-switch>
+        <gf-form-switch class="gf-form"
+                        label="Hide Controls"
+                        tooltip="Hide row controls. Shortcut: CTRL+H"
+                        checked="dashboard.hideControls"
+                        label-class="width-10">
+        </gf-form-switch>
+        <gf-form-switch class="gf-form"
+                        label="Shared Crosshair"
+                        tooltip="Shared Crosshair line on all graphs. Shortcut: CTRL+O"
+                        checked="dashboard.sharedCrosshair"
+                        label-class="width-10">
+        </gf-form-switch>
+      </div>
+    </div>
+
 	</div>
 
 	<div ng-if="editor.index == 1">
@@ -62,19 +73,19 @@
 				<div class="gf-form">
 					<span class="gf-form-label">Title</span>
 					<input type="text" class="gf-form-input max-width-14" ng-model='row.title'></input>
-					<editor-checkbox text="Show title" model="row.showTitle"></editor-checkbox>
 				</div>
+				<gf-form-switch class="gf-form" label="Show title" checked="row.showTitle" switch-class="max-width-6"></gf-form-switch>
 
 				<div class="gf-form">
-					<button class="btn btn-inverse btn-mini" style="margin-right: 5px;" ng-click="dashboard.rows = _.without(dashboard.rows,row)">
-						<i class="fa fa-trash"></i>
-					</button>
-					<button class="btn btn-inverse btn-mini" ng-hide="$first" style="margin-right: 5px;" ng-click="_.move(dashboard.rows,$index,$index-1)">
+					<button class="btn btn-inverse gf-form-btn" ng-click="_.move(dashboard.rows,$index,$index-1)">
 						<i ng-class="{'invisible': $first}" class="fa fa-arrow-up"></i>
 					</button>
-					<button class="btn btn-inverse btn-mini" ng-hide="$last" style="margin-right: 5px;" ng-click="_.move(dashboard.rows,$index,$index+1)">
+					<button class="btn btn-inverse gf-from-btn" ng-click="_.move(dashboard.rows,$index,$index+1)">
 						<i ng-class="{'invisible': $last}" class="fa fa-arrow-down"></i>
 					</button>
+					<button class="btn btn-inverse gf-form-btn" click="dashboard.rows = _.without(dashboard.rows,row)">
+						<i class="fa fa-trash"></i>
+					</button>
 				</div>
 			</div>
 		</div>

+ 24 - 28
public/app/features/dashboard/partials/shareModal.html

@@ -38,31 +38,26 @@
 
 	<div ng-include src="'shareLinkOptions.html'"></div>
 
-	<div class="gf-form-group position-center">
-		<div class="gf-form width-30" >
+	<div class="gf-form-group section">
+		<div class="gf-form width-30">
 			<textarea rows="5" data-share-panel-url class="gf-form-input width-30" ng-model='iframeHtml'></textarea>
 		</div>
 	</div>
-	<div class="gf-form-group">
-		<div class="gf-form position-center">
-			<button class="btn btn-inverse" data-clipboard-text="{{iframeHtml}}" clipboard-button><i class="fa fa-clipboard"></i> Copy</button>
-		</div>
-	</div>
 </script>
 
 <script type="text/ng-template" id="shareLinkOptions.html">
-	<div class="gf-form-group position-center">
-		<div class="gf-form">
-			<span class="gf-form-label width-5">Include</span>
-			<editor-checkbox text="Current time range" model="options.forCurrent" change="buildUrl()"></editor-checkbox>
-		</div>
-		<div class="gf-form">
-			<span class="gf-form-label width-5">Include</span>
-			<editor-checkbox text="Template variables" model="options.includeTemplateVars" change="buildUrl()"></editor-checkbox>
-		</div>
+	<div class="gf-form-group section">
+		<gf-form-switch class="gf-form"
+			label="Current time range" label-class="width-12" switch-class="max-width-6"
+			checked="options.forCurrent" on-change="buildUrl()">
+		</gf-form-switch>
+		<gf-form-switch class="gf-form"
+			label="Template variables" label-class="width-12" switch-class="max-width-6"
+			checked="options.includeTemplateVars" on-change="buildUrl()">
+		</gf-form-switch>
 		<div class="gf-form">
-			<span class="gf-form-label width-5">Theme</span>
-			<div class="gf-form-select-wrapper max-width-10">
+			<span class="gf-form-label width-12">Theme</span>
+			<div class="gf-form-select-wrapper width-6">
 				<select class="gf-form-input" ng-model="options.theme" ng-options="f as f for f in ['current', 'dark', 'light']" ng-change="buildUrl()"></select>
 			</div>
 		</div>
@@ -75,18 +70,19 @@
 	</div>
 
 	<div ng-include src="'shareLinkOptions.html'"></div>
-	<div class="gf-form-group position-center">
-		<div class="gf-form-inline">
-
-			<div class="gf-form width-30">
-				<input type="text" data-share-panel-url class="gf-form-input" ng-model="shareUrl"></input>
-			</div>
-			<div class="gf-form pull-right">
-				<button class="btn btn-inverse pull-right" data-clipboard-text="{{shareUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy</button>
+	<div>
+		<div class="gf-form-group section">
+			<div class="gf-form-inline">
+				<div class="gf-form width-30">
+					<input type="text" data-share-panel-url class="gf-form-input" ng-model="shareUrl"></input>
+				</div>
+				<div class="gf-form pull-right">
+					<button class="btn btn-inverse pull-right" data-clipboard-text="{{shareUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy</button>
+				</div>
 			</div>
 		</div>
 	</div>
-	<div class="gf-form position-center" ng-show="modeSharePanel">
+	<div class="gf-form section" ng-show="modeSharePanel">
 		<a href="{{imageUrl}}" target="_blank"><i class="fa fa-camera"></i> Direct link rendered image</a>
 	</div>
 </script>
@@ -117,7 +113,7 @@
 			</p>
 		</div>
 
-		<div class="gf-form-group share-modal-options position-center">
+		<div class="gf-form-group share-modal-options">
 			<div class="gf-form" ng-if="step === 1">
 				<span class="gf-form-label width-12">Snapshot name</span>
 				<input type="text" ng-model="snapshot.name" class="gf-form-input max-width-15" >

+ 2 - 2
public/app/features/dashboard/submenu/submenu.html

@@ -1,7 +1,7 @@
 <div class="submenu-controls">
 	<ul ng-if="ctrl.dashboard.templating.list.length > 0">
-		<li ng-repeat="variable in ctrl.variables" ng-show="!variable.hideVariable" class="submenu-item">
-			<span class="submenu-item-label template-variable " ng-show="!variable.hideLabel">
+		<li ng-repeat="variable in ctrl.variables" ng-hide="variable.hide === 2" class="submenu-item">
+			<span class="submenu-item-label template-variable " ng-hide="variable.hide === 1">
 				{{variable.label || variable.name}}:
 			</span>
 			<value-select-dropdown variable="variable" on-updated="ctrl.variableUpdated(variable)" get-values-for-tag="ctrl.getValuesForTag(variable, tagKey)"></value-select-dropdown>

+ 2 - 2
public/app/features/dashboard/timepicker/timepicker.ts

@@ -44,7 +44,7 @@ export class TimePickerCtrl {
     var time = angular.copy(this.timeSrv.timeRange());
     var timeRaw = angular.copy(this.timeSrv.timeRange(false));
 
-    if (this.dashboard.timezone === 'browser') {
+    if (!this.dashboard.isTimezoneUtc()) {
       time.from.local();
       time.to.local();
       if (moment.isMoment(timeRaw.from)) {
@@ -125,7 +125,7 @@ export class TimePickerCtrl {
   }
 
   getAbsoluteMomentForTimezone(jsDate) {
-    return this.dashboard.timezone === 'browser' ? moment(jsDate) : moment(jsDate).utc();
+    return this.dashboard.isTimezoneUtc() ? moment(jsDate).utc() : moment(jsDate);
   }
 
   setRelativeFilter(timespan) {

+ 5 - 0
public/app/features/org/all.js

@@ -1,7 +1,12 @@
 define([
   './org_users_ctrl',
+  './profile_ctrl',
+  './org_users_ctrl',
+  './select_org_ctrl',
+  './change_password_ctrl',
   './newOrgCtrl',
   './userInviteCtrl',
   './orgApiKeysCtrl',
   './orgDetailsCtrl',
+  './prefs_control',
 ], function () {});

+ 0 - 0
public/app/features/profile/changePasswordCtrl.js → public/app/features/org/change_password_ctrl.js


+ 0 - 0
public/app/features/profile/partials/password.html → public/app/features/org/partials/change_password.html


+ 1 - 0
public/app/features/org/partials/orgDetails.html

@@ -19,6 +19,7 @@
 		</div>
 	</form>
 
+	<prefs-control mode="org"></prefs-control>
 
 	<h3 class="page-heading">Address</h3>
 

Некоторые файлы не были показаны из-за большого количества измененных файлов