diff --git a/Gopkg.lock b/Gopkg.lock index 0ed63562..cd2fde74 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,14 +1,6 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. -[[projects]] - digest = "1:ec2b97c119fc66f96b421f8798deb2f87cb4a5ee81cafeaf9b55420d035f8fea" - name = "github.com/andygrunwald/go-jira" - packages = ["."] - pruneopts = "UT" - revision = "0298784c4606cdf01e99644da115863c052a737c" - version = "v1.5.0" - [[projects]] digest = "1:606d068450c82b9ddaa21de992f73563754077f0f411235cdfe71d0903a268c3" name = "github.com/codegangsta/negroni" @@ -250,11 +242,26 @@ revision = "0298784c4606cdf01e99644da115863c052a737c" version = "v1.5.0" +[[projects]] + digest = "1:81e1c5cee195fca5de06e2540cb63eea727a850b7e5c213548e7f81521c97a57" + name = "gopkg.in/asn1-ber.v1" + packages = ["."] + pruneopts = "UT" + revision = "379148ca0225df7a432012b8df0355c2a2063ac0" + version = "v1.2" + +[[projects]] + digest = "1:93aaeb913621a3a53aaa78592c00f46d63e3bb0ea76e2d9b07327b50959a5778" + name = "gopkg.in/ldap.v2" + packages = ["."] + pruneopts = "UT" + revision = "bb7a9ca6e4fbc2129e3db588a34bc970ffe811a9" + version = "v2.5.1" + [solve-meta] analyzer-name = "dep" analyzer-version = 1 input-imports = [ - "github.com/andygrunwald/go-jira", "github.com/codegangsta/negroni", "github.com/dgrijalva/jwt-go", "github.com/documize/blackfriday", @@ -275,6 +282,7 @@ "golang.org/x/oauth2", "gopkg.in/alexcesaro/quotedprintable.v3", "gopkg.in/andygrunwald/go-jira.v1", + "gopkg.in/ldap.v2", ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index ebafca0b..38a94e1f 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -89,14 +89,10 @@ go-tests = true unused-packages = true -[[constraint]] - branch = "master" - name = "github.com/documize/unidecode" - [[constraint]] name = "github.com/documize/slug" version = "1.1.1" [[constraint]] - name = "github.com/andygrunwald/go-jira" + name = "gopkg.in/andygrunwald/go-jira.v1" version = "1.5.0" diff --git a/domain/auth/keycloak/endpoint.go b/domain/auth/keycloak/endpoint.go index 691f7658..cb90e10c 100644 --- a/domain/auth/keycloak/endpoint.go +++ b/domain/auth/keycloak/endpoint.go @@ -64,7 +64,7 @@ func (h *Handler) Sync(w http.ResponseWriter, r *http.Request) { } // Exit if not using Keycloak - if org.AuthProvider != "keycloak" { + if org.AuthProvider != ath.AuthProviderKeycloak { result.Message = "Error: skipping user sync with Keycloak as it is not the configured option" result.IsError = true response.WriteJSON(w, result) @@ -73,7 +73,7 @@ func (h *Handler) Sync(w http.ResponseWriter, r *http.Request) { } // Make Keycloak auth provider config - c := keycloakConfig{} + c := ath.KeycloakConfig{} err = json.Unmarshal([]byte(org.AuthConfig), &c) if err != nil { result.Message = "Error: unable read Keycloak configuration data" @@ -121,6 +121,7 @@ func (h *Handler) Sync(w http.ResponseWriter, r *http.Request) { insert = append(insert, k) } } + // Track the number of Keycloak users with missing data. missing := 0 @@ -153,7 +154,7 @@ func (h *Handler) Authenticate(w http.ResponseWriter, r *http.Request) { return } - a := keycloakAuthRequest{} + a := ath.KeycloakAuthRequest{} err = json.Unmarshal(body, &a) if err != nil { response.WriteBadRequestError(w, method, err.Error()) @@ -181,7 +182,7 @@ func (h *Handler) Authenticate(w http.ResponseWriter, r *http.Request) { ctx.OrgID = org.RefID // Fetch Keycloak auth provider config - ac := keycloakConfig{} + ac := ath.KeycloakConfig{} err = json.Unmarshal([]byte(org.AuthConfig), &ac) if err != nil { response.WriteBadRequestError(w, method, "Unable to unmarshall Keycloak Public Key") diff --git a/domain/auth/keycloak/keycloak.go b/domain/auth/keycloak/keycloak.go index b40dba91..cb8389ee 100644 --- a/domain/auth/keycloak/keycloak.go +++ b/domain/auth/keycloak/keycloak.go @@ -28,12 +28,13 @@ import ( "github.com/documize/community/domain" usr "github.com/documize/community/domain/user" "github.com/documize/community/model/account" + "github.com/documize/community/model/auth" "github.com/documize/community/model/user" "github.com/pkg/errors" ) // Fetch gets list of Keycloak users for specified Realm, Client Id -func Fetch(c keycloakConfig) (users []user.User, err error) { +func Fetch(c auth.KeycloakConfig) (users []user.User, err error) { users = []user.User{} form := url.Values{} @@ -71,7 +72,7 @@ func Fetch(c keycloakConfig) (users []user.User, err error) { return users, errors.New("Keycloak authentication failed " + res.Status) } - ka := keycloakAPIAuth{} + ka := auth.KeycloakAPIAuth{} err = json.Unmarshal(body, &ka) if err != nil { return users, err @@ -114,7 +115,7 @@ func Fetch(c keycloakConfig) (users []user.User, err error) { return users, errors.New("Keycloak users list call failed " + res.Status) } - kcUsers := []keycloakUser{} + kcUsers := []auth.KeycloakUser{} err = json.Unmarshal(body, &kcUsers) if err != nil { err = errors.Wrap(err, "cannot unmarshal Keycloak user list response") diff --git a/domain/auth/keycloak/model.go b/domain/auth/keycloak/model.go deleted file mode 100644 index 705ffaae..00000000 --- a/domain/auth/keycloak/model.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2016 Documize Inc. . All rights reserved. -// -// This software (Documize Community Edition) is licensed under -// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html -// -// You can operate outside the AGPL restrictions by purchasing -// Documize Enterprise Edition and obtaining a commercial license -// by contacting . -// -// https://documize.com - -package keycloak - -// Data received via Keycloak client library -type keycloakAuthRequest struct { - Domain string `json:"domain"` - Token string `json:"token"` - RemoteID string `json:"remoteId"` - Email string `json:"email"` - Username string `json:"username"` - Firstname string `json:"firstname"` - Lastname string `json:"lastname"` - Enabled bool `json:"enabled"` -} - -// Keycloak server configuration -type keycloakConfig struct { - URL string `json:"url"` - Realm string `json:"realm"` - ClientID string `json:"clientId"` - PublicKey string `json:"publicKey"` - AdminUser string `json:"adminUser"` - AdminPassword string `json:"adminPassword"` - Group string `json:"group"` - DisableLogout bool `json:"disableLogout"` - DefaultPermissionAddSpace bool `json:"defaultPermissionAddSpace"` -} - -// keycloakAPIAuth is returned when authenticating with Keycloak REST API. -type keycloakAPIAuth struct { - AccessToken string `json:"access_token"` -} - -// keycloakUser details user record returned by Keycloak -type keycloakUser struct { - ID string `json:"id"` - Username string `json:"username"` - Email string `json:"email"` - Firstname string `json:"firstName"` - Lastname string `json:"lastName"` - Enabled bool `json:"enabled"` -} diff --git a/domain/auth/ldap/endpoint.go b/domain/auth/ldap/endpoint.go new file mode 100644 index 00000000..c4997522 --- /dev/null +++ b/domain/auth/ldap/endpoint.go @@ -0,0 +1,376 @@ +// Copyright 2016 Documize Inc. . All rights reserved. +// +// This software (Documize Community Edition) is licensed under +// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html +// +// You can operate outside the AGPL restrictions by purchasing +// Documize Enterprise Edition and obtaining a commercial license +// by contacting . +// +// https://documize.com + +package ldap + +import ( + "crypto/tls" + // "database/sql" + // "encoding/json" + "fmt" + // "io/ioutil" + "net/http" + // "sort" + // "strings" + + "github.com/documize/community/core/env" + "github.com/documize/community/core/response" + // "github.com/documize/community/core/secrets" + // "github.com/documize/community/core/streamutil" + // "github.com/documize/community/core/stringutil" + "github.com/documize/community/domain" + // "github.com/documize/community/domain/auth" + // usr "github.com/documize/community/domain/user" + ath "github.com/documize/community/model/auth" + lm "github.com/documize/community/model/auth" + ld "gopkg.in/ldap.v2" + // "github.com/documize/community/model/user" +) + +// Handler contains the runtime information such as logging and database. +type Handler struct { + Runtime *env.Runtime + Store *domain.Store +} + +// Sync gets list of Keycloak users and inserts new users into Documize +// and marks Keycloak disabled users as inactive. +func (h *Handler) Sync(w http.ResponseWriter, r *http.Request) { + ctx := domain.GetRequestContext(r) + + if !ctx.Administrator { + response.WriteForbiddenError(w) + return + } + + var result struct { + Message string `json:"message"` + IsError bool `json:"isError"` + } + + // Org contains raw auth provider config + org, err := h.Store.Organization.GetOrganization(ctx, ctx.OrgID) + if err != nil { + result.Message = "Error: unable to get organization record" + result.IsError = true + response.WriteJSON(w, result) + h.Runtime.Log.Error(result.Message, err) + return + } + + // Exit if not using LDAP + if org.AuthProvider != ath.AuthProviderLDAP { + // result.Message = "Error: skipping user sync with LDAP as it is not the configured option" + // result.IsError = true + // response.WriteJSON(w, result) + // h.Runtime.Log.Info(result.Message) + // return + } + + // Make Keycloak auth provider config + c := lm.LDAPConfig{} + // err = json.Unmarshal([]byte(org.AuthConfig), &c) + // if err != nil { + // result.Message = "Error: unable read LDAP configuration data" + // result.IsError = true + // response.WriteJSON(w, result) + // h.Runtime.Log.Error(result.Message, err) + // return + // } + + c.ServerHost = "ldap.forumsys.com" + c.ServerPort = 389 + c.EncryptionType = "none" + c.BaseDN = "dc=example,dc=com" + c.BindDN = "cn=read-only-admin,dc=example,dc=com" + c.BindPassword = "password" + c.GroupFilter = "" + c.DisableLogout = false + c.DefaultPermissionAddSpace = false + + address := fmt.Sprintf("%s:%d", c.ServerHost, c.ServerPort) + + h.Runtime.Log.Info("Connecting to LDAP server") + + l, err := ld.Dial("tcp", address) + if err != nil { + result.Message = "Error: unable to dial LDAP server: " + err.Error() + result.IsError = true + response.WriteJSON(w, result) + h.Runtime.Log.Error(result.Message, err) + return + } + defer l.Close() + + if c.EncryptionType == "starttls" { + h.Runtime.Log.Info("Using StartTLS with LDAP server") + err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) + if err != nil { + result.Message = "Error: unable to startTLS with LDAP server: " + err.Error() + result.IsError = true + response.WriteJSON(w, result) + h.Runtime.Log.Error(result.Message, err) + return + } + } + + // Authenticate with LDAP server using admin credentials. + h.Runtime.Log.Info("Binding LDAP admin user") + err = l.Bind(c.BindDN, c.BindPassword) + if err != nil { + result.Message = "Error: unable to bind specified admin user to LDAP: " + err.Error() + result.IsError = true + response.WriteJSON(w, result) + h.Runtime.Log.Error(result.Message, err) + return + } + + // Get users from LDAP server by using filter + filter := "" + attrs := []string{} + if len(c.GroupFilter) > 0 { + filter = fmt.Sprintf("(&(objectClass=group)(cn=%s))", c.GroupFilter) + attrs = []string{"cn"} + } else { + filter = "(|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))" + attrs = []string{"dn", "cn", "givenName", "sn", "mail", "uid"} + } + + searchRequest := ld.NewSearchRequest( + c.BaseDN, + ld.ScopeWholeSubtree, ld.NeverDerefAliases, 0, 0, false, + filter, + attrs, + nil, + ) + + sr, err := l.Search(searchRequest) + if err != nil { + result.Message = "Error: unable to bind specified admin user to LDAP: " + err.Error() + result.IsError = true + response.WriteJSON(w, result) + h.Runtime.Log.Error(result.Message, err) + return + } + fmt.Printf("entries found: %d", len(sr.Entries)) + + for _, entry := range sr.Entries { + fmt.Printf("[%s] %s (%s %s) @ %s\n", + entry.GetAttributeValue("uid"), + entry.GetAttributeValue("cn"), + entry.GetAttributeValue("givenName"), + entry.GetAttributeValue("sn"), + entry.GetAttributeValue("mail")) + } + // // User list from LDAP + // kcUsers, err := Fetch(c) + // if err != nil { + // result.Message = "Error: unable to fetch Keycloak users: " + err.Error() + // result.IsError = true + // response.WriteJSON(w, result) + // h.Runtime.Log.Error(result.Message, err) + // return + // } + + // // User list from Documize + // dmzUsers, err := h.Store.User.GetUsersForOrganization(ctx, "", 99999) + // if err != nil { + // result.Message = "Error: unable to fetch Documize users" + // result.IsError = true + // response.WriteJSON(w, result) + // h.Runtime.Log.Error(result.Message, err) + // return + // } + + // sort.Slice(kcUsers, func(i, j int) bool { return kcUsers[i].Email < kcUsers[j].Email }) + // sort.Slice(dmzUsers, func(i, j int) bool { return dmzUsers[i].Email < dmzUsers[j].Email }) + + // insert := []user.User{} + + // for _, k := range kcUsers { + // exists := false + + // for _, d := range dmzUsers { + // if k.Email == d.Email { + // exists = true + // } + // } + + // if !exists { + // insert = append(insert, k) + // } + // } + + // // Track the number of Keycloak users with missing data. + // missing := 0 + + // // Insert new users into Documize + // for _, u := range insert { + // if len(u.Email) == 0 { + // missing++ + // } else { + // err = addUser(ctx, h.Runtime, h.Store, u, c.DefaultPermissionAddSpace) + // } + // } + + // result.Message = fmt.Sprintf("LDAP sync found %d users, %d new users added, %d users with missing data ignored", + // len(kcUsers), len(insert), missing) + + result.IsError = false + result.Message = "Sync complete with LDAP server" + + response.WriteJSON(w, result) + h.Runtime.Log.Info(result.Message) +} + +// Authenticate checks Keycloak authentication credentials. +// func (h *Handler) Authenticate(w http.ResponseWriter, r *http.Request) { +// method := "authenticate" +// ctx := domain.GetRequestContext(r) + +// defer streamutil.Close(r.Body) +// body, err := ioutil.ReadAll(r.Body) +// if err != nil { +// response.WriteBadRequestError(w, method, "Bad payload") +// h.Runtime.Log.Error(method, err) +// return +// } + +// a := ath.KeycloakAuthRequest{} +// err = json.Unmarshal(body, &a) +// if err != nil { +// response.WriteBadRequestError(w, method, err.Error()) +// h.Runtime.Log.Error(method, err) +// return +// } + +// a.Domain = strings.TrimSpace(strings.ToLower(a.Domain)) +// a.Domain = h.Store.Organization.CheckDomain(ctx, a.Domain) // TODO optimize by removing this once js allows empty domains +// a.Email = strings.TrimSpace(strings.ToLower(a.Email)) + +// // Check for required fields. +// if len(a.Email) == 0 { +// response.WriteUnauthorizedError(w) +// return +// } + +// org, err := h.Store.Organization.GetOrganizationByDomain(a.Domain) +// if err != nil { +// response.WriteUnauthorizedError(w) +// h.Runtime.Log.Error(method, err) +// return +// } + +// ctx.OrgID = org.RefID + +// // Fetch Keycloak auth provider config +// ac := ath.KeycloakConfig{} +// err = json.Unmarshal([]byte(org.AuthConfig), &ac) +// if err != nil { +// response.WriteBadRequestError(w, method, "Unable to unmarshall Keycloak Public Key") +// h.Runtime.Log.Error(method, err) +// return +// } + +// // Decode and prepare RSA Public Key used by keycloak to sign JWT. +// pkb, err := secrets.DecodeBase64([]byte(ac.PublicKey)) +// if err != nil { +// response.WriteBadRequestError(w, method, "Unable to base64 decode Keycloak Public Key") +// h.Runtime.Log.Error(method, err) +// return +// } +// pk := string(pkb) +// pk = fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", pk) + +// // Decode and verify Keycloak JWT +// claims, err := auth.DecodeKeycloakJWT(a.Token, pk) +// if err != nil { +// response.WriteBadRequestError(w, method, err.Error()) +// h.Runtime.Log.Info("decodeKeycloakJWT failed") +// return +// } + +// // Compare the contents from JWT with what we have. +// // Guards against MITM token tampering. +// if a.Email != claims["email"].(string) { +// response.WriteUnauthorizedError(w) +// h.Runtime.Log.Info(">> Start Keycloak debug") +// h.Runtime.Log.Info(a.Email) +// h.Runtime.Log.Info(claims["email"].(string)) +// h.Runtime.Log.Info(">> End Keycloak debug") +// return +// } + +// h.Runtime.Log.Info("keycloak logon attempt " + a.Email + " @ " + a.Domain) + +// u, err := h.Store.User.GetByDomain(ctx, a.Domain, a.Email) +// if err != nil && err != sql.ErrNoRows { +// response.WriteServerError(w, method, err) +// h.Runtime.Log.Error(method, err) +// return +// } + +// // Create user account if not found +// if err == sql.ErrNoRows { +// h.Runtime.Log.Info("keycloak add user " + a.Email + " @ " + a.Domain) + +// u = user.User{} +// u.Firstname = a.Firstname +// u.Lastname = a.Lastname +// u.Email = a.Email +// u.Initials = stringutil.MakeInitials(u.Firstname, u.Lastname) +// u.Salt = secrets.GenerateSalt() +// u.Password = secrets.GeneratePassword(secrets.GenerateRandomPassword(), u.Salt) + +// err = addUser(ctx, h.Runtime, h.Store, u, ac.DefaultPermissionAddSpace) +// if err != nil { +// response.WriteServerError(w, method, err) +// h.Runtime.Log.Error(method, err) +// return +// } +// } + +// // Password correct and active user +// if a.Email != strings.TrimSpace(strings.ToLower(u.Email)) { +// response.WriteUnauthorizedError(w) +// return +// } + +// // Attach user accounts and work out permissions. +// usr.AttachUserAccounts(ctx, *h.Store, org.RefID, &u) + +// // No accounts signals data integrity problem +// // so we reject login request. +// if len(u.Accounts) == 0 { +// response.WriteUnauthorizedError(w) +// h.Runtime.Log.Error(method, err) +// return +// } + +// // Abort login request if account is disabled. +// for _, ac := range u.Accounts { +// if ac.OrgID == org.RefID { +// if ac.Active == false { +// response.WriteUnauthorizedError(w) +// h.Runtime.Log.Error(method, err) +// return +// } +// break +// } +// } + +// // Generate JWT token +// authModel := ath.AuthenticationModel{} +// authModel.Token = auth.GenerateJWT(h.Runtime, u.RefID, org.RefID, a.Domain) +// authModel.User = u + +// response.WriteJSON(w, authModel) +// } diff --git a/domain/auth/ldap/ldap_test.go b/domain/auth/ldap/ldap_test.go new file mode 100644 index 00000000..28090c91 --- /dev/null +++ b/domain/auth/ldap/ldap_test.go @@ -0,0 +1,179 @@ +// Copyright 2016 Documize Inc. . All rights reserved. +// +// This software (Documize Community Edition) is licensed under +// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html +// +// You can operate outside the AGPL restrictions by purchasing +// Documize Enterprise Edition and obtaining a commercial license +// by contacting . +// +// https://documize.com + +package ldap + +import ( + "crypto/tls" + "fmt" + "testing" + + lm "github.com/documize/community/model/auth" + ld "gopkg.in/ldap.v2" +) + +func TestPublicLDAPServer(t *testing.T) { + c := lm.LDAPConfig{} + c.ServerHost = "ldap.forumsys.com" + c.ServerPort = 389 + c.EncryptionType = "none" + c.BaseDN = "dc=example,dc=com" + c.BindDN = "cn=read-only-admin,dc=example,dc=com" + c.BindPassword = "password" + c.GroupFilter = "" + + address := fmt.Sprintf("%s:%d", c.ServerHost, c.ServerPort) + + t.Log("Connecting to LDAP server ", address) + + l, err := ld.Dial("tcp", address) + if err != nil { + t.Error("Error: unable to dial LDAP server: ", err.Error()) + return + } + defer l.Close() + + if c.EncryptionType == "starttls" { + t.Log("Using StartTLS with LDAP server") + err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) + if err != nil { + t.Error("Error: unable to startTLS with LDAP server: ", err.Error()) + return + } + } + + // Authenticate with LDAP server using admin credentials. + t.Log("Binding LDAP admin user") + err = l.Bind(c.BindDN, c.BindPassword) + if err != nil { + t.Error("Error: unable to bind specified admin user to LDAP: ", err.Error()) + return + } + + // Get users from LDAP server by using filter + filter := "" + attrs := []string{} + if len(c.GroupFilter) > 0 { + filter = fmt.Sprintf("(&(objectClass=group)(cn=%s))", c.GroupFilter) + attrs = []string{"cn"} + } else { + filter = "(|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))" + attrs = []string{"dn", "cn", "givenName", "sn", "mail", "uid"} + } + + searchRequest := ld.NewSearchRequest( + c.BaseDN, + ld.ScopeWholeSubtree, ld.NeverDerefAliases, 0, 0, false, + filter, + attrs, + nil, + ) + + sr, err := l.Search(searchRequest) + if err != nil { + t.Error("Error: unable to execute directory search: ", err.Error()) + return + } + + t.Logf("entries found: %d", len(sr.Entries)) + if len(sr.Entries) == 0 { + t.Error("Received ZERO entries") + return + } + + for _, entry := range sr.Entries { + fmt.Printf("[%s] %s (%s %s) @ %s\n", + entry.GetAttributeValue("uid"), + entry.GetAttributeValue("cn"), + entry.GetAttributeValue("givenName"), + entry.GetAttributeValue("sn"), + entry.GetAttributeValue("mail")) + } +} + +func TestLocalLDAPServer(t *testing.T) { + c := lm.LDAPConfig{} + c.ServerHost = "127.0.0.1" + c.ServerPort = 389 + c.EncryptionType = "starttls" + c.BaseDN = "ou=people,dc=planetexpress,dc=com" + c.BindDN = "cn=admin,dc=planetexpress,dc=com" + c.BindPassword = "GoodNewsEveryone" + c.GroupFilter = "" + + address := fmt.Sprintf("%s:%d", c.ServerHost, c.ServerPort) + + t.Log("Connecting to LDAP server", address) + + l, err := ld.Dial("tcp", address) + if err != nil { + t.Error("Error: unable to dial LDAP server: ", err.Error()) + return + } + defer l.Close() + + if c.EncryptionType == "starttls" { + t.Log("Using StartTLS with LDAP server") + err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) + if err != nil { + t.Error("Error: unable to startTLS with LDAP server: ", err.Error()) + return + } + } + + // Authenticate with LDAP server using admin credentials. + t.Log("Binding LDAP admin user") + err = l.Bind(c.BindDN, c.BindPassword) + if err != nil { + t.Error("Error: unable to bind specified admin user to LDAP: ", err.Error()) + return + } + + // Get users from LDAP server by using filter + filter := "" + attrs := []string{} + if len(c.GroupFilter) > 0 { + filter = fmt.Sprintf("(&(objectClass=group)(cn=%s))", c.GroupFilter) + attrs = []string{"cn"} + } else { + filter = "(|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))" + attrs = []string{"dn", "cn", "givenName", "sn", "mail", "uid"} + } + + searchRequest := ld.NewSearchRequest( + c.BaseDN, + ld.ScopeWholeSubtree, ld.NeverDerefAliases, 0, 0, false, + filter, + attrs, + nil, + ) + + sr, err := l.Search(searchRequest) + if err != nil { + t.Error("Error: unable to execute directory search: ", err.Error()) + return + } + + t.Logf("entries found: %d", len(sr.Entries)) + if len(sr.Entries) == 0 { + t.Error("Received ZERO entries") + return + } + + for _, entry := range sr.Entries { + fmt.Printf("[%s] %s (%s %s) @ %s\n", + entry.GetAttributeValue("uid"), + entry.GetAttributeValue("cn"), + entry.GetAttributeValue("givenName"), + entry.GetAttributeValue("sn"), + entry.GetAttributeValue("mail")) + } +} diff --git a/domain/auth/auth.go b/domain/auth/secrets.go similarity index 72% rename from domain/auth/auth.go rename to domain/auth/secrets.go index a881c9bf..f96f74da 100644 --- a/domain/auth/auth.go +++ b/domain/auth/secrets.go @@ -21,10 +21,9 @@ import ( // StripAuthSecrets removes sensitive data from auth provider configuration func StripAuthSecrets(r *env.Runtime, provider, config string) string { switch provider { - case "documize": + case auth.AuthProviderDocumize: return config - break - case "keycloak": + case auth.AuthProviderKeycloak: c := auth.KeycloakConfig{} err := json.Unmarshal([]byte(config), &c) if err != nil { @@ -42,7 +41,23 @@ func StripAuthSecrets(r *env.Runtime, provider, config string) string { } return string(j) - break + case auth.AuthProviderLDAP: + c := auth.LDAPConfig{} + err := json.Unmarshal([]byte(config), &c) + if err != nil { + r.Log.Error("StripAuthSecrets", err) + return config + } + c.BindDN = "" + c.BindPassword = "" + + j, err := json.Marshal(c) + if err != nil { + r.Log.Error("StripAuthSecrets", err) + return config + } + + return string(j) } return config diff --git a/gui/app/components/customize/auth-settings.js b/gui/app/components/customize/auth-settings.js index 36f58dea..d9d12950 100644 --- a/gui/app/components/customize/auth-settings.js +++ b/gui/app/components/customize/auth-settings.js @@ -36,6 +36,7 @@ export default Component.extend(Notifier, { init() { this._super(...arguments); + this.keycloakConfig = { url: '', realm: '', diff --git a/gui/app/constants/constants.js b/gui/app/constants/constants.js index ce804f8d..ea526018 100644 --- a/gui/app/constants/constants.js +++ b/gui/app/constants/constants.js @@ -23,7 +23,8 @@ let constants = EmberObject.extend({ AuthProvider: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects Documize: 'documize', - Keycloak: 'keycloak' + Keycloak: 'keycloak', + LDAP: 'ldap' }, DocumentActionType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects diff --git a/gui/app/mixins/auth.js b/gui/app/mixins/auth.js index 4cc64c81..13c757d8 100644 --- a/gui/app/mixins/auth.js +++ b/gui/app/mixins/auth.js @@ -16,6 +16,7 @@ export default Mixin.create({ appMeta: service(), isAuthProviderDocumize: true, IsAuthProviderKeycloak: false, + IsAuthProviderLDAP: false, init() { this._super(...arguments); @@ -23,5 +24,6 @@ export default Mixin.create({ this.set('isAuthProviderDocumize', this.get('appMeta.authProvider') === constants.AuthProvider.Documize); this.set('isAuthProviderKeycloak', this.get('appMeta.authProvider') === constants.AuthProvider.Keycloak); + this.set('isAuthProviderLDAP', this.get('appMeta.authProvider') === constants.AuthProvider.LDAP); } }); diff --git a/gui/app/pods/customize/auth/controller.js b/gui/app/pods/customize/auth/controller.js index b68019b8..45a37d03 100644 --- a/gui/app/pods/customize/auth/controller.js +++ b/gui/app/pods/customize/auth/controller.js @@ -11,8 +11,8 @@ import { Promise as EmberPromise } from 'rsvp'; import { inject as service } from '@ember/service'; -import Controller from '@ember/controller'; import NotifierMixin from "../../../mixins/notifier"; +import Controller from '@ember/controller'; export default Controller.extend(NotifierMixin, { global: service(), @@ -34,7 +34,7 @@ export default Controller.extend(NotifierMixin, { onSync() { return new EmberPromise((resolve) => { - this.get('global').syncExternalUsers().then((response) => { + this.get('global').syncKeycloak().then((response) => { resolve(response); }); }); diff --git a/gui/app/pods/customize/auth/route.js b/gui/app/pods/customize/auth/route.js index f36b6350..b1c38e5e 100644 --- a/gui/app/pods/customize/auth/route.js +++ b/gui/app/pods/customize/auth/route.js @@ -11,8 +11,8 @@ import { Promise as EmberPromise } from 'rsvp'; import { inject as service } from '@ember/service'; -import Route from '@ember/routing/route'; import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; +import Route from '@ember/routing/route'; export default Route.extend(AuthenticatedRouteMixin, { appMeta: service(), @@ -31,6 +31,18 @@ export default Route.extend(AuthenticatedRouteMixin, { authConfig: null, }; + // TEST + // TEST + // TEST + // TEST + // TEST + this.get('global').syncLDAP().then(() => {}); + // TEST + // TEST + // TEST + // TEST + // TEST + return new EmberPromise((resolve) => { let constants = this.get('constants'); @@ -39,6 +51,9 @@ export default Route.extend(AuthenticatedRouteMixin, { case constants.AuthProvider.Keycloak: data.authConfig = config; break; + case constants.AuthProvider.LDAP: + data.authConfig = config; + break; case constants.AuthProvider.Documize: data.authConfig = ''; break; diff --git a/gui/app/pods/customize/users/controller.js b/gui/app/pods/customize/users/controller.js index 0da631af..14291e51 100644 --- a/gui/app/pods/customize/users/controller.js +++ b/gui/app/pods/customize/users/controller.js @@ -59,7 +59,7 @@ export default Controller.extend({ onSync() { this.set('syncInProgress', true); - this.get('globalSvc').syncExternalUsers().then(() => { + this.get('globalSvc').syncKeycloak().then(() => { this.set('syncInProgress', false); this.loadUsers(''); }); diff --git a/gui/app/services/global.js b/gui/app/services/global.js index 53adc037..a02a8c31 100644 --- a/gui/app/services/global.js +++ b/gui/app/services/global.js @@ -82,9 +82,21 @@ export default Service.extend({ } }, - syncExternalUsers() { + syncKeycloak() { if(this.get('sessionService.isAdmin')) { - return this.get('ajax').request(`users/sync`, { + return this.get('ajax').request(`global/sync/keycloak`, { + method: 'GET' + }).then((response) => { + return response; + }).catch((error) => { + return error; + }); + } + }, + + syncLDAP() { + if(this.get('sessionService.isAdmin')) { + return this.get('ajax').request(`global/sync/ldap`, { method: 'GET' }).then((response) => { return response; diff --git a/model/auth/auth.go b/model/auth/auth.go index 018b9c8f..91ae8e51 100644 --- a/model/auth/auth.go +++ b/model/auth/auth.go @@ -18,3 +18,14 @@ type AuthenticationModel struct { Token string `json:"token"` User user.User `json:"user"` } + +const ( + // AuthProviderDocumize is email/password based + AuthProviderDocumize = "documize" + + // AuthProviderKeycloak performs login and user sync with external provider + AuthProviderKeycloak = "keycloak" + + // AuthProviderLDAP performs login and user sync with external provider + AuthProviderLDAP = "ldap" +) diff --git a/model/auth/ldap.go b/model/auth/ldap.go new file mode 100644 index 00000000..939430a3 --- /dev/null +++ b/model/auth/ldap.go @@ -0,0 +1,47 @@ +// Copyright 2016 Documize Inc. . All rights reserved. +// +// This software (Documize Community Edition) is licensed under +// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html +// +// You can operate outside the AGPL restrictions by purchasing +// Documize Enterprise Edition and obtaining a commercial license +// by contacting . +// +// https://documize.com + +package auth + +// LDAPConfig connection information +type LDAPConfig struct { + ServerHost string `json:"serverHost"` + ServerPort int `json:"serverPort"` + EncryptionType string `json:"encryptionType"` + BaseDN string `json:"baseDN"` + BindDN string `json:"bindDN"` + BindPassword string `json:"bindPassword"` + GroupFilter string `json:"groupFilter"` + DisableLogout bool `json:"disableLogout"` + DefaultPermissionAddSpace bool `json:"defaultPermissionAddSpace"` +} + +// LDAPUser details user record returned by LDAP +// type LDAPUser struct { +// ID string `json:"id"` +// Username string `json:"username"` +// Email string `json:"email"` +// Firstname string `json:"firstName"` +// Lastname string `json:"lastName"` +// Enabled bool `json:"enabled"` +// } + +// LDAPAuthRequest data received via LDAP client library +// type LDAPAuthRequest struct { +// Domain string `json:"domain"` +// Token string `json:"token"` +// RemoteID string `json:"remoteId"` +// Email string `json:"email"` +// Username string `json:"username"` +// Firstname string `json:"firstname"` +// Lastname string `json:"lastname"` +// Enabled bool `json:"enabled"` +// } diff --git a/server/routing/routes.go b/server/routing/routes.go index 3135bb5e..e78aff79 100644 --- a/server/routing/routes.go +++ b/server/routing/routes.go @@ -19,6 +19,7 @@ import ( "github.com/documize/community/domain/attachment" "github.com/documize/community/domain/auth" "github.com/documize/community/domain/auth/keycloak" + "github.com/documize/community/domain/auth/ldap" "github.com/documize/community/domain/block" "github.com/documize/community/domain/category" "github.com/documize/community/domain/conversion" @@ -52,6 +53,7 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) { user := user.Handler{Runtime: rt, Store: s} link := link.Handler{Runtime: rt, Store: s} page := page.Handler{Runtime: rt, Store: s, Indexer: indexer} + ldap := ldap.Handler{Runtime: rt, Store: s} space := space.Handler{Runtime: rt, Store: s} block := block.Handler{Runtime: rt, Store: s} group := group.Handler{Runtime: rt, Store: s} @@ -150,7 +152,6 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) { AddPrivate(rt, "users/{userID}", []string{"GET", "OPTIONS"}, nil, user.Get) AddPrivate(rt, "users/{userID}", []string{"PUT", "OPTIONS"}, nil, user.Update) AddPrivate(rt, "users/{userID}", []string{"DELETE", "OPTIONS"}, nil, user.Delete) - AddPrivate(rt, "users/sync", []string{"GET", "OPTIONS"}, nil, keycloak.Sync) AddPrivate(rt, "users/match", []string{"POST", "OPTIONS"}, nil, user.MatchUsers) AddPrivate(rt, "users/import", []string{"POST", "OPTIONS"}, nil, user.BulkImport) @@ -212,6 +213,8 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) { AddPrivate(rt, "global/auth", []string{"PUT", "OPTIONS"}, nil, setting.SetAuthConfig) AddPrivate(rt, "global/search/status", []string{"GET", "OPTIONS"}, nil, meta.SearchStatus) AddPrivate(rt, "global/search/reindex", []string{"POST", "OPTIONS"}, nil, meta.Reindex) + AddPrivate(rt, "global/sync/keycloak", []string{"GET", "OPTIONS"}, nil, keycloak.Sync) + AddPrivate(rt, "global/sync/ldap", []string{"GET", "OPTIONS"}, nil, ldap.Sync) Add(rt, RoutePrefixRoot, "robots.txt", []string{"GET", "OPTIONS"}, nil, meta.RobotsTxt) Add(rt, RoutePrefixRoot, "sitemap.xml", []string{"GET", "OPTIONS"}, nil, meta.Sitemap) diff --git a/vendor/github.com/andygrunwald/go-jira/.gitignore b/vendor/github.com/andygrunwald/go-jira/.gitignore deleted file mode 100644 index 1e57f8a7..00000000 --- a/vendor/github.com/andygrunwald/go-jira/.gitignore +++ /dev/null @@ -1,29 +0,0 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Don't check in vendor -vendor/ - -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test -*.prof -*.iml -.idea diff --git a/vendor/github.com/andygrunwald/go-jira/.travis.yml b/vendor/github.com/andygrunwald/go-jira/.travis.yml deleted file mode 100644 index 5b477ff8..00000000 --- a/vendor/github.com/andygrunwald/go-jira/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: go - -sudo: false - -go: - - 1.4 - - 1.5 - - 1.6 - - 1.7 - - 1.8 - - 1.9 - -before_install: - - go get -t ./... - -script: - - GOMAXPROCS=4 GORACE="halt_on_error=1" go test -race -v ./... diff --git a/vendor/github.com/andygrunwald/go-jira/Gopkg.lock b/vendor/github.com/andygrunwald/go-jira/Gopkg.lock deleted file mode 100644 index 00b3051b..00000000 --- a/vendor/github.com/andygrunwald/go-jira/Gopkg.lock +++ /dev/null @@ -1,36 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - name = "github.com/fatih/structs" - packages = ["."] - revision = "a720dfa8df582c51dee1b36feabb906bde1588bd" - version = "v1.0" - -[[projects]] - branch = "master" - name = "github.com/google/go-querystring" - packages = ["query"] - revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a" - -[[projects]] - name = "github.com/pkg/errors" - packages = ["."] - revision = "645ef00459ed84a119197bfb8d8205042c6df63d" - version = "v0.8.0" - -[[projects]] - name = "github.com/trivago/tgo" - packages = [ - "tcontainer", - "treflect" - ] - revision = "e4d1ddd28c17dd89ed26327cf69fded22060671b" - version = "v1.0.1" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - inputs-digest = "e84ca9eea6d233e0947b0d760913db2983fd4cbf6fd0d8690c737a71affb635c" - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/vendor/github.com/andygrunwald/go-jira/Gopkg.toml b/vendor/github.com/andygrunwald/go-jira/Gopkg.toml deleted file mode 100644 index 5ebe7d4d..00000000 --- a/vendor/github.com/andygrunwald/go-jira/Gopkg.toml +++ /dev/null @@ -1,46 +0,0 @@ -# Gopkg.toml example -# -# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md -# for detailed Gopkg.toml documentation. -# -# required = ["github.com/user/thing/cmd/thing"] -# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] -# -# [[constraint]] -# name = "github.com/user/project" -# version = "1.0.0" -# -# [[constraint]] -# name = "github.com/user/project2" -# branch = "dev" -# source = "github.com/myfork/project2" -# -# [[override]] -# name = "github.com/x/y" -# version = "2.4.0" -# -# [prune] -# non-go = false -# go-tests = true -# unused-packages = true - - -[[constraint]] - name = "github.com/fatih/structs" - version = "1.0.0" - -[[constraint]] - branch = "master" - name = "github.com/google/go-querystring" - -[[constraint]] - name = "github.com/pkg/errors" - version = "0.8.0" - -[[constraint]] - name = "github.com/trivago/tgo" - version = "1.0.1" - -[prune] - go-tests = true - unused-packages = true diff --git a/vendor/github.com/andygrunwald/go-jira/Makefile b/vendor/github.com/andygrunwald/go-jira/Makefile deleted file mode 100644 index d9ddca95..00000000 --- a/vendor/github.com/andygrunwald/go-jira/Makefile +++ /dev/null @@ -1,2 +0,0 @@ -test: - go test -v ./... \ No newline at end of file diff --git a/vendor/github.com/andygrunwald/go-jira/README.md b/vendor/github.com/andygrunwald/go-jira/README.md deleted file mode 100644 index a8703ac3..00000000 --- a/vendor/github.com/andygrunwald/go-jira/README.md +++ /dev/null @@ -1,271 +0,0 @@ -# go-jira - -[![GoDoc](https://godoc.org/github.com/andygrunwald/go-jira?status.svg)](https://godoc.org/github.com/andygrunwald/go-jira) -[![Build Status](https://travis-ci.org/andygrunwald/go-jira.svg?branch=master)](https://travis-ci.org/andygrunwald/go-jira) -[![Go Report Card](https://goreportcard.com/badge/github.com/andygrunwald/go-jira)](https://goreportcard.com/report/github.com/andygrunwald/go-jira) - -[Go](https://golang.org/) client library for [Atlassian JIRA](https://www.atlassian.com/software/jira). - -![Go client library for Atlassian JIRA](./img/logo_small.png "Go client library for Atlassian JIRA.") - -## Features - -* Authentication (HTTP Basic, OAuth, Session Cookie) -* Create and retrieve issues -* Create and retrieve issue transitions (status updates) -* Call every API endpoint of the JIRA, even if it is not directly implemented in this library - -This package is not JIRA API complete (yet), but you can call every API endpoint you want. See [Call a not implemented API endpoint](#call-a-not-implemented-api-endpoint) how to do this. For all possible API endpoints of JIRA have a look at [latest JIRA REST API documentation](https://docs.atlassian.com/jira/REST/latest/). - -## Compatible JIRA versions - -This package was tested against JIRA v6.3.4 and v7.1.2. - -## Installation - -It is go gettable - - $ go get github.com/andygrunwald/go-jira - -For stable versions you can use one of our tags with [gopkg.in](http://labix.org/gopkg.in). E.g. - -```go -package main - -import ( - jira "gopkg.in/andygrunwald/go-jira.v1" -) -... -``` - -(optional) to run unit / example tests: - - $ cd $GOPATH/src/github.com/andygrunwald/go-jira - $ go test -v ./... - -## API - -Please have a look at the [GoDoc documentation](https://godoc.org/github.com/andygrunwald/go-jira) for a detailed API description. - -The [latest JIRA REST API documentation](https://docs.atlassian.com/jira/REST/latest/) was the base document for this package. - -## Examples - -Further a few examples how the API can be used. -A few more examples are available in the [GoDoc examples section](https://godoc.org/github.com/andygrunwald/go-jira#pkg-examples). - -### Get a single issue - -Lets retrieve [MESOS-3325](https://issues.apache.org/jira/browse/MESOS-3325) from the [Apache Mesos](http://mesos.apache.org/) project. - -```go -package main - -import ( - "fmt" - "github.com/andygrunwald/go-jira" -) - -func main() { - jiraClient, _ := jira.NewClient(nil, "https://issues.apache.org/jira/") - issue, _, _ := jiraClient.Issue.Get("MESOS-3325", nil) - - fmt.Printf("%s: %+v\n", issue.Key, issue.Fields.Summary) - fmt.Printf("Type: %s\n", issue.Fields.Type.Name) - fmt.Printf("Priority: %s\n", issue.Fields.Priority.Name) - - // MESOS-3325: Running mesos-slave@0.23 in a container causes slave to be lost after a restart - // Type: Bug - // Priority: Critical -} -``` - -### Authentication - -The `go-jira` library does not handle most authentication directly. Instead, authentication should be handled within -an `http.Client`. That client can then be passed into the `NewClient` function when creating a jira client. - -For convenience, capability for basic and cookie-based authentication is included in the main library. - -#### Basic auth example - -A more thorough, [runnable example](examples/basicauth/main.go) is provided in the examples directory. - -```go -func main() { - tp := jira.BasicAuthTransport{ - Username: "username", - Password: "password", - } - - client, err := jira.NewClient(tp.Client(), "https://my.jira.com") - - u, _, err := client.User.Get("some_user") - - fmt.Printf("\nEmail: %v\nSuccess!\n", u.EmailAddress) -} -``` - -#### Authenticate with session cookie - -A more thorough, [runnable example](examples/cookieauth/main.go) is provided in the examples directory. - -Note: The `AuthURL` is almost always going to have the path `/rest/auth/1/session` - -```go - tp := jira.CookieAuthTransport{ - Username: "username", - Password: "password", - AuthURL: "https://my.jira.com/rest/auth/1/session", - } - - client, err := jira.NewClient(tp.Client(), "https://my.jira.com") - u, _, err := client.User.Get("admin") - - fmt.Printf("\nEmail: %v\nSuccess!\n", u.EmailAddress) -} -``` - -#### Authenticate with OAuth - -If you want to connect via OAuth to your JIRA Cloud instance checkout the [example of using OAuth authentication with JIRA in Go](https://gist.github.com/Lupus/edafe9a7c5c6b13407293d795442fe67) by [@Lupus](https://github.com/Lupus). - -For more details have a look at the [issue #56](https://github.com/andygrunwald/go-jira/issues/56). - -### Create an issue - -Example how to create an issue. - -```go -package main - -import ( - "fmt" - "github.com/andygrunwald/go-jira" -) - -func main() { - base := "https://my.jira.com" - tp := jira.CookieAuthTransport{ - Username: "username", - Password: "password", - AuthURL: fmt.Sprintf("%s/rest/auth/1/session", base), - } - - jiraClient, err := jira.NewClient(tp.Client(), base) - if err != nil { - panic(err) - } - - i := jira.Issue{ - Fields: &jira.IssueFields{ - Assignee: &jira.User{ - Name: "myuser", - }, - Reporter: &jira.User{ - Name: "youruser", - }, - Description: "Test Issue", - Type: jira.IssueType{ - Name: "Bug", - }, - Project: jira.Project{ - Key: "PROJ1", - }, - Summary: "Just a demo issue", - }, - } - issue, _, err := jiraClient.Issue.Create(&i) - if err != nil { - panic(err) - } - - fmt.Printf("%s: %+v\n", issue.Key, issue.Fields.Summary) -} -``` - -### Call a not implemented API endpoint - -Not all API endpoints of the JIRA API are implemented into *go-jira*. -But you can call them anyway: -Lets get all public projects of [Atlassian`s JIRA instance](https://jira.atlassian.com/). - -```go -package main - -import ( - "fmt" - "github.com/andygrunwald/go-jira" -) - -func main() { - base := "https://my.jira.com" - tp := jira.CookieAuthTransport{ - Username: "username", - Password: "password", - AuthURL: fmt.Sprintf("%s/rest/auth/1/session", base), - } - - jiraClient, err := jira.NewClient(tp.Client(), base) - req, _ := jiraClient.NewRequest("GET", "rest/api/2/project", nil) - - projects := new([]jira.Project) - _, err := jiraClient.Do(req, projects) - if err != nil { - panic(err) - } - - for _, project := range *projects { - fmt.Printf("%s: %s\n", project.Key, project.Name) - } - - // ... - // BAM: Bamboo - // BAMJ: Bamboo JIRA Plugin - // CLOV: Clover - // CONF: Confluence - // ... -} -``` - -## Implementations - -* [andygrunwald/jitic](https://github.com/andygrunwald/jitic) - The JIRA Ticket Checker - -## Code structure - -The code structure of this package was inspired by [google/go-github](https://github.com/google/go-github). - -There is one main part (the client). -Based on this main client the other endpoints, like Issues or Authentication are extracted in services. E.g. `IssueService` or `AuthenticationService`. -These services own a responsibility of the single endpoints / usecases of JIRA. - -## Contribution - -Contribution, in any kind of way, is highly welcome! -It doesn't matter if you are not able to write code. -Creating issues or holding talks and help other people to use [go-jira](https://github.com/andygrunwald/go-jira) is contribution, too! -A few examples: - -* Correct typos in the README / documentation -* Reporting bugs -* Implement a new feature or endpoint -* Sharing the love if [go-jira](https://github.com/andygrunwald/go-jira) and help people to get use to it - -If you are new to pull requests, checkout [Collaborating on projects using issues and pull requests / Creating a pull request](https://help.github.com/articles/creating-a-pull-request/). - -### Dependency management - -`go-jira` uses `dep` for dependency management. After cloning the repo, it's easy to make sure you have the correct dependencies by running `dep ensure`. - -For adding new dependencies, updating dependencies, and other operations, the [Daily Dep](https://golang.github.io/dep/docs/daily-dep.html) is a good place to start. - -### Sandbox environment for testing - -Jira offers sandbox test environments at http://go.atlassian.com/cloud-dev. - -You can read more about them at https://developer.atlassian.com/blog/2016/04/cloud-ecosystem-dev-env/. - -## License - -This project is released under the terms of the [MIT license](http://en.wikipedia.org/wiki/MIT_License). diff --git a/vendor/github.com/andygrunwald/go-jira/authentication.go b/vendor/github.com/andygrunwald/go-jira/authentication.go deleted file mode 100644 index f848a1d5..00000000 --- a/vendor/github.com/andygrunwald/go-jira/authentication.go +++ /dev/null @@ -1,187 +0,0 @@ -package jira - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" -) - -const ( - // HTTP Basic Authentication - authTypeBasic = 1 - // HTTP Session Authentication - authTypeSession = 2 -) - -// AuthenticationService handles authentication for the JIRA instance / API. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#authentication -type AuthenticationService struct { - client *Client - - // Authentication type - authType int - - // Basic auth username - username string - - // Basic auth password - password string -} - -// Session represents a Session JSON response by the JIRA API. -type Session struct { - Self string `json:"self,omitempty"` - Name string `json:"name,omitempty"` - Session struct { - Name string `json:"name"` - Value string `json:"value"` - } `json:"session,omitempty"` - LoginInfo struct { - FailedLoginCount int `json:"failedLoginCount"` - LoginCount int `json:"loginCount"` - LastFailedLoginTime string `json:"lastFailedLoginTime"` - PreviousLoginTime string `json:"previousLoginTime"` - } `json:"loginInfo"` - Cookies []*http.Cookie -} - -// AcquireSessionCookie creates a new session for a user in JIRA. -// Once a session has been successfully created it can be used to access any of JIRA's remote APIs and also the web UI by passing the appropriate HTTP Cookie header. -// The header will by automatically applied to every API request. -// Note that it is generally preferrable to use HTTP BASIC authentication with the REST API. -// However, this resource may be used to mimic the behaviour of JIRA's log-in page (e.g. to display log-in errors to a user). -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session -// -// Deprecated: Use CookieAuthTransport instead -func (s *AuthenticationService) AcquireSessionCookie(username, password string) (bool, error) { - apiEndpoint := "rest/auth/1/session" - body := struct { - Username string `json:"username"` - Password string `json:"password"` - }{ - username, - password, - } - - req, err := s.client.NewRequest("POST", apiEndpoint, body) - if err != nil { - return false, err - } - - session := new(Session) - resp, err := s.client.Do(req, session) - - if resp != nil { - session.Cookies = resp.Cookies() - } - - if err != nil { - return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err) - } - if resp != nil && resp.StatusCode != 200 { - return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). Status code: %d", resp.StatusCode) - } - - s.client.session = session - s.authType = authTypeSession - - return true, nil -} - -// SetBasicAuth sets username and password for the basic auth against the JIRA instance. -// -// Deprecated: Use BasicAuthTransport instead -func (s *AuthenticationService) SetBasicAuth(username, password string) { - s.username = username - s.password = password - s.authType = authTypeBasic -} - -// Authenticated reports if the current Client has authentication details for JIRA -func (s *AuthenticationService) Authenticated() bool { - if s != nil { - if s.authType == authTypeSession { - return s.client.session != nil - } else if s.authType == authTypeBasic { - return s.username != "" - } - - } - return false -} - -// Logout logs out the current user that has been authenticated and the session in the client is destroyed. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session -// -// Deprecated: Use CookieAuthTransport to create base client. Logging out is as simple as not using the -// client anymore -func (s *AuthenticationService) Logout() error { - if s.authType != authTypeSession || s.client.session == nil { - return fmt.Errorf("no user is authenticated") - } - - apiEndpoint := "rest/auth/1/session" - req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) - if err != nil { - return fmt.Errorf("Creating the request to log the user out failed : %s", err) - } - - resp, err := s.client.Do(req, nil) - if err != nil { - return fmt.Errorf("Error sending the logout request: %s", err) - } - if resp.StatusCode != 204 { - return fmt.Errorf("The logout was unsuccessful with status %d", resp.StatusCode) - } - - // If logout successful, delete session - s.client.session = nil - - return nil - -} - -// GetCurrentUser gets the details of the current user. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session -func (s *AuthenticationService) GetCurrentUser() (*Session, error) { - if s == nil { - return nil, fmt.Errorf("AUthenticaiton Service is not instantiated") - } - if s.authType != authTypeSession || s.client.session == nil { - return nil, fmt.Errorf("No user is authenticated yet") - } - - apiEndpoint := "rest/auth/1/session" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, fmt.Errorf("Could not create request for getting user info : %s", err) - } - - resp, err := s.client.Do(req, nil) - if err != nil { - return nil, fmt.Errorf("Error sending request to get user info : %s", err) - } - if resp.StatusCode != 200 { - return nil, fmt.Errorf("Getting user info failed with status : %d", resp.StatusCode) - } - - defer resp.Body.Close() - ret := new(Session) - data, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("Couldn't read body from the response : %s", err) - } - - err = json.Unmarshal(data, &ret) - - if err != nil { - return nil, fmt.Errorf("Could not unmarshall received user info : %s", err) - } - - return ret, nil -} diff --git a/vendor/github.com/andygrunwald/go-jira/board.go b/vendor/github.com/andygrunwald/go-jira/board.go deleted file mode 100644 index 091797d4..00000000 --- a/vendor/github.com/andygrunwald/go-jira/board.go +++ /dev/null @@ -1,204 +0,0 @@ -package jira - -import ( - "fmt" - "strconv" - "time" -) - -// BoardService handles Agile Boards for the JIRA instance / API. -// -// JIRA API docs: https://docs.atlassian.com/jira-software/REST/server/ -type BoardService struct { - client *Client -} - -// BoardsList reflects a list of agile boards -type BoardsList struct { - MaxResults int `json:"maxResults" structs:"maxResults"` - StartAt int `json:"startAt" structs:"startAt"` - Total int `json:"total" structs:"total"` - IsLast bool `json:"isLast" structs:"isLast"` - Values []Board `json:"values" structs:"values"` -} - -// Board represents a JIRA agile board -type Board struct { - ID int `json:"id,omitempty" structs:"id,omitempty"` - Self string `json:"self,omitempty" structs:"self,omitempty"` - Name string `json:"name,omitempty" structs:"name,omitemtpy"` - Type string `json:"type,omitempty" structs:"type,omitempty"` - FilterID int `json:"filterId,omitempty" structs:"filterId,omitempty"` -} - -// BoardListOptions specifies the optional parameters to the BoardService.GetList -type BoardListOptions struct { - // BoardType filters results to boards of the specified type. - // Valid values: scrum, kanban. - BoardType string `url:"boardType,omitempty"` - // Name filters results to boards that match or partially match the specified name. - Name string `url:"name,omitempty"` - // ProjectKeyOrID filters results to boards that are relevant to a project. - // Relevance meaning that the JQL filter defined in board contains a reference to a project. - ProjectKeyOrID string `url:"projectKeyOrId,omitempty"` - - SearchOptions -} - -// GetAllSprintsOptions specifies the optional parameters to the BoardService.GetList -type GetAllSprintsOptions struct { - // State filters results to sprints in the specified states, comma-separate list - State string `url:"state,omitempty"` - - SearchOptions -} - -// SprintsList reflects a list of agile sprints -type SprintsList struct { - MaxResults int `json:"maxResults" structs:"maxResults"` - StartAt int `json:"startAt" structs:"startAt"` - Total int `json:"total" structs:"total"` - IsLast bool `json:"isLast" structs:"isLast"` - Values []Sprint `json:"values" structs:"values"` -} - -// Sprint represents a sprint on JIRA agile board -type Sprint struct { - ID int `json:"id" structs:"id"` - Name string `json:"name" structs:"name"` - CompleteDate *time.Time `json:"completeDate" structs:"completeDate"` - EndDate *time.Time `json:"endDate" structs:"endDate"` - StartDate *time.Time `json:"startDate" structs:"startDate"` - OriginBoardID int `json:"originBoardId" structs:"originBoardId"` - Self string `json:"self" structs:"self"` - State string `json:"state" structs:"state"` -} - -// GetAllBoards will returns all boards. This only includes boards that the user has permission to view. -// -// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getAllBoards -func (s *BoardService) GetAllBoards(opt *BoardListOptions) (*BoardsList, *Response, error) { - apiEndpoint := "rest/agile/1.0/board" - url, err := addOptions(apiEndpoint, opt) - if err != nil { - return nil, nil, err - } - req, err := s.client.NewRequest("GET", url, nil) - if err != nil { - return nil, nil, err - } - - boards := new(BoardsList) - resp, err := s.client.Do(req, boards) - if err != nil { - jerr := NewJiraError(resp, err) - return nil, resp, jerr - } - - return boards, resp, err -} - -// GetBoard will returns the board for the given boardID. -// This board will only be returned if the user has permission to view it. -// -// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getBoard -func (s *BoardService) GetBoard(boardID int) (*Board, *Response, error) { - apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - - board := new(Board) - resp, err := s.client.Do(req, board) - if err != nil { - jerr := NewJiraError(resp, err) - return nil, resp, jerr - } - - return board, resp, nil -} - -// CreateBoard creates a new board. Board name, type and filter Id is required. -// name - Must be less than 255 characters. -// type - Valid values: scrum, kanban -// filterId - Id of a filter that the user has permissions to view. -// Note, if the user does not have the 'Create shared objects' permission and tries to create a shared board, a private -// board will be created instead (remember that board sharing depends on the filter sharing). -// -// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-createBoard -func (s *BoardService) CreateBoard(board *Board) (*Board, *Response, error) { - apiEndpoint := "rest/agile/1.0/board" - req, err := s.client.NewRequest("POST", apiEndpoint, board) - if err != nil { - return nil, nil, err - } - - responseBoard := new(Board) - resp, err := s.client.Do(req, responseBoard) - if err != nil { - jerr := NewJiraError(resp, err) - return nil, resp, jerr - } - - return responseBoard, resp, nil -} - -// DeleteBoard will delete an agile board. -// -// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-deleteBoard -func (s *BoardService) DeleteBoard(boardID int) (*Board, *Response, error) { - apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID) - req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - - resp, err := s.client.Do(req, nil) - if err != nil { - err = NewJiraError(resp, err) - } - return nil, resp, err -} - -// GetAllSprints will return all sprints from a board, for a given board Id. -// This only includes sprints that the user has permission to view. -// -// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint -func (s *BoardService) GetAllSprints(boardID string) ([]Sprint, *Response, error) { - id, err := strconv.Atoi(boardID) - if err != nil { - return nil, nil, err - } - - result, response, err := s.GetAllSprintsWithOptions(id, &GetAllSprintsOptions{}) - if err != nil { - return nil, nil, err - } - - return result.Values, response, nil -} - -// GetAllSprintsWithOptions will return sprints from a board, for a given board Id and filtering options -// This only includes sprints that the user has permission to view. -// -// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint -func (s *BoardService) GetAllSprintsWithOptions(boardID int, options *GetAllSprintsOptions) (*SprintsList, *Response, error) { - apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%d/sprint", boardID) - url, err := addOptions(apiEndpoint, options) - if err != nil { - return nil, nil, err - } - req, err := s.client.NewRequest("GET", url, nil) - if err != nil { - return nil, nil, err - } - - result := new(SprintsList) - resp, err := s.client.Do(req, result) - if err != nil { - err = NewJiraError(resp, err) - } - - return result, resp, err -} diff --git a/vendor/github.com/andygrunwald/go-jira/component.go b/vendor/github.com/andygrunwald/go-jira/component.go deleted file mode 100644 index 407ad36a..00000000 --- a/vendor/github.com/andygrunwald/go-jira/component.go +++ /dev/null @@ -1,38 +0,0 @@ -package jira - -// ComponentService handles components for the JIRA instance / API. -// -// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/7.10.1/#api/2/component -type ComponentService struct { - client *Client -} - -// CreateComponentOptions are passed to the ComponentService.Create function to create a new JIRA component -type CreateComponentOptions struct { - Name string `json:"name,omitempty" structs:"name,omitempty"` - Description string `json:"description,omitempty" structs:"description,omitempty"` - Lead *User `json:"lead,omitempty" structs:"lead,omitempty"` - LeadUserName string `json:"leadUserName,omitempty" structs:"leadUserName,omitempty"` - AssigneeType string `json:"assigneeType,omitempty" structs:"assigneeType,omitempty"` - Assignee *User `json:"assignee,omitempty" structs:"assignee,omitempty"` - Project string `json:"project,omitempty" structs:"project,omitempty"` - ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"` -} - -// Create creates a new JIRA component based on the given options. -func (s *ComponentService) Create(options *CreateComponentOptions) (*ProjectComponent, *Response, error) { - apiEndpoint := "rest/api/2/component" - req, err := s.client.NewRequest("POST", apiEndpoint, options) - if err != nil { - return nil, nil, err - } - - component := new(ProjectComponent) - resp, err := s.client.Do(req, component) - - if err != nil { - return nil, resp, NewJiraError(resp, err) - } - - return component, resp, nil -} diff --git a/vendor/github.com/andygrunwald/go-jira/error.go b/vendor/github.com/andygrunwald/go-jira/error.go deleted file mode 100644 index 0cb8c4b8..00000000 --- a/vendor/github.com/andygrunwald/go-jira/error.go +++ /dev/null @@ -1,82 +0,0 @@ -package jira - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - - "github.com/pkg/errors" -) - -// Error message from JIRA -// See https://docs.atlassian.com/jira/REST/cloud/#error-responses -type Error struct { - HTTPError error - ErrorMessages []string `json:"errorMessages"` - Errors map[string]string `json:"errors"` -} - -// NewJiraError creates a new jira Error -func NewJiraError(resp *Response, httpError error) error { - if resp == nil { - return errors.Wrap(httpError, "No response returned") - } - - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return errors.Wrap(err, httpError.Error()) - } - - jerr := Error{HTTPError: httpError} - err = json.Unmarshal(body, &jerr) - if err != nil { - httpError = errors.Wrap(errors.New("Could not parse JSON"), httpError.Error()) - return errors.Wrap(err, httpError.Error()) - } - - return &jerr -} - -// Error is a short string representing the error -func (e *Error) Error() string { - if len(e.ErrorMessages) > 0 { - // return fmt.Sprintf("%v", e.HTTPError) - return fmt.Sprintf("%s: %v", e.ErrorMessages[0], e.HTTPError) - } - if len(e.Errors) > 0 { - for key, value := range e.Errors { - return fmt.Sprintf("%s - %s: %v", key, value, e.HTTPError) - } - } - return e.HTTPError.Error() -} - -// LongError is a full representation of the error as a string -func (e *Error) LongError() string { - var msg bytes.Buffer - if e.HTTPError != nil { - msg.WriteString("Original:\n") - msg.WriteString(e.HTTPError.Error()) - msg.WriteString("\n") - } - if len(e.ErrorMessages) > 0 { - msg.WriteString("Messages:\n") - for _, v := range e.ErrorMessages { - msg.WriteString(" - ") - msg.WriteString(v) - msg.WriteString("\n") - } - } - if len(e.Errors) > 0 { - for key, value := range e.Errors { - msg.WriteString(" - ") - msg.WriteString(key) - msg.WriteString(" - ") - msg.WriteString(value) - msg.WriteString("\n") - } - } - return msg.String() -} diff --git a/vendor/github.com/andygrunwald/go-jira/field.go b/vendor/github.com/andygrunwald/go-jira/field.go deleted file mode 100644 index 257d4f99..00000000 --- a/vendor/github.com/andygrunwald/go-jira/field.go +++ /dev/null @@ -1,43 +0,0 @@ -package jira - -// FieldService handles fields for the JIRA instance / API. -// -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Field -type FieldService struct { - client *Client -} - -// Field represents a field of a JIRA issue. -type Field struct { - ID string `json:"id,omitempty" structs:"id,omitempty"` - Key string `json:"key,omitempty" structs:"key,omitempty"` - Name string `json:"name,omitempty" structs:"name,omitempty"` - Custom bool `json:"custom,omitempty" structs:"custom,omitempty"` - Navigable bool `json:"navigable,omitempty" structs:"navigable,omitempty"` - Searchable bool `json:"searchable,omitempty" structs:"searchable,omitempty"` - ClauseNames []string `json:"clauseNames,omitempty" structs:"clauseNames,omitempty"` - Schema FieldSchema `json:"schema,omitempty" structs:"schema,omitempty"` -} - -type FieldSchema struct { - Type string `json:"type,omitempty" structs:"type,omitempty"` - System string `json:"system,omitempty" structs:"system,omitempty"` -} - -// GetList gets all fields from JIRA -// -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-field-get -func (s *FieldService) GetList() ([]Field, *Response, error) { - apiEndpoint := "rest/api/2/field" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - - fieldList := []Field{} - resp, err := s.client.Do(req, &fieldList) - if err != nil { - return nil, resp, NewJiraError(resp, err) - } - return fieldList, resp, nil -} diff --git a/vendor/github.com/andygrunwald/go-jira/group.go b/vendor/github.com/andygrunwald/go-jira/group.go deleted file mode 100644 index 8ceadc9b..00000000 --- a/vendor/github.com/andygrunwald/go-jira/group.go +++ /dev/null @@ -1,154 +0,0 @@ -package jira - -import ( - "fmt" - "net/url" -) - -// GroupService handles Groups for the JIRA instance / API. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group -type GroupService struct { - client *Client -} - -// groupMembersResult is only a small wrapper around the Group* methods -// to be able to parse the results -type groupMembersResult struct { - StartAt int `json:"startAt"` - MaxResults int `json:"maxResults"` - Total int `json:"total"` - Members []GroupMember `json:"values"` -} - -// Group represents a JIRA group -type Group struct { - ID string `json:"id"` - Title string `json:"title"` - Type string `json:"type"` - Properties groupProperties `json:"properties"` - AdditionalProperties bool `json:"additionalProperties"` -} - -type groupProperties struct { - Name groupPropertiesName `json:"name"` -} - -type groupPropertiesName struct { - Type string `json:"type"` -} - -// GroupMember reflects a single member of a group -type GroupMember struct { - Self string `json:"self,omitempty"` - Name string `json:"name,omitempty"` - Key string `json:"key,omitempty"` - EmailAddress string `json:"emailAddress,omitempty"` - DisplayName string `json:"displayName,omitempty"` - Active bool `json:"active,omitempty"` - TimeZone string `json:"timeZone,omitempty"` -} - -// GroupSearchOptions specifies the optional parameters for the Get Group methods -type GroupSearchOptions struct { - StartAt int - MaxResults int - IncludeInactiveUsers bool -} - -// Get returns a paginated list of users who are members of the specified group and its subgroups. -// Users in the page are ordered by user names. -// User of this resource is required to have sysadmin or admin permissions. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup -// -// WARNING: This API only returns the first page of group members -func (s *GroupService) Get(name string) ([]GroupMember, *Response, error) { - apiEndpoint := fmt.Sprintf("/rest/api/2/group/member?groupname=%s", url.QueryEscape(name)) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - - group := new(groupMembersResult) - resp, err := s.client.Do(req, group) - if err != nil { - return nil, resp, err - } - - return group.Members, resp, nil -} - -// GetWithOptions returns a paginated list of members of the specified group and its subgroups. -// Users in the page are ordered by user names. -// User of this resource is required to have sysadmin or admin permissions. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup -func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions) ([]GroupMember, *Response, error) { - var apiEndpoint string - if options == nil { - apiEndpoint = fmt.Sprintf("/rest/api/2/group/member?groupname=%s", url.QueryEscape(name)) - } else { - apiEndpoint = fmt.Sprintf( - "/rest/api/2/group/member?groupname=%s&startAt=%d&maxResults=%d&includeInactiveUsers=%t", - url.QueryEscape(name), - options.StartAt, - options.MaxResults, - options.IncludeInactiveUsers, - ) - } - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - - group := new(groupMembersResult) - resp, err := s.client.Do(req, group) - if err != nil { - return nil, resp, err - } - return group.Members, resp, nil -} - -// Add adds user to group -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-addUserToGroup -func (s *GroupService) Add(groupname string, username string) (*Group, *Response, error) { - apiEndpoint := fmt.Sprintf("/rest/api/2/group/user?groupname=%s", groupname) - var user struct { - Name string `json:"name"` - } - user.Name = username - req, err := s.client.NewRequest("POST", apiEndpoint, &user) - if err != nil { - return nil, nil, err - } - - responseGroup := new(Group) - resp, err := s.client.Do(req, responseGroup) - if err != nil { - jerr := NewJiraError(resp, err) - return nil, resp, jerr - } - - return responseGroup, resp, nil -} - -// Remove removes user from group -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-removeUserFromGroup -func (s *GroupService) Remove(groupname string, username string) (*Response, error) { - apiEndpoint := fmt.Sprintf("/rest/api/2/group/user?groupname=%s&username=%s", groupname, username) - req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) - if err != nil { - return nil, err - } - - resp, err := s.client.Do(req, nil) - if err != nil { - jerr := NewJiraError(resp, err) - return resp, jerr - } - - return resp, nil -} diff --git a/vendor/github.com/andygrunwald/go-jira/issue.go b/vendor/github.com/andygrunwald/go-jira/issue.go deleted file mode 100644 index 0949ea12..00000000 --- a/vendor/github.com/andygrunwald/go-jira/issue.go +++ /dev/null @@ -1,1090 +0,0 @@ -package jira - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "mime/multipart" - "net/url" - "reflect" - "strings" - "time" - - "github.com/fatih/structs" - "github.com/google/go-querystring/query" - "github.com/trivago/tgo/tcontainer" -) - -const ( - // AssigneeAutomatic represents the value of the "Assignee: Automatic" of JIRA - AssigneeAutomatic = "-1" -) - -// IssueService handles Issues for the JIRA instance / API. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue -type IssueService struct { - client *Client -} - -// Issue represents a JIRA issue. -type Issue struct { - Expand string `json:"expand,omitempty" structs:"expand,omitempty"` - ID string `json:"id,omitempty" structs:"id,omitempty"` - Self string `json:"self,omitempty" structs:"self,omitempty"` - Key string `json:"key,omitempty" structs:"key,omitempty"` - Fields *IssueFields `json:"fields,omitempty" structs:"fields,omitempty"` - RenderedFields *IssueRenderedFields `json:"renderedFields,omitempty" structs:"renderedFields,omitempty"` - Changelog *Changelog `json:"changelog,omitempty" structs:"changelog,omitempty"` -} - -// ChangelogItems reflects one single changelog item of a history item -type ChangelogItems struct { - Field string `json:"field" structs:"field"` - FieldType string `json:"fieldtype" structs:"fieldtype"` - From interface{} `json:"from" structs:"from"` - FromString string `json:"fromString" structs:"fromString"` - To interface{} `json:"to" structs:"to"` - ToString string `json:"toString" structs:"toString"` -} - -// ChangelogHistory reflects one single changelog history entry -type ChangelogHistory struct { - Id string `json:"id" structs:"id"` - Author User `json:"author" structs:"author"` - Created string `json:"created" structs:"created"` - Items []ChangelogItems `json:"items" structs:"items"` -} - -// Changelog reflects the change log of an issue -type Changelog struct { - Histories []ChangelogHistory `json:"histories,omitempty"` -} - -// Attachment represents a JIRA attachment -type Attachment struct { - Self string `json:"self,omitempty" structs:"self,omitempty"` - ID string `json:"id,omitempty" structs:"id,omitempty"` - Filename string `json:"filename,omitempty" structs:"filename,omitempty"` - Author *User `json:"author,omitempty" structs:"author,omitempty"` - Created string `json:"created,omitempty" structs:"created,omitempty"` - Size int `json:"size,omitempty" structs:"size,omitempty"` - MimeType string `json:"mimeType,omitempty" structs:"mimeType,omitempty"` - Content string `json:"content,omitempty" structs:"content,omitempty"` - Thumbnail string `json:"thumbnail,omitempty" structs:"thumbnail,omitempty"` -} - -// Epic represents the epic to which an issue is associated -// Not that this struct does not process the returned "color" value -type Epic struct { - ID int `json:"id" structs:"id"` - Key string `json:"key" structs:"key"` - Self string `json:"self" structs:"self"` - Name string `json:"name" structs:"name"` - Summary string `json:"summary" structs:"summary"` - Done bool `json:"done" structs:"done"` -} - -// IssueFields represents single fields of a JIRA issue. -// Every JIRA issue has several fields attached. -type IssueFields struct { - // TODO Missing fields - // * "aggregatetimespent": null, - // * "workratio": -1, - // * "lastViewed": null, - // * "aggregatetimeoriginalestimate": null, - // * "aggregatetimeestimate": null, - // * "environment": null, - Expand string `json:"expand,omitempty" structs:"expand,omitempty"` - Type IssueType `json:"issuetype,omitempty" structs:"issuetype,omitempty"` - Project Project `json:"project,omitempty" structs:"project,omitempty"` - Resolution *Resolution `json:"resolution,omitempty" structs:"resolution,omitempty"` - Priority *Priority `json:"priority,omitempty" structs:"priority,omitempty"` - Resolutiondate Time `json:"resolutiondate,omitempty" structs:"resolutiondate,omitempty"` - Created Time `json:"created,omitempty" structs:"created,omitempty"` - Duedate Date `json:"duedate,omitempty" structs:"duedate,omitempty"` - Watches *Watches `json:"watches,omitempty" structs:"watches,omitempty"` - Assignee *User `json:"assignee,omitempty" structs:"assignee,omitempty"` - Updated Time `json:"updated,omitempty" structs:"updated,omitempty"` - Description string `json:"description,omitempty" structs:"description,omitempty"` - Summary string `json:"summary,omitempty" structs:"summary,omitempty"` - Creator *User `json:"Creator,omitempty" structs:"Creator,omitempty"` - Reporter *User `json:"reporter,omitempty" structs:"reporter,omitempty"` - Components []*Component `json:"components,omitempty" structs:"components,omitempty"` - Status *Status `json:"status,omitempty" structs:"status,omitempty"` - Progress *Progress `json:"progress,omitempty" structs:"progress,omitempty"` - AggregateProgress *Progress `json:"aggregateprogress,omitempty" structs:"aggregateprogress,omitempty"` - TimeTracking *TimeTracking `json:"timetracking,omitempty" structs:"timetracking,omitempty"` - TimeSpent int `json:"timespent,omitempty" structs:"timespent,omitempty"` - TimeEstimate int `json:"timeestimate,omitempty" structs:"timeestimate,omitempty"` - TimeOriginalEstimate int `json:"timeoriginalestimate,omitempty" structs:"timeoriginalestimate,omitempty"` - Worklog *Worklog `json:"worklog,omitempty" structs:"worklog,omitempty"` - IssueLinks []*IssueLink `json:"issuelinks,omitempty" structs:"issuelinks,omitempty"` - Comments *Comments `json:"comment,omitempty" structs:"comment,omitempty"` - FixVersions []*FixVersion `json:"fixVersions,omitempty" structs:"fixVersions,omitempty"` - Labels []string `json:"labels,omitempty" structs:"labels,omitempty"` - Subtasks []*Subtasks `json:"subtasks,omitempty" structs:"subtasks,omitempty"` - Attachments []*Attachment `json:"attachment,omitempty" structs:"attachment,omitempty"` - Epic *Epic `json:"epic,omitempty" structs:"epic,omitempty"` - Sprint *Sprint `json:"sprint,omitempty" structs:"sprint,omitempty"` - Parent *Parent `json:"parent,omitempty" structs:"parent,omitempty"` - Unknowns tcontainer.MarshalMap -} - -// MarshalJSON is a custom JSON marshal function for the IssueFields structs. -// It handles JIRA custom fields and maps those from / to "Unknowns" key. -func (i *IssueFields) MarshalJSON() ([]byte, error) { - m := structs.Map(i) - unknowns, okay := m["Unknowns"] - if okay { - // if unknowns present, shift all key value from unknown to a level up - for key, value := range unknowns.(tcontainer.MarshalMap) { - m[key] = value - } - delete(m, "Unknowns") - } - return json.Marshal(m) -} - -// UnmarshalJSON is a custom JSON marshal function for the IssueFields structs. -// It handles JIRA custom fields and maps those from / to "Unknowns" key. -func (i *IssueFields) UnmarshalJSON(data []byte) error { - - // Do the normal unmarshalling first - // Details for this way: http://choly.ca/post/go-json-marshalling/ - type Alias IssueFields - aux := &struct { - *Alias - }{ - Alias: (*Alias)(i), - } - if err := json.Unmarshal(data, &aux); err != nil { - return err - } - - totalMap := tcontainer.NewMarshalMap() - err := json.Unmarshal(data, &totalMap) - if err != nil { - return err - } - - t := reflect.TypeOf(*i) - for i := 0; i < t.NumField(); i++ { - field := t.Field(i) - tagDetail := field.Tag.Get("json") - if tagDetail == "" { - // ignore if there are no tags - continue - } - options := strings.Split(tagDetail, ",") - - if len(options) == 0 { - return fmt.Errorf("No tags options found for %s", field.Name) - } - // the first one is the json tag - key := options[0] - if _, okay := totalMap.Value(key); okay { - delete(totalMap, key) - } - - } - i = (*IssueFields)(aux.Alias) - // all the tags found in the struct were removed. Whatever is left are unknowns to struct - i.Unknowns = totalMap - return nil - -} - -// IssueRenderedFields represents rendered fields of a JIRA issue. -// Not all IssueFields are rendered. -type IssueRenderedFields struct { - // TODO Missing fields - // * "aggregatetimespent": null, - // * "workratio": -1, - // * "lastViewed": null, - // * "aggregatetimeoriginalestimate": null, - // * "aggregatetimeestimate": null, - // * "environment": null, - Resolutiondate string `json:"resolutiondate,omitempty" structs:"resolutiondate,omitempty"` - Created string `json:"created,omitempty" structs:"created,omitempty"` - Duedate string `json:"duedate,omitempty" structs:"duedate,omitempty"` - Updated string `json:"updated,omitempty" structs:"updated,omitempty"` - Comments *Comments `json:"comment,omitempty" structs:"comment,omitempty"` -} - -// IssueType represents a type of a JIRA issue. -// Typical types are "Request", "Bug", "Story", ... -type IssueType struct { - Self string `json:"self,omitempty" structs:"self,omitempty"` - ID string `json:"id,omitempty" structs:"id,omitempty"` - Description string `json:"description,omitempty" structs:"description,omitempty"` - IconURL string `json:"iconUrl,omitempty" structs:"iconUrl,omitempty"` - Name string `json:"name,omitempty" structs:"name,omitempty"` - Subtask bool `json:"subtask,omitempty" structs:"subtask,omitempty"` - AvatarID int `json:"avatarId,omitempty" structs:"avatarId,omitempty"` -} - -// Watches represents a type of how many and which user are "observing" a JIRA issue to track the status / updates. -type Watches struct { - Self string `json:"self,omitempty" structs:"self,omitempty"` - WatchCount int `json:"watchCount,omitempty" structs:"watchCount,omitempty"` - IsWatching bool `json:"isWatching,omitempty" structs:"isWatching,omitempty"` - Watchers []*Watcher `json:"watchers,omitempty" structs:"watchers,omitempty"` -} - -// Watcher represents a simplified user that "observes" the issue -type Watcher struct { - Self string `json:"self,omitempty" structs:"self,omitempty"` - Name string `json:"name,omitempty" structs:"name,omitempty"` - DisplayName string `json:"displayName,omitempty" structs:"displayName,omitempty"` - Active bool `json:"active,omitempty" structs:"active,omitempty"` -} - -// AvatarUrls represents different dimensions of avatars / images -type AvatarUrls struct { - Four8X48 string `json:"48x48,omitempty" structs:"48x48,omitempty"` - Two4X24 string `json:"24x24,omitempty" structs:"24x24,omitempty"` - One6X16 string `json:"16x16,omitempty" structs:"16x16,omitempty"` - Three2X32 string `json:"32x32,omitempty" structs:"32x32,omitempty"` -} - -// Component represents a "component" of a JIRA issue. -// Components can be user defined in every JIRA instance. -type Component struct { - Self string `json:"self,omitempty" structs:"self,omitempty"` - ID string `json:"id,omitempty" structs:"id,omitempty"` - Name string `json:"name,omitempty" structs:"name,omitempty"` -} - -// Status represents the current status of a JIRA issue. -// Typical status are "Open", "In Progress", "Closed", ... -// Status can be user defined in every JIRA instance. -type Status struct { - Self string `json:"self" structs:"self"` - Description string `json:"description" structs:"description"` - IconURL string `json:"iconUrl" structs:"iconUrl"` - Name string `json:"name" structs:"name"` - ID string `json:"id" structs:"id"` - StatusCategory StatusCategory `json:"statusCategory" structs:"statusCategory"` -} - -// Progress represents the progress of a JIRA issue. -type Progress struct { - Progress int `json:"progress" structs:"progress"` - Total int `json:"total" structs:"total"` -} - -// Parent represents the parent of a JIRA issue, to be used with subtask issue types. -type Parent struct { - ID string `json:"id,omitempty" structs:"id"` - Key string `json:"key,omitempty" structs:"key"` -} - -// Time represents the Time definition of JIRA as a time.Time of go -type Time time.Time - -// Date represents the Date definition of JIRA as a time.Time of go -type Date time.Time - -// Wrapper struct for search result -type transitionResult struct { - Transitions []Transition `json:"transitions" structs:"transitions"` -} - -// Transition represents an issue transition in JIRA -type Transition struct { - ID string `json:"id" structs:"id"` - Name string `json:"name" structs:"name"` - To Status `json:"to" structs:"status"` - Fields map[string]TransitionField `json:"fields" structs:"fields"` -} - -// TransitionField represents the value of one Transition -type TransitionField struct { - Required bool `json:"required" structs:"required"` -} - -// CreateTransitionPayload is used for creating new issue transitions -type CreateTransitionPayload struct { - Transition TransitionPayload `json:"transition" structs:"transition"` - Fields TransitionPayloadFields `json:"fields" structs:"fields"` -} - -// TransitionPayload represents the request payload of Transition calls like DoTransition -type TransitionPayload struct { - ID string `json:"id" structs:"id"` -} - -// TransitionPayloadFields represents the fields that can be set when executing a transition -type TransitionPayloadFields struct { - Resolution *Resolution `json:"resolution,omitempty" structs:"resolution,omitempty"` -} - -// Option represents an option value in a SelectList or MultiSelect -// custom issue field -type Option struct { - Value string `json:"value" structs:"value"` -} - -// UnmarshalJSON will transform the JIRA time into a time.Time -// during the transformation of the JIRA JSON response -func (t *Time) UnmarshalJSON(b []byte) error { - // Ignore null, like in the main JSON package. - if string(b) == "null" { - return nil - } - ti, err := time.Parse("\"2006-01-02T15:04:05.999-0700\"", string(b)) - if err != nil { - return err - } - *t = Time(ti) - return nil -} - -// UnmarshalJSON will transform the JIRA date into a time.Time -// during the transformation of the JIRA JSON response -func (t *Date) UnmarshalJSON(b []byte) error { - // Ignore null, like in the main JSON package. - if string(b) == "null" { - return nil - } - ti, err := time.Parse("\"2006-01-02\"", string(b)) - if err != nil { - return err - } - *t = Date(ti) - return nil -} - -// MarshalJSON will transform the Date object into a short -// date string as JIRA expects during the creation of a -// JIRA request -func (t Date) MarshalJSON() ([]byte, error) { - time := time.Time(t) - return []byte(time.Format("\"2006-01-02\"")), nil -} - -// Worklog represents the work log of a JIRA issue. -// One Worklog contains zero or n WorklogRecords -// JIRA Wiki: https://confluence.atlassian.com/jira/logging-work-on-an-issue-185729605.html -type Worklog struct { - StartAt int `json:"startAt" structs:"startAt"` - MaxResults int `json:"maxResults" structs:"maxResults"` - Total int `json:"total" structs:"total"` - Worklogs []WorklogRecord `json:"worklogs" structs:"worklogs"` -} - -// WorklogRecord represents one entry of a Worklog -type WorklogRecord struct { - Self string `json:"self,omitempty" structs:"self,omitempty"` - Author *User `json:"author,omitempty" structs:"author,omitempty"` - UpdateAuthor *User `json:"updateAuthor,omitempty" structs:"updateAuthor,omitempty"` - Comment string `json:"comment,omitempty" structs:"comment,omitempty"` - Created *Time `json:"created,omitempty" structs:"created,omitempty"` - Updated *Time `json:"updated,omitempty" structs:"updated,omitempty"` - Started *Time `json:"started,omitempty" structs:"started,omitempty"` - TimeSpent string `json:"timeSpent,omitempty" structs:"timeSpent,omitempty"` - TimeSpentSeconds int `json:"timeSpentSeconds,omitempty" structs:"timeSpentSeconds,omitempty"` - ID string `json:"id,omitempty" structs:"id,omitempty"` - IssueID string `json:"issueId,omitempty" structs:"issueId,omitempty"` -} - -// TimeTracking represents the timetracking fields of a JIRA issue. -type TimeTracking struct { - OriginalEstimate string `json:"originalEstimate,omitempty" structs:"originalEstimate,omitempty"` - RemainingEstimate string `json:"remainingEstimate,omitempty" structs:"remainingEstimate,omitempty"` - TimeSpent string `json:"timeSpent,omitempty" structs:"timeSpent,omitempty"` - OriginalEstimateSeconds int `json:"originalEstimateSeconds,omitempty" structs:"originalEstimateSeconds,omitempty"` - RemainingEstimateSeconds int `json:"remainingEstimateSeconds,omitempty" structs:"remainingEstimateSeconds,omitempty"` - TimeSpentSeconds int `json:"timeSpentSeconds,omitempty" structs:"timeSpentSeconds,omitempty"` -} - -// Subtasks represents all issues of a parent issue. -type Subtasks struct { - ID string `json:"id" structs:"id"` - Key string `json:"key" structs:"key"` - Self string `json:"self" structs:"self"` - Fields IssueFields `json:"fields" structs:"fields"` -} - -// IssueLink represents a link between two issues in JIRA. -type IssueLink struct { - ID string `json:"id,omitempty" structs:"id,omitempty"` - Self string `json:"self,omitempty" structs:"self,omitempty"` - Type IssueLinkType `json:"type" structs:"type"` - OutwardIssue *Issue `json:"outwardIssue" structs:"outwardIssue"` - InwardIssue *Issue `json:"inwardIssue" structs:"inwardIssue"` - Comment *Comment `json:"comment,omitempty" structs:"comment,omitempty"` -} - -// IssueLinkType represents a type of a link between to issues in JIRA. -// Typical issue link types are "Related to", "Duplicate", "Is blocked by", etc. -type IssueLinkType struct { - ID string `json:"id,omitempty" structs:"id,omitempty"` - Self string `json:"self,omitempty" structs:"self,omitempty"` - Name string `json:"name" structs:"name"` - Inward string `json:"inward" structs:"inward"` - Outward string `json:"outward" structs:"outward"` -} - -// Comments represents a list of Comment. -type Comments struct { - Comments []*Comment `json:"comments,omitempty" structs:"comments,omitempty"` -} - -// Comment represents a comment by a person to an issue in JIRA. -type Comment struct { - ID string `json:"id,omitempty" structs:"id,omitempty"` - Self string `json:"self,omitempty" structs:"self,omitempty"` - Name string `json:"name,omitempty" structs:"name,omitempty"` - Author User `json:"author,omitempty" structs:"author,omitempty"` - Body string `json:"body,omitempty" structs:"body,omitempty"` - UpdateAuthor User `json:"updateAuthor,omitempty" structs:"updateAuthor,omitempty"` - Updated string `json:"updated,omitempty" structs:"updated,omitempty"` - Created string `json:"created,omitempty" structs:"created,omitempty"` - Visibility CommentVisibility `json:"visibility,omitempty" structs:"visibility,omitempty"` -} - -// FixVersion represents a software release in which an issue is fixed. -type FixVersion struct { - Archived *bool `json:"archived,omitempty" structs:"archived,omitempty"` - ID string `json:"id,omitempty" structs:"id,omitempty"` - Name string `json:"name,omitempty" structs:"name,omitempty"` - ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"` - ReleaseDate string `json:"releaseDate,omitempty" structs:"releaseDate,omitempty"` - Released *bool `json:"released,omitempty" structs:"released,omitempty"` - Self string `json:"self,omitempty" structs:"self,omitempty"` - UserReleaseDate string `json:"userReleaseDate,omitempty" structs:"userReleaseDate,omitempty"` -} - -// CommentVisibility represents he visibility of a comment. -// E.g. Type could be "role" and Value "Administrators" -type CommentVisibility struct { - Type string `json:"type,omitempty" structs:"type,omitempty"` - Value string `json:"value,omitempty" structs:"value,omitempty"` -} - -// SearchOptions specifies the optional parameters to various List methods that -// support pagination. -// Pagination is used for the JIRA REST APIs to conserve server resources and limit -// response size for resources that return potentially large collection of items. -// A request to a pages API will result in a values array wrapped in a JSON object with some paging metadata -// Default Pagination options -type SearchOptions struct { - // StartAt: The starting index of the returned projects. Base index: 0. - StartAt int `url:"startAt,omitempty"` - // MaxResults: The maximum number of projects to return per page. Default: 50. - MaxResults int `url:"maxResults,omitempty"` - // Expand: Expand specific sections in the returned issues - Expand string `url:"expand,omitempty"` - Fields []string - // ValidateQuery: The validateQuery param offers control over whether to validate and how strictly to treat the validation. Default: strict. - ValidateQuery string `url:"validateQuery,omitempty"` -} - -// searchResult is only a small wrapper around the Search (with JQL) method -// to be able to parse the results -type searchResult struct { - Issues []Issue `json:"issues" structs:"issues"` - StartAt int `json:"startAt" structs:"startAt"` - MaxResults int `json:"maxResults" structs:"maxResults"` - Total int `json:"total" structs:"total"` -} - -// GetQueryOptions specifies the optional parameters for the Get Issue methods -type GetQueryOptions struct { - // Fields is the list of fields to return for the issue. By default, all fields are returned. - Fields string `url:"fields,omitempty"` - Expand string `url:"expand,omitempty"` - // Properties is the list of properties to return for the issue. By default no properties are returned. - Properties string `url:"properties,omitempty"` - // FieldsByKeys if true then fields in issues will be referenced by keys instead of ids - FieldsByKeys bool `url:"fieldsByKeys,omitempty"` - UpdateHistory bool `url:"updateHistory,omitempty"` - ProjectKeys string `url:"projectKeys,omitempty"` -} - -// CustomFields represents custom fields of JIRA -// This can heavily differ between JIRA instances -type CustomFields map[string]string - -// Get returns a full representation of the issue for the given issue key. -// JIRA will attempt to identify the issue by the issueIdOrKey path parameter. -// This can be an issue id, or an issue key. -// If the issue cannot be found via an exact match, JIRA will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved. -// -// The given options will be appended to the query string -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getIssue -func (s *IssueService) Get(issueID string, options *GetQueryOptions) (*Issue, *Response, error) { - apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - - if options != nil { - q, err := query.Values(options) - if err != nil { - return nil, nil, err - } - req.URL.RawQuery = q.Encode() - } - - issue := new(Issue) - resp, err := s.client.Do(req, issue) - if err != nil { - jerr := NewJiraError(resp, err) - return nil, resp, jerr - } - - return issue, resp, nil -} - -// DownloadAttachment returns a Response of an attachment for a given attachmentID. -// The attachment is in the Response.Body of the response. -// This is an io.ReadCloser. -// The caller should close the resp.Body. -func (s *IssueService) DownloadAttachment(attachmentID string) (*Response, error) { - apiEndpoint := fmt.Sprintf("secure/attachment/%s/", attachmentID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, err - } - - resp, err := s.client.Do(req, nil) - if err != nil { - jerr := NewJiraError(resp, err) - return resp, jerr - } - - return resp, nil -} - -// PostAttachment uploads r (io.Reader) as an attachment to a given issueID -func (s *IssueService) PostAttachment(issueID string, r io.Reader, attachmentName string) (*[]Attachment, *Response, error) { - apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/attachments", issueID) - - b := new(bytes.Buffer) - writer := multipart.NewWriter(b) - - fw, err := writer.CreateFormFile("file", attachmentName) - if err != nil { - return nil, nil, err - } - - if r != nil { - // Copy the file - if _, err = io.Copy(fw, r); err != nil { - return nil, nil, err - } - } - writer.Close() - - req, err := s.client.NewMultiPartRequest("POST", apiEndpoint, b) - if err != nil { - return nil, nil, err - } - - req.Header.Set("Content-Type", writer.FormDataContentType()) - - // PostAttachment response returns a JSON array (as multiple attachments can be posted) - attachment := new([]Attachment) - resp, err := s.client.Do(req, attachment) - if err != nil { - jerr := NewJiraError(resp, err) - return nil, resp, jerr - } - - return attachment, resp, nil -} - -// GetWorklogs gets all the worklogs for an issue. -// This method is especially important if you need to read all the worklogs, not just the first page. -// -// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/worklog-getIssueWorklog -func (s *IssueService) GetWorklogs(issueID string) (*Worklog, *Response, error) { - apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/worklog", issueID) - - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - - v := new(Worklog) - resp, err := s.client.Do(req, v) - return v, resp, err -} - -// Create creates an issue or a sub-task from a JSON representation. -// Creating a sub-task is similar to creating a regular issue, with two important differences: -// The issueType field must correspond to a sub-task issue type and you must provide a parent field in the issue create request containing the id or key of the parent issue. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-createIssues -func (s *IssueService) Create(issue *Issue) (*Issue, *Response, error) { - apiEndpoint := "rest/api/2/issue/" - req, err := s.client.NewRequest("POST", apiEndpoint, issue) - if err != nil { - return nil, nil, err - } - resp, err := s.client.Do(req, nil) - if err != nil { - // incase of error return the resp for further inspection - return nil, resp, err - } - - responseIssue := new(Issue) - defer resp.Body.Close() - data, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, resp, fmt.Errorf("Could not read the returned data") - } - err = json.Unmarshal(data, responseIssue) - if err != nil { - return nil, resp, fmt.Errorf("Could not unmarshall the data into struct") - } - return responseIssue, resp, nil -} - -// Update updates an issue from a JSON representation. The issue is found by key. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-editIssue -func (s *IssueService) Update(issue *Issue) (*Issue, *Response, error) { - apiEndpoint := fmt.Sprintf("rest/api/2/issue/%v", issue.Key) - req, err := s.client.NewRequest("PUT", apiEndpoint, issue) - if err != nil { - return nil, nil, err - } - resp, err := s.client.Do(req, nil) - if err != nil { - jerr := NewJiraError(resp, err) - return nil, resp, jerr - } - - // This is just to follow the rest of the API's convention of returning an issue. - // Returning the same pointer here is pointless, so we return a copy instead. - ret := *issue - return &ret, resp, nil -} - -// UpdateIssue updates an issue from a JSON representation. The issue is found by key. -// -// https://docs.atlassian.com/jira/REST/7.4.0/#api/2/issue-editIssue -func (s *IssueService) UpdateIssue(jiraID string, data map[string]interface{}) (*Response, error) { - apiEndpoint := fmt.Sprintf("rest/api/2/issue/%v", jiraID) - req, err := s.client.NewRequest("PUT", apiEndpoint, data) - if err != nil { - return nil, err - } - resp, err := s.client.Do(req, nil) - if err != nil { - return resp, err - } - - // This is just to follow the rest of the API's convention of returning an issue. - // Returning the same pointer here is pointless, so we return a copy instead. - return resp, nil -} - -// AddComment adds a new comment to issueID. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-addComment -func (s *IssueService) AddComment(issueID string, comment *Comment) (*Comment, *Response, error) { - apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/comment", issueID) - req, err := s.client.NewRequest("POST", apiEndpoint, comment) - if err != nil { - return nil, nil, err - } - - responseComment := new(Comment) - resp, err := s.client.Do(req, responseComment) - if err != nil { - jerr := NewJiraError(resp, err) - return nil, resp, jerr - } - - return responseComment, resp, nil -} - -// UpdateComment updates the body of a comment, identified by comment.ID, on the issueID. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/comment-updateComment -func (s *IssueService) UpdateComment(issueID string, comment *Comment) (*Comment, *Response, error) { - reqBody := struct { - Body string `json:"body"` - }{ - Body: comment.Body, - } - apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/comment/%s", issueID, comment.ID) - req, err := s.client.NewRequest("POST", apiEndpoint, reqBody) - if err != nil { - return nil, nil, err - } - - responseComment := new(Comment) - resp, err := s.client.Do(req, responseComment) - if err != nil { - return nil, resp, err - } - - return responseComment, resp, nil -} - -// AddWorklogRecord adds a new worklog record to issueID. -// -// https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-issue-issueIdOrKey-worklog-post -func (s *IssueService) AddWorklogRecord(issueID string, record *WorklogRecord) (*WorklogRecord, *Response, error) { - apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/worklog", issueID) - req, err := s.client.NewRequest("POST", apiEndpoint, record) - if err != nil { - return nil, nil, err - } - - responseRecord := new(WorklogRecord) - resp, err := s.client.Do(req, responseRecord) - if err != nil { - jerr := NewJiraError(resp, err) - return nil, resp, jerr - } - - return responseRecord, resp, nil -} - -// AddLink adds a link between two issues. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issueLink -func (s *IssueService) AddLink(issueLink *IssueLink) (*Response, error) { - apiEndpoint := fmt.Sprintf("rest/api/2/issueLink") - req, err := s.client.NewRequest("POST", apiEndpoint, issueLink) - if err != nil { - return nil, err - } - - resp, err := s.client.Do(req, nil) - if err != nil { - err = NewJiraError(resp, err) - } - - return resp, err -} - -// Search will search for tickets according to the jql -// -// JIRA API docs: https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-query-issues -func (s *IssueService) Search(jql string, options *SearchOptions) ([]Issue, *Response, error) { - var u string - if options == nil { - u = fmt.Sprintf("rest/api/2/search?jql=%s", url.QueryEscape(jql)) - } else { - u = fmt.Sprintf("rest/api/2/search?jql=%s&startAt=%d&maxResults=%d&expand=%s&fields=%s&validateQuery=%s", url.QueryEscape(jql), - options.StartAt, options.MaxResults, options.Expand, strings.Join(options.Fields, ","), options.ValidateQuery) - } - - req, err := s.client.NewRequest("GET", u, nil) - if err != nil { - return []Issue{}, nil, err - } - - v := new(searchResult) - resp, err := s.client.Do(req, v) - if err != nil { - err = NewJiraError(resp, err) - } - return v.Issues, resp, err -} - -// SearchPages will get issues from all pages in a search -// -// JIRA API docs: https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-query-issues -func (s *IssueService) SearchPages(jql string, options *SearchOptions, f func(Issue) error) error { - if options == nil { - options = &SearchOptions{ - StartAt: 0, - MaxResults: 50, - } - } - - if options.MaxResults == 0 { - options.MaxResults = 50 - } - - issues, resp, err := s.Search(jql, options) - if err != nil { - return err - } - - for { - for _, issue := range issues { - err = f(issue) - if err != nil { - return err - } - } - - if resp.StartAt+resp.MaxResults >= resp.Total { - return nil - } - - options.StartAt += resp.MaxResults - issues, resp, err = s.Search(jql, options) - if err != nil { - return err - } - } -} - -// GetCustomFields returns a map of customfield_* keys with string values -func (s *IssueService) GetCustomFields(issueID string) (CustomFields, *Response, error) { - apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - - issue := new(map[string]interface{}) - resp, err := s.client.Do(req, issue) - if err != nil { - jerr := NewJiraError(resp, err) - return nil, resp, jerr - } - - m := *issue - f := m["fields"] - cf := make(CustomFields) - if f == nil { - return cf, resp, nil - } - - if rec, ok := f.(map[string]interface{}); ok { - for key, val := range rec { - if strings.Contains(key, "customfield") { - if valMap, ok := val.(map[string]interface{}); ok { - if v, ok := valMap["value"]; ok { - val = v - } - } - cf[key] = fmt.Sprint(val) - } - } - } - return cf, resp, nil -} - -// GetTransitions gets a list of the transitions possible for this issue by the current user, -// along with fields that are required and their types. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getTransitions -func (s *IssueService) GetTransitions(id string) ([]Transition, *Response, error) { - apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/transitions?expand=transitions.fields", id) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - - result := new(transitionResult) - resp, err := s.client.Do(req, result) - if err != nil { - err = NewJiraError(resp, err) - } - return result.Transitions, resp, err -} - -// DoTransition performs a transition on an issue. -// When performing the transition you can update or set other issue fields. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-doTransition -func (s *IssueService) DoTransition(ticketID, transitionID string) (*Response, error) { - payload := CreateTransitionPayload{ - Transition: TransitionPayload{ - ID: transitionID, - }, - } - return s.DoTransitionWithPayload(ticketID, payload) -} - -// DoTransitionWithPayload performs a transition on an issue using any payload. -// When performing the transition you can update or set other issue fields. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-doTransition -func (s *IssueService) DoTransitionWithPayload(ticketID, payload interface{}) (*Response, error) { - apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/transitions", ticketID) - - req, err := s.client.NewRequest("POST", apiEndpoint, payload) - if err != nil { - return nil, err - } - - resp, err := s.client.Do(req, nil) - if err != nil { - err = NewJiraError(resp, err) - } - - return resp, err -} - -// InitIssueWithMetaAndFields returns Issue with with values from fieldsConfig properly set. -// * metaProject should contain metaInformation about the project where the issue should be created. -// * metaIssuetype is the MetaInformation about the Issuetype that needs to be created. -// * fieldsConfig is a key->value pair where key represents the name of the field as seen in the UI -// And value is the string value for that particular key. -// Note: This method doesn't verify that the fieldsConfig is complete with mandatory fields. The fieldsConfig is -// supposed to be already verified with MetaIssueType.CheckCompleteAndAvailable. It will however return -// error if the key is not found. -// All values will be packed into Unknowns. This is much convenient. If the struct fields needs to be -// configured as well, marshalling and unmarshalling will set the proper fields. -func InitIssueWithMetaAndFields(metaProject *MetaProject, metaIssuetype *MetaIssueType, fieldsConfig map[string]string) (*Issue, error) { - issue := new(Issue) - issueFields := new(IssueFields) - issueFields.Unknowns = tcontainer.NewMarshalMap() - - // map the field names the User presented to jira's internal key - allFields, _ := metaIssuetype.GetAllFields() - for key, value := range fieldsConfig { - jiraKey, found := allFields[key] - if !found { - return nil, fmt.Errorf("key %s is not found in the list of fields", key) - } - - valueType, err := metaIssuetype.Fields.String(jiraKey + "/schema/type") - if err != nil { - return nil, err - } - switch valueType { - case "array": - elemType, err := metaIssuetype.Fields.String(jiraKey + "/schema/items") - if err != nil { - return nil, err - } - switch elemType { - case "component": - issueFields.Unknowns[jiraKey] = []Component{{Name: value}} - case "option": - issueFields.Unknowns[jiraKey] = []map[string]string{{"value": value}} - default: - issueFields.Unknowns[jiraKey] = []string{value} - } - case "string": - issueFields.Unknowns[jiraKey] = value - case "date": - issueFields.Unknowns[jiraKey] = value - case "datetime": - issueFields.Unknowns[jiraKey] = value - case "any": - // Treat any as string - issueFields.Unknowns[jiraKey] = value - case "project": - issueFields.Unknowns[jiraKey] = Project{ - Name: metaProject.Name, - ID: metaProject.Id, - } - case "priority": - issueFields.Unknowns[jiraKey] = Priority{Name: value} - case "user": - issueFields.Unknowns[jiraKey] = User{ - Name: value, - } - case "issuetype": - issueFields.Unknowns[jiraKey] = IssueType{ - Name: value, - } - case "option": - issueFields.Unknowns[jiraKey] = Option{ - Value: value, - } - default: - return nil, fmt.Errorf("Unknown issue type encountered: %s for %s", valueType, key) - } - } - - issue.Fields = issueFields - - return issue, nil -} - -// Delete will delete a specified issue. -func (s *IssueService) Delete(issueID string) (*Response, error) { - apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID) - - // to enable deletion of subtasks; without this, the request will fail if the issue has subtasks - deletePayload := make(map[string]interface{}) - deletePayload["deleteSubtasks"] = "true" - content, _ := json.Marshal(deletePayload) - - req, err := s.client.NewRequest("DELETE", apiEndpoint, content) - if err != nil { - return nil, err - } - - resp, err := s.client.Do(req, nil) - return resp, err -} - -// GetWatchers wil return all the users watching/observing the given issue -// -// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-getIssueWatchers -func (s *IssueService) GetWatchers(issueID string) (*[]User, *Response, error) { - watchesAPIEndpoint := fmt.Sprintf("rest/api/2/issue/%s/watchers", issueID) - - req, err := s.client.NewRequest("GET", watchesAPIEndpoint, nil) - if err != nil { - return nil, nil, err - } - - watches := new(Watches) - resp, err := s.client.Do(req, watches) - if err != nil { - return nil, nil, NewJiraError(resp, err) - } - - result := []User{} - user := new(User) - for _, watcher := range watches.Watchers { - user, resp, err = s.client.User.Get(watcher.Name) - if err != nil { - return nil, resp, NewJiraError(resp, err) - } - result = append(result, *user) - } - - return &result, resp, nil -} - -// AddWatcher adds watcher to the given issue -// -// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-addWatcher -func (s *IssueService) AddWatcher(issueID string, userName string) (*Response, error) { - apiEndPoint := fmt.Sprintf("rest/api/2/issue/%s/watchers", issueID) - - req, err := s.client.NewRequest("POST", apiEndPoint, userName) - if err != nil { - return nil, err - } - - resp, err := s.client.Do(req, nil) - if err != nil { - err = NewJiraError(resp, err) - } - - return resp, err -} - -// RemoveWatcher removes given user from given issue -// -// JIRA API docs: https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-removeWatcher -func (s *IssueService) RemoveWatcher(issueID string, userName string) (*Response, error) { - apiEndPoint := fmt.Sprintf("rest/api/2/issue/%s/watchers", issueID) - - req, err := s.client.NewRequest("DELETE", apiEndPoint, userName) - if err != nil { - return nil, err - } - - resp, err := s.client.Do(req, nil) - if err != nil { - err = NewJiraError(resp, err) - } - - return resp, err -} diff --git a/vendor/github.com/andygrunwald/go-jira/jira.go b/vendor/github.com/andygrunwald/go-jira/jira.go deleted file mode 100644 index 49a1685e..00000000 --- a/vendor/github.com/andygrunwald/go-jira/jira.go +++ /dev/null @@ -1,451 +0,0 @@ -package jira - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "reflect" - "strings" - "time" - - "github.com/google/go-querystring/query" - "github.com/pkg/errors" -) - -// A Client manages communication with the JIRA API. -type Client struct { - // HTTP client used to communicate with the API. - client *http.Client - - // Base URL for API requests. - baseURL *url.URL - - // Session storage if the user authentificate with a Session cookie - session *Session - - // Services used for talking to different parts of the JIRA API. - Authentication *AuthenticationService - Issue *IssueService - Project *ProjectService - Board *BoardService - Sprint *SprintService - User *UserService - Group *GroupService - Version *VersionService - Priority *PriorityService - Field *FieldService - Component *ComponentService - Resolution *ResolutionService - StatusCategory *StatusCategoryService -} - -// NewClient returns a new JIRA API client. -// If a nil httpClient is provided, http.DefaultClient will be used. -// To use API methods which require authentication you can follow the preferred solution and -// provide an http.Client that will perform the authentication for you with OAuth and HTTP Basic (such as that provided by the golang.org/x/oauth2 library). -// As an alternative you can use Session Cookie based authentication provided by this package as well. -// See https://docs.atlassian.com/jira/REST/latest/#authentication -// baseURL is the HTTP endpoint of your JIRA instance and should always be specified with a trailing slash. -func NewClient(httpClient *http.Client, baseURL string) (*Client, error) { - if httpClient == nil { - httpClient = http.DefaultClient - } - - // ensure the baseURL contains a trailing slash so that all paths are preserved in later calls - if !strings.HasSuffix(baseURL, "/") { - baseURL += "/" - } - - parsedBaseURL, err := url.Parse(baseURL) - if err != nil { - return nil, err - } - - c := &Client{ - client: httpClient, - baseURL: parsedBaseURL, - } - c.Authentication = &AuthenticationService{client: c} - c.Issue = &IssueService{client: c} - c.Project = &ProjectService{client: c} - c.Board = &BoardService{client: c} - c.Sprint = &SprintService{client: c} - c.User = &UserService{client: c} - c.Group = &GroupService{client: c} - c.Version = &VersionService{client: c} - c.Priority = &PriorityService{client: c} - c.Field = &FieldService{client: c} - c.Component = &ComponentService{client: c} - c.Resolution = &ResolutionService{client: c} - c.StatusCategory = &StatusCategoryService{client: c} - - return c, nil -} - -// NewRawRequest creates an API request. -// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client. -// Allows using an optional native io.Reader for sourcing the request body. -func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*http.Request, error) { - rel, err := url.Parse(urlStr) - if err != nil { - return nil, err - } - // Relative URLs should be specified without a preceding slash since baseURL will have the trailing slash - rel.Path = strings.TrimLeft(rel.Path, "/") - - u := c.baseURL.ResolveReference(rel) - - req, err := http.NewRequest(method, u.String(), body) - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/json") - - // Set authentication information - if c.Authentication.authType == authTypeSession { - // Set session cookie if there is one - if c.session != nil { - for _, cookie := range c.session.Cookies { - req.AddCookie(cookie) - } - } - } else if c.Authentication.authType == authTypeBasic { - // Set basic auth information - if c.Authentication.username != "" { - req.SetBasicAuth(c.Authentication.username, c.Authentication.password) - } - } - - return req, nil -} - -// NewRequest creates an API request. -// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client. -// If specified, the value pointed to by body is JSON encoded and included as the request body. -func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { - rel, err := url.Parse(urlStr) - if err != nil { - return nil, err - } - // Relative URLs should be specified without a preceding slash since baseURL will have the trailing slash - rel.Path = strings.TrimLeft(rel.Path, "/") - - u := c.baseURL.ResolveReference(rel) - - var buf io.ReadWriter - if body != nil { - buf = new(bytes.Buffer) - err = json.NewEncoder(buf).Encode(body) - if err != nil { - return nil, err - } - } - - req, err := http.NewRequest(method, u.String(), buf) - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/json") - - // Set authentication information - if c.Authentication.authType == authTypeSession { - // Set session cookie if there is one - if c.session != nil { - for _, cookie := range c.session.Cookies { - req.AddCookie(cookie) - } - } - } else if c.Authentication.authType == authTypeBasic { - // Set basic auth information - if c.Authentication.username != "" { - req.SetBasicAuth(c.Authentication.username, c.Authentication.password) - } - } - - return req, nil -} - -// addOptions adds the parameters in opt as URL query parameters to s. opt -// must be a struct whose fields may contain "url" tags. -func addOptions(s string, opt interface{}) (string, error) { - v := reflect.ValueOf(opt) - if v.Kind() == reflect.Ptr && v.IsNil() { - return s, nil - } - - u, err := url.Parse(s) - if err != nil { - return s, err - } - - qs, err := query.Values(opt) - if err != nil { - return s, err - } - - u.RawQuery = qs.Encode() - return u.String(), nil -} - -// NewMultiPartRequest creates an API request including a multi-part file. -// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client. -// If specified, the value pointed to by buf is a multipart form. -func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (*http.Request, error) { - rel, err := url.Parse(urlStr) - if err != nil { - return nil, err - } - // Relative URLs should be specified without a preceding slash since baseURL will have the trailing slash - rel.Path = strings.TrimLeft(rel.Path, "/") - - u := c.baseURL.ResolveReference(rel) - - req, err := http.NewRequest(method, u.String(), buf) - if err != nil { - return nil, err - } - - // Set required headers - req.Header.Set("X-Atlassian-Token", "nocheck") - - // Set authentication information - if c.Authentication.authType == authTypeSession { - // Set session cookie if there is one - if c.session != nil { - for _, cookie := range c.session.Cookies { - req.AddCookie(cookie) - } - } - } else if c.Authentication.authType == authTypeBasic { - // Set basic auth information - if c.Authentication.username != "" { - req.SetBasicAuth(c.Authentication.username, c.Authentication.password) - } - } - - return req, nil -} - -// Do sends an API request and returns the API response. -// The API response is JSON decoded and stored in the value pointed to by v, or returned as an error if an API error has occurred. -func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { - httpResp, err := c.client.Do(req) - if err != nil { - return nil, err - } - - err = CheckResponse(httpResp) - if err != nil { - // Even though there was an error, we still return the response - // in case the caller wants to inspect it further - return newResponse(httpResp, nil), err - } - - if v != nil { - // Open a NewDecoder and defer closing the reader only if there is a provided interface to decode to - defer httpResp.Body.Close() - err = json.NewDecoder(httpResp.Body).Decode(v) - } - - resp := newResponse(httpResp, v) - return resp, err -} - -// CheckResponse checks the API response for errors, and returns them if present. -// A response is considered an error if it has a status code outside the 200 range. -// The caller is responsible to analyze the response body. -// The body can contain JSON (if the error is intended) or xml (sometimes JIRA just failes). -func CheckResponse(r *http.Response) error { - if c := r.StatusCode; 200 <= c && c <= 299 { - return nil - } - - err := fmt.Errorf("Request failed. Please analyze the request body for more details. Status code: %d", r.StatusCode) - return err -} - -// GetBaseURL will return you the Base URL. -// This is the same URL as in the NewClient constructor -func (c *Client) GetBaseURL() url.URL { - return *c.baseURL -} - -// Response represents JIRA API response. It wraps http.Response returned from -// API and provides information about paging. -type Response struct { - *http.Response - - StartAt int - MaxResults int - Total int -} - -func newResponse(r *http.Response, v interface{}) *Response { - resp := &Response{Response: r} - resp.populatePageValues(v) - return resp -} - -// Sets paging values if response json was parsed to searchResult type -// (can be extended with other types if they also need paging info) -func (r *Response) populatePageValues(v interface{}) { - switch value := v.(type) { - case *searchResult: - r.StartAt = value.StartAt - r.MaxResults = value.MaxResults - r.Total = value.Total - case *groupMembersResult: - r.StartAt = value.StartAt - r.MaxResults = value.MaxResults - r.Total = value.Total - } - return -} - -// BasicAuthTransport is an http.RoundTripper that authenticates all requests -// using HTTP Basic Authentication with the provided username and password. -type BasicAuthTransport struct { - Username string - Password string - - // Transport is the underlying HTTP transport to use when making requests. - // It will default to http.DefaultTransport if nil. - Transport http.RoundTripper -} - -// RoundTrip implements the RoundTripper interface. We just add the -// basic auth and return the RoundTripper for this transport type. -func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { - req2 := cloneRequest(req) // per RoundTripper contract - - req2.SetBasicAuth(t.Username, t.Password) - return t.transport().RoundTrip(req2) -} - -// Client returns an *http.Client that makes requests that are authenticated -// using HTTP Basic Authentication. This is a nice little bit of sugar -// so we can just get the client instead of creating the client in the calling code. -// If it's necessary to send more information on client init, the calling code can -// always skip this and set the transport itself. -func (t *BasicAuthTransport) Client() *http.Client { - return &http.Client{Transport: t} -} - -func (t *BasicAuthTransport) transport() http.RoundTripper { - if t.Transport != nil { - return t.Transport - } - return http.DefaultTransport -} - -// CookieAuthTransport is an http.RoundTripper that authenticates all requests -// using Jira's cookie-based authentication. -// -// Note that it is generally preferrable to use HTTP BASIC authentication with the REST API. -// However, this resource may be used to mimic the behaviour of JIRA's log-in page (e.g. to display log-in errors to a user). -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session -type CookieAuthTransport struct { - Username string - Password string - AuthURL string - - // SessionObject is the authenticated cookie string.s - // It's passed in each call to prove the client is authenticated. - SessionObject []*http.Cookie - - // Transport is the underlying HTTP transport to use when making requests. - // It will default to http.DefaultTransport if nil. - Transport http.RoundTripper -} - -// RoundTrip adds the session object to the request. -func (t *CookieAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { - if t.SessionObject == nil { - err := t.setSessionObject() - if err != nil { - return nil, errors.Wrap(err, "cookieauth: no session object has been set") - } - } - - req2 := cloneRequest(req) // per RoundTripper contract - for _, cookie := range t.SessionObject { - req2.AddCookie(cookie) - } - - return t.transport().RoundTrip(req2) -} - -// Client returns an *http.Client that makes requests that are authenticated -// using cookie authentication -func (t *CookieAuthTransport) Client() *http.Client { - return &http.Client{Transport: t} -} - -// setSessionObject attempts to authenticate the user and set -// the session object (e.g. cookie) -func (t *CookieAuthTransport) setSessionObject() error { - req, err := t.buildAuthRequest() - if err != nil { - return err - } - - var authClient = &http.Client{ - Timeout: time.Second * 60, - } - resp, err := authClient.Do(req) - if err != nil { - return err - } - - t.SessionObject = resp.Cookies() - return nil -} - -// getAuthRequest assembles the request to get the authenticated cookie -func (t *CookieAuthTransport) buildAuthRequest() (*http.Request, error) { - body := struct { - Username string `json:"username"` - Password string `json:"password"` - }{ - t.Username, - t.Password, - } - - b := new(bytes.Buffer) - json.NewEncoder(b).Encode(body) - - req, err := http.NewRequest("POST", t.AuthURL, b) - if err != nil { - return nil, err - } - - req.Header.Set("Content-Type", "application/json") - return req, nil -} - -func (t *CookieAuthTransport) transport() http.RoundTripper { - if t.Transport != nil { - return t.Transport - } - return http.DefaultTransport -} - -// cloneRequest returns a clone of the provided *http.Request. -// The clone is a shallow copy of the struct and its Header map. -func cloneRequest(r *http.Request) *http.Request { - // shallow copy of the struct - r2 := new(http.Request) - *r2 = *r - // deep copy of the Header - r2.Header = make(http.Header, len(r.Header)) - for k, s := range r.Header { - r2.Header[k] = append([]string(nil), s...) - } - return r2 -} diff --git a/vendor/github.com/andygrunwald/go-jira/metaissue.go b/vendor/github.com/andygrunwald/go-jira/metaissue.go deleted file mode 100644 index 19813786..00000000 --- a/vendor/github.com/andygrunwald/go-jira/metaissue.go +++ /dev/null @@ -1,194 +0,0 @@ -package jira - -import ( - "fmt" - "strings" - - "github.com/google/go-querystring/query" - "github.com/trivago/tgo/tcontainer" -) - -// CreateMetaInfo contains information about fields and their attributed to create a ticket. -type CreateMetaInfo struct { - Expand string `json:"expand,omitempty"` - Projects []*MetaProject `json:"projects,omitempty"` -} - -// MetaProject is the meta information about a project returned from createmeta api -type MetaProject struct { - Expand string `json:"expand,omitempty"` - Self string `json:"self,omitempty"` - Id string `json:"id,omitempty"` - Key string `json:"key,omitempty"` - Name string `json:"name,omitempty"` - // omitted avatarUrls - IssueTypes []*MetaIssueType `json:"issuetypes,omitempty"` -} - -// MetaIssueType represents the different issue types a project has. -// -// Note: Fields is interface because this is an object which can -// have arbitraty keys related to customfields. It is not possible to -// expect these for a general way. This will be returning a map. -// Further processing must be done depending on what is required. -type MetaIssueType struct { - Self string `json:"self,omitempty"` - Id string `json:"id,omitempty"` - Description string `json:"description,omitempty"` - IconUrl string `json:"iconurl,omitempty"` - Name string `json:"name,omitempty"` - Subtasks bool `json:"subtask,omitempty"` - Expand string `json:"expand,omitempty"` - Fields tcontainer.MarshalMap `json:"fields,omitempty"` -} - -// GetCreateMeta makes the api call to get the meta information required to create a ticket -func (s *IssueService) GetCreateMeta(projectkeys string) (*CreateMetaInfo, *Response, error) { - return s.GetCreateMetaWithOptions(&GetQueryOptions{ProjectKeys: projectkeys, Expand: "projects.issuetypes.fields"}) -} - -// GetCreateMetaWithOptions makes the api call to get the meta information without requiring to have a projectKey -func (s *IssueService) GetCreateMetaWithOptions(options *GetQueryOptions) (*CreateMetaInfo, *Response, error) { - apiEndpoint := "rest/api/2/issue/createmeta" - - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - if options != nil { - q, err := query.Values(options) - if err != nil { - return nil, nil, err - } - req.URL.RawQuery = q.Encode() - } - - meta := new(CreateMetaInfo) - resp, err := s.client.Do(req, meta) - - if err != nil { - return nil, resp, err - } - - return meta, resp, nil -} - -// GetProjectWithName returns a project with "name" from the meta information received. If not found, this returns nil. -// The comparison of the name is case insensitive. -func (m *CreateMetaInfo) GetProjectWithName(name string) *MetaProject { - for _, m := range m.Projects { - if strings.ToLower(m.Name) == strings.ToLower(name) { - return m - } - } - return nil -} - -// GetProjectWithKey returns a project with "name" from the meta information received. If not found, this returns nil. -// The comparison of the name is case insensitive. -func (m *CreateMetaInfo) GetProjectWithKey(key string) *MetaProject { - for _, m := range m.Projects { - if strings.ToLower(m.Key) == strings.ToLower(key) { - return m - } - } - return nil -} - -// GetIssueTypeWithName returns an IssueType with name from a given MetaProject. If not found, this returns nil. -// The comparison of the name is case insensitive -func (p *MetaProject) GetIssueTypeWithName(name string) *MetaIssueType { - for _, m := range p.IssueTypes { - if strings.ToLower(m.Name) == strings.ToLower(name) { - return m - } - } - return nil -} - -// GetMandatoryFields returns a map of all the required fields from the MetaIssueTypes. -// if a field returned by the api was: -// "customfield_10806": { -// "required": true, -// "schema": { -// "type": "any", -// "custom": "com.pyxis.greenhopper.jira:gh-epic-link", -// "customId": 10806 -// }, -// "name": "Epic Link", -// "hasDefaultValue": false, -// "operations": [ -// "set" -// ] -// } -// the returned map would have "Epic Link" as the key and "customfield_10806" as value. -// This choice has been made so that the it is easier to generate the create api request later. -func (t *MetaIssueType) GetMandatoryFields() (map[string]string, error) { - ret := make(map[string]string) - for key := range t.Fields { - required, err := t.Fields.Bool(key + "/required") - if err != nil { - return nil, err - } - if required { - name, err := t.Fields.String(key + "/name") - if err != nil { - return nil, err - } - ret[name] = key - } - } - return ret, nil -} - -// GetAllFields returns a map of all the fields for an IssueType. This includes all required and not required. -// The key of the returned map is what you see in the form and the value is how it is representated in the jira schema. -func (t *MetaIssueType) GetAllFields() (map[string]string, error) { - ret := make(map[string]string) - for key := range t.Fields { - - name, err := t.Fields.String(key + "/name") - if err != nil { - return nil, err - } - ret[name] = key - } - return ret, nil -} - -// CheckCompleteAndAvailable checks if the given fields satisfies the mandatory field required to create a issue for the given type -// And also if the given fields are available. -func (t *MetaIssueType) CheckCompleteAndAvailable(config map[string]string) (bool, error) { - mandatory, err := t.GetMandatoryFields() - if err != nil { - return false, err - } - all, err := t.GetAllFields() - if err != nil { - return false, err - } - - // check templateconfig against mandatory fields - for key := range mandatory { - if _, okay := config[key]; !okay { - var requiredFields []string - for name := range mandatory { - requiredFields = append(requiredFields, name) - } - return false, fmt.Errorf("Required field not found in provided jira.fields. Required are: %#v", requiredFields) - } - } - - // check templateConfig against all fields to verify they are available - for key := range config { - if _, okay := all[key]; !okay { - var availableFields []string - for name := range all { - availableFields = append(availableFields, name) - } - return false, fmt.Errorf("Fields in jira.fields are not available in jira. Available are: %#v", availableFields) - } - } - - return true, nil -} diff --git a/vendor/github.com/andygrunwald/go-jira/priority.go b/vendor/github.com/andygrunwald/go-jira/priority.go deleted file mode 100644 index 481f9592..00000000 --- a/vendor/github.com/andygrunwald/go-jira/priority.go +++ /dev/null @@ -1,37 +0,0 @@ -package jira - -// PriorityService handles priorities for the JIRA instance / API. -// -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Priority -type PriorityService struct { - client *Client -} - -// Priority represents a priority of a JIRA issue. -// Typical types are "Normal", "Moderate", "Urgent", ... -type Priority struct { - Self string `json:"self,omitempty" structs:"self,omitempty"` - IconURL string `json:"iconUrl,omitempty" structs:"iconUrl,omitempty"` - Name string `json:"name,omitempty" structs:"name,omitempty"` - ID string `json:"id,omitempty" structs:"id,omitempty"` - StatusColor string `json:"statusColor,omitempty" structs:"statusColor,omitempty"` - Description string `json:"description,omitempty" structs:"description,omitempty"` -} - -// GetList gets all priorities from JIRA -// -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-priority-get -func (s *PriorityService) GetList() ([]Priority, *Response, error) { - apiEndpoint := "rest/api/2/priority" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - - priorityList := []Priority{} - resp, err := s.client.Do(req, &priorityList) - if err != nil { - return nil, resp, NewJiraError(resp, err) - } - return priorityList, resp, nil -} diff --git a/vendor/github.com/andygrunwald/go-jira/project.go b/vendor/github.com/andygrunwald/go-jira/project.go deleted file mode 100644 index b71b5bba..00000000 --- a/vendor/github.com/andygrunwald/go-jira/project.go +++ /dev/null @@ -1,162 +0,0 @@ -package jira - -import ( - "fmt" - - "github.com/google/go-querystring/query" -) - -// ProjectService handles projects for the JIRA instance / API. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project -type ProjectService struct { - client *Client -} - -// ProjectList represent a list of Projects -type ProjectList []struct { - Expand string `json:"expand" structs:"expand"` - Self string `json:"self" structs:"self"` - ID string `json:"id" structs:"id"` - Key string `json:"key" structs:"key"` - Name string `json:"name" structs:"name"` - AvatarUrls AvatarUrls `json:"avatarUrls" structs:"avatarUrls"` - ProjectTypeKey string `json:"projectTypeKey" structs:"projectTypeKey"` - ProjectCategory ProjectCategory `json:"projectCategory,omitempty" structs:"projectsCategory,omitempty"` - IssueTypes []IssueType `json:"issueTypes,omitempty" structs:"issueTypes,omitempty"` -} - -// ProjectCategory represents a single project category -type ProjectCategory struct { - Self string `json:"self" structs:"self,omitempty"` - ID string `json:"id" structs:"id,omitempty"` - Name string `json:"name" structs:"name,omitempty"` - Description string `json:"description" structs:"description,omitempty"` -} - -// Project represents a JIRA Project. -type Project struct { - Expand string `json:"expand,omitempty" structs:"expand,omitempty"` - Self string `json:"self,omitempty" structs:"self,omitempty"` - ID string `json:"id,omitempty" structs:"id,omitempty"` - Key string `json:"key,omitempty" structs:"key,omitempty"` - Description string `json:"description,omitempty" structs:"description,omitempty"` - Lead User `json:"lead,omitempty" structs:"lead,omitempty"` - Components []ProjectComponent `json:"components,omitempty" structs:"components,omitempty"` - IssueTypes []IssueType `json:"issueTypes,omitempty" structs:"issueTypes,omitempty"` - URL string `json:"url,omitempty" structs:"url,omitempty"` - Email string `json:"email,omitempty" structs:"email,omitempty"` - AssigneeType string `json:"assigneeType,omitempty" structs:"assigneeType,omitempty"` - Versions []Version `json:"versions,omitempty" structs:"versions,omitempty"` - Name string `json:"name,omitempty" structs:"name,omitempty"` - Roles struct { - Developers string `json:"Developers,omitempty" structs:"Developers,omitempty"` - } `json:"roles,omitempty" structs:"roles,omitempty"` - AvatarUrls AvatarUrls `json:"avatarUrls,omitempty" structs:"avatarUrls,omitempty"` - ProjectCategory ProjectCategory `json:"projectCategory,omitempty" structs:"projectCategory,omitempty"` -} - -// ProjectComponent represents a single component of a project -type ProjectComponent struct { - Self string `json:"self" structs:"self,omitempty"` - ID string `json:"id" structs:"id,omitempty"` - Name string `json:"name" structs:"name,omitempty"` - Description string `json:"description" structs:"description,omitempty"` - Lead User `json:"lead,omitempty" structs:"lead,omitempty"` - AssigneeType string `json:"assigneeType" structs:"assigneeType,omitempty"` - Assignee User `json:"assignee" structs:"assignee,omitempty"` - RealAssigneeType string `json:"realAssigneeType" structs:"realAssigneeType,omitempty"` - RealAssignee User `json:"realAssignee" structs:"realAssignee,omitempty"` - IsAssigneeTypeValid bool `json:"isAssigneeTypeValid" structs:"isAssigneeTypeValid,omitempty"` - Project string `json:"project" structs:"project,omitempty"` - ProjectID int `json:"projectId" structs:"projectId,omitempty"` -} - -// PermissionScheme represents the permission scheme for the project -type PermissionScheme struct { - Expand string `json:"expand" structs:"expand,omitempty"` - Self string `json:"self" structs:"self,omitempty"` - ID int `json:"id" structs:"id,omitempty"` - Name string `json:"name" structs:"name,omitempty"` - Description string `json:"description" structs:"description,omitempty"` -} - -// GetList gets all projects form JIRA -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects -func (s *ProjectService) GetList() (*ProjectList, *Response, error) { - return s.ListWithOptions(&GetQueryOptions{}) -} - -// ListWithOptions gets all projects form JIRA with optional query params, like &GetQueryOptions{Expand: "issueTypes"} to get -// a list of all projects and their supported issuetypes -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects -func (s *ProjectService) ListWithOptions(options *GetQueryOptions) (*ProjectList, *Response, error) { - apiEndpoint := "rest/api/2/project" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - - if options != nil { - q, err := query.Values(options) - if err != nil { - return nil, nil, err - } - req.URL.RawQuery = q.Encode() - } - - projectList := new(ProjectList) - resp, err := s.client.Do(req, projectList) - if err != nil { - jerr := NewJiraError(resp, err) - return nil, resp, jerr - } - - return projectList, resp, nil -} - -// Get returns a full representation of the project for the given issue key. -// JIRA will attempt to identify the project by the projectIdOrKey path parameter. -// This can be an project id, or an project key. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject -func (s *ProjectService) Get(projectID string) (*Project, *Response, error) { - apiEndpoint := fmt.Sprintf("rest/api/2/project/%s", projectID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - - project := new(Project) - resp, err := s.client.Do(req, project) - if err != nil { - jerr := NewJiraError(resp, err) - return nil, resp, jerr - } - - return project, resp, nil -} - -// GetPermissionScheme returns a full representation of the permission scheme for the project -// JIRA will attempt to identify the project by the projectIdOrKey path parameter. -// This can be an project id, or an project key. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject -func (s *ProjectService) GetPermissionScheme(projectID string) (*PermissionScheme, *Response, error) { - apiEndpoint := fmt.Sprintf("/rest/api/2/project/%s/permissionscheme", projectID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - - ps := new(PermissionScheme) - resp, err := s.client.Do(req, ps) - if err != nil { - jerr := NewJiraError(resp, err) - return nil, resp, jerr - } - - return ps, resp, nil -} diff --git a/vendor/github.com/andygrunwald/go-jira/resolution.go b/vendor/github.com/andygrunwald/go-jira/resolution.go deleted file mode 100644 index 36a651fb..00000000 --- a/vendor/github.com/andygrunwald/go-jira/resolution.go +++ /dev/null @@ -1,35 +0,0 @@ -package jira - -// ResolutionService handles resolutions for the JIRA instance / API. -// -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Resolution -type ResolutionService struct { - client *Client -} - -// Resolution represents a resolution of a JIRA issue. -// Typical types are "Fixed", "Suspended", "Won't Fix", ... -type Resolution struct { - Self string `json:"self" structs:"self"` - ID string `json:"id" structs:"id"` - Description string `json:"description" structs:"description"` - Name string `json:"name" structs:"name"` -} - -// GetList gets all resolutions from JIRA -// -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-resolution-get -func (s *ResolutionService) GetList() ([]Resolution, *Response, error) { - apiEndpoint := "rest/api/2/resolution" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - - resolutionList := []Resolution{} - resp, err := s.client.Do(req, &resolutionList) - if err != nil { - return nil, resp, NewJiraError(resp, err) - } - return resolutionList, resp, nil -} diff --git a/vendor/github.com/andygrunwald/go-jira/sprint.go b/vendor/github.com/andygrunwald/go-jira/sprint.go deleted file mode 100644 index 7e8e697d..00000000 --- a/vendor/github.com/andygrunwald/go-jira/sprint.go +++ /dev/null @@ -1,107 +0,0 @@ -package jira - -import ( - "fmt" - - "github.com/google/go-querystring/query" -) - -// SprintService handles sprints in JIRA Agile API. -// See https://docs.atlassian.com/jira-software/REST/cloud/ -type SprintService struct { - client *Client -} - -// IssuesWrapper represents a wrapper struct for moving issues to sprint -type IssuesWrapper struct { - Issues []string `json:"issues"` -} - -// IssuesInSprintResult represents a wrapper struct for search result -type IssuesInSprintResult struct { - Issues []Issue `json:"issues"` -} - -// MoveIssuesToSprint moves issues to a sprint, for a given sprint Id. -// Issues can only be moved to open or active sprints. -// The maximum number of issues that can be moved in one operation is 50. -// -// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-moveIssuesToSprint -func (s *SprintService) MoveIssuesToSprint(sprintID int, issueIDs []string) (*Response, error) { - apiEndpoint := fmt.Sprintf("rest/agile/1.0/sprint/%d/issue", sprintID) - - payload := IssuesWrapper{Issues: issueIDs} - - req, err := s.client.NewRequest("POST", apiEndpoint, payload) - - if err != nil { - return nil, err - } - - resp, err := s.client.Do(req, nil) - if err != nil { - err = NewJiraError(resp, err) - } - return resp, err -} - -// GetIssuesForSprint returns all issues in a sprint, for a given sprint Id. -// This only includes issues that the user has permission to view. -// By default, the returned issues are ordered by rank. -// -// JIRA API Docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-getIssuesForSprint -func (s *SprintService) GetIssuesForSprint(sprintID int) ([]Issue, *Response, error) { - apiEndpoint := fmt.Sprintf("rest/agile/1.0/sprint/%d/issue", sprintID) - - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - - if err != nil { - return nil, nil, err - } - - result := new(IssuesInSprintResult) - resp, err := s.client.Do(req, result) - if err != nil { - err = NewJiraError(resp, err) - } - - return result.Issues, resp, err -} - -// GetIssue returns a full representation of the issue for the given issue key. -// JIRA will attempt to identify the issue by the issueIdOrKey path parameter. -// This can be an issue id, or an issue key. -// If the issue cannot be found via an exact match, JIRA will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved. -// -// The given options will be appended to the query string -// -// JIRA API docs: https://docs.atlassian.com/jira-software/REST/7.3.1/#agile/1.0/issue-getIssue -// -// TODO: create agile service for holding all agile apis' implementation -func (s *SprintService) GetIssue(issueID string, options *GetQueryOptions) (*Issue, *Response, error) { - apiEndpoint := fmt.Sprintf("rest/agile/1.0/issue/%s", issueID) - - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - - if err != nil { - return nil, nil, err - } - - if options != nil { - q, err := query.Values(options) - if err != nil { - return nil, nil, err - } - req.URL.RawQuery = q.Encode() - } - - issue := new(Issue) - resp, err := s.client.Do(req, issue) - - if err != nil { - jerr := NewJiraError(resp, err) - return nil, resp, jerr - } - - return issue, resp, nil -} diff --git a/vendor/github.com/andygrunwald/go-jira/statuscategory.go b/vendor/github.com/andygrunwald/go-jira/statuscategory.go deleted file mode 100644 index 05db4207..00000000 --- a/vendor/github.com/andygrunwald/go-jira/statuscategory.go +++ /dev/null @@ -1,44 +0,0 @@ -package jira - -// StatusCategoryService handles status categories for the JIRA instance / API. -// -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Statuscategory -type StatusCategoryService struct { - client *Client -} - -// StatusCategory represents the category a status belongs to. -// Those categories can be user defined in every JIRA instance. -type StatusCategory struct { - Self string `json:"self" structs:"self"` - ID int `json:"id" structs:"id"` - Name string `json:"name" structs:"name"` - Key string `json:"key" structs:"key"` - ColorName string `json:"colorName" structs:"colorName"` -} - -// These constants are the keys of the default JIRA status categories -const ( - StatusCategoryComplete = "done" - StatusCategoryInProgress = "indeterminate" - StatusCategoryToDo = "new" - StatusCategoryUndefined = "undefined" -) - -// GetList gets all status categories from JIRA -// -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-statuscategory-get -func (s *StatusCategoryService) GetList() ([]StatusCategory, *Response, error) { - apiEndpoint := "rest/api/2/statuscategory" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - - statusCategoryList := []StatusCategory{} - resp, err := s.client.Do(req, &statusCategoryList) - if err != nil { - return nil, resp, NewJiraError(resp, err) - } - return statusCategoryList, resp, nil -} diff --git a/vendor/github.com/andygrunwald/go-jira/user.go b/vendor/github.com/andygrunwald/go-jira/user.go deleted file mode 100644 index 10fe5e0f..00000000 --- a/vendor/github.com/andygrunwald/go-jira/user.go +++ /dev/null @@ -1,136 +0,0 @@ -package jira - -import ( - "encoding/json" - "fmt" - "io/ioutil" -) - -// UserService handles users for the JIRA instance / API. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user -type UserService struct { - client *Client -} - -// User represents a JIRA user. -type User struct { - Self string `json:"self,omitempty" structs:"self,omitempty"` - Name string `json:"name,omitempty" structs:"name,omitempty"` - Password string `json:"-"` - Key string `json:"key,omitempty" structs:"key,omitempty"` - EmailAddress string `json:"emailAddress,omitempty" structs:"emailAddress,omitempty"` - AvatarUrls AvatarUrls `json:"avatarUrls,omitempty" structs:"avatarUrls,omitempty"` - DisplayName string `json:"displayName,omitempty" structs:"displayName,omitempty"` - Active bool `json:"active,omitempty" structs:"active,omitempty"` - TimeZone string `json:"timeZone,omitempty" structs:"timeZone,omitempty"` - ApplicationKeys []string `json:"applicationKeys,omitempty" structs:"applicationKeys,omitempty"` -} - -// UserGroup represents the group list -type UserGroup struct { - Self string `json:"self,omitempty" structs:"self,omitempty"` - Name string `json:"name,omitempty" structs:"name,omitempty"` -} - -// Get gets user info from JIRA -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-getUser -func (s *UserService) Get(username string) (*User, *Response, error) { - apiEndpoint := fmt.Sprintf("/rest/api/2/user?username=%s", username) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - - user := new(User) - resp, err := s.client.Do(req, user) - if err != nil { - return nil, resp, NewJiraError(resp, err) - } - return user, resp, nil -} - -// Create creates an user in JIRA. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-createUser -func (s *UserService) Create(user *User) (*User, *Response, error) { - apiEndpoint := "/rest/api/2/user" - req, err := s.client.NewRequest("POST", apiEndpoint, user) - if err != nil { - return nil, nil, err - } - - resp, err := s.client.Do(req, nil) - if err != nil { - return nil, resp, err - } - - responseUser := new(User) - defer resp.Body.Close() - data, err := ioutil.ReadAll(resp.Body) - if err != nil { - e := fmt.Errorf("Could not read the returned data") - return nil, resp, NewJiraError(resp, e) - } - err = json.Unmarshal(data, responseUser) - if err != nil { - e := fmt.Errorf("Could not unmarshall the data into struct") - return nil, resp, NewJiraError(resp, e) - } - return responseUser, resp, nil -} - -// GetGroups returns the groups which the user belongs to -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-getUserGroups -func (s *UserService) GetGroups(username string) (*[]UserGroup, *Response, error) { - apiEndpoint := fmt.Sprintf("/rest/api/2/user/groups?username=%s", username) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - - userGroups := new([]UserGroup) - resp, err := s.client.Do(req, userGroups) - if err != nil { - return nil, resp, NewJiraError(resp, err) - } - return userGroups, resp, nil -} - -// Get information about the current logged-in user -// -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-myself-get -func (s *UserService) GetSelf() (*User, *Response, error) { - const apiEndpoint = "rest/api/2/myself" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - var user User - resp, err := s.client.Do(req, &user) - if err != nil { - return nil, resp, NewJiraError(resp, err) - } - return &user, resp, nil -} - -// Find searches for user info from JIRA: -// It can find users by email, username or name -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-findUsers -func (s *UserService) Find(property string) ([]User, *Response, error) { - apiEndpoint := fmt.Sprintf("/rest/api/2/user/search?username=%s", property) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - - users := []User{} - resp, err := s.client.Do(req, &users) - if err != nil { - return nil, resp, NewJiraError(resp, err) - } - return users, resp, nil -} diff --git a/vendor/github.com/andygrunwald/go-jira/version.go b/vendor/github.com/andygrunwald/go-jira/version.go deleted file mode 100644 index 152005e9..00000000 --- a/vendor/github.com/andygrunwald/go-jira/version.go +++ /dev/null @@ -1,96 +0,0 @@ -package jira - -import ( - "encoding/json" - "fmt" - "io/ioutil" -) - -// VersionService handles Versions for the JIRA instance / API. -// -// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/version -type VersionService struct { - client *Client -} - -// Version represents a single release version of a project -type Version struct { - Self string `json:"self,omitempty" structs:"self,omitempty"` - ID string `json:"id,omitempty" structs:"id,omitempty"` - Name string `json:"name,omitempty" structs:"name,omitempty"` - Description string `json:"description,omitempty" structs:"name,omitempty"` - Archived bool `json:"archived,omitempty" structs:"archived,omitempty"` - Released bool `json:"released,omitempty" structs:"released,omitempty"` - ReleaseDate string `json:"releaseDate,omitempty" structs:"releaseDate,omitempty"` - UserReleaseDate string `json:"userReleaseDate,omitempty" structs:"userReleaseDate,omitempty"` - ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"` // Unlike other IDs, this is returned as a number -} - -// Get gets version info from JIRA -// -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-id-get -func (s *VersionService) Get(versionID int) (*Version, *Response, error) { - apiEndpoint := fmt.Sprintf("/rest/api/2/version/%v", versionID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) - if err != nil { - return nil, nil, err - } - - version := new(Version) - resp, err := s.client.Do(req, version) - if err != nil { - return nil, resp, NewJiraError(resp, err) - } - return version, resp, nil -} - -// Create creates a version in JIRA. -// -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-post -func (s *VersionService) Create(version *Version) (*Version, *Response, error) { - apiEndpoint := "/rest/api/2/version" - req, err := s.client.NewRequest("POST", apiEndpoint, version) - if err != nil { - return nil, nil, err - } - - resp, err := s.client.Do(req, nil) - if err != nil { - return nil, resp, err - } - - responseVersion := new(Version) - defer resp.Body.Close() - data, err := ioutil.ReadAll(resp.Body) - if err != nil { - e := fmt.Errorf("Could not read the returned data") - return nil, resp, NewJiraError(resp, e) - } - err = json.Unmarshal(data, responseVersion) - if err != nil { - e := fmt.Errorf("Could not unmarshall the data into struct") - return nil, resp, NewJiraError(resp, e) - } - return responseVersion, resp, nil -} - -// Update updates a version from a JSON representation. -// -// JIRA API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-id-put -func (s *VersionService) Update(version *Version) (*Version, *Response, error) { - apiEndpoint := fmt.Sprintf("rest/api/2/version/%v", version.ID) - req, err := s.client.NewRequest("PUT", apiEndpoint, version) - if err != nil { - return nil, nil, err - } - resp, err := s.client.Do(req, nil) - if err != nil { - jerr := NewJiraError(resp, err) - return nil, resp, jerr - } - - // This is just to follow the rest of the API's convention of returning a version. - // Returning the same pointer here is pointless, so we return a copy instead. - ret := *version - return &ret, resp, nil -} diff --git a/vendor/gopkg.in/asn1-ber.v1/.travis.yml b/vendor/gopkg.in/asn1-ber.v1/.travis.yml new file mode 100644 index 00000000..53063d07 --- /dev/null +++ b/vendor/gopkg.in/asn1-ber.v1/.travis.yml @@ -0,0 +1,18 @@ +language: go +go: + - 1.2 + - 1.3 + - 1.4 + - 1.5 + - 1.6 + - 1.7 + - 1.8 + - tip +go_import_path: gopkg.in/asn-ber.v1 +install: + - go list -f '{{range .Imports}}{{.}} {{end}}' ./... | xargs go get -v + - go list -f '{{range .TestImports}}{{.}} {{end}}' ./... | xargs go get -v + - go get code.google.com/p/go.tools/cmd/cover || go get golang.org/x/tools/cmd/cover + - go build -v ./... +script: + - go test -v -cover ./... diff --git a/vendor/gopkg.in/asn1-ber.v1/LICENSE b/vendor/gopkg.in/asn1-ber.v1/LICENSE new file mode 100644 index 00000000..23f94253 --- /dev/null +++ b/vendor/gopkg.in/asn1-ber.v1/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com) +Portions copyright (c) 2015-2016 go-asn1-ber Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/gopkg.in/asn1-ber.v1/README.md b/vendor/gopkg.in/asn1-ber.v1/README.md new file mode 100644 index 00000000..e3a9560d --- /dev/null +++ b/vendor/gopkg.in/asn1-ber.v1/README.md @@ -0,0 +1,24 @@ +[![GoDoc](https://godoc.org/gopkg.in/asn1-ber.v1?status.svg)](https://godoc.org/gopkg.in/asn1-ber.v1) [![Build Status](https://travis-ci.org/go-asn1-ber/asn1-ber.svg)](https://travis-ci.org/go-asn1-ber/asn1-ber) + + +ASN1 BER Encoding / Decoding Library for the GO programming language. +--------------------------------------------------------------------- + +Required libraries: + None + +Working: + Very basic encoding / decoding needed for LDAP protocol + +Tests Implemented: + A few + +TODO: + Fix all encoding / decoding to conform to ASN1 BER spec + Implement Tests / Benchmarks + +--- + +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 diff --git a/vendor/gopkg.in/asn1-ber.v1/ber.go b/vendor/gopkg.in/asn1-ber.v1/ber.go new file mode 100644 index 00000000..25cc921b --- /dev/null +++ b/vendor/gopkg.in/asn1-ber.v1/ber.go @@ -0,0 +1,504 @@ +package ber + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "reflect" +) + +type Packet struct { + Identifier + Value interface{} + ByteValue []byte + Data *bytes.Buffer + Children []*Packet + Description string +} + +type Identifier struct { + ClassType Class + TagType Type + Tag Tag +} + +type Tag uint64 + +const ( + TagEOC Tag = 0x00 + TagBoolean Tag = 0x01 + TagInteger Tag = 0x02 + TagBitString Tag = 0x03 + TagOctetString Tag = 0x04 + TagNULL Tag = 0x05 + TagObjectIdentifier Tag = 0x06 + TagObjectDescriptor Tag = 0x07 + TagExternal Tag = 0x08 + TagRealFloat Tag = 0x09 + TagEnumerated Tag = 0x0a + TagEmbeddedPDV Tag = 0x0b + TagUTF8String Tag = 0x0c + TagRelativeOID Tag = 0x0d + TagSequence Tag = 0x10 + TagSet Tag = 0x11 + TagNumericString Tag = 0x12 + TagPrintableString Tag = 0x13 + TagT61String Tag = 0x14 + TagVideotexString Tag = 0x15 + TagIA5String Tag = 0x16 + TagUTCTime Tag = 0x17 + TagGeneralizedTime Tag = 0x18 + TagGraphicString Tag = 0x19 + TagVisibleString Tag = 0x1a + TagGeneralString Tag = 0x1b + TagUniversalString Tag = 0x1c + TagCharacterString Tag = 0x1d + TagBMPString Tag = 0x1e + TagBitmask Tag = 0x1f // xxx11111b + + // HighTag indicates the start of a high-tag byte sequence + HighTag Tag = 0x1f // xxx11111b + // HighTagContinueBitmask indicates the high-tag byte sequence should continue + HighTagContinueBitmask Tag = 0x80 // 10000000b + // HighTagValueBitmask obtains the tag value from a high-tag byte sequence byte + HighTagValueBitmask Tag = 0x7f // 01111111b +) + +const ( + // LengthLongFormBitmask is the mask to apply to the length byte to see if a long-form byte sequence is used + LengthLongFormBitmask = 0x80 + // LengthValueBitmask is the mask to apply to the length byte to get the number of bytes in the long-form byte sequence + LengthValueBitmask = 0x7f + + // LengthIndefinite is returned from readLength to indicate an indefinite length + LengthIndefinite = -1 +) + +var tagMap = map[Tag]string{ + TagEOC: "EOC (End-of-Content)", + TagBoolean: "Boolean", + TagInteger: "Integer", + TagBitString: "Bit String", + TagOctetString: "Octet String", + TagNULL: "NULL", + TagObjectIdentifier: "Object Identifier", + TagObjectDescriptor: "Object Descriptor", + TagExternal: "External", + TagRealFloat: "Real (float)", + TagEnumerated: "Enumerated", + TagEmbeddedPDV: "Embedded PDV", + TagUTF8String: "UTF8 String", + TagRelativeOID: "Relative-OID", + TagSequence: "Sequence and Sequence of", + TagSet: "Set and Set OF", + TagNumericString: "Numeric String", + TagPrintableString: "Printable String", + TagT61String: "T61 String", + TagVideotexString: "Videotex String", + TagIA5String: "IA5 String", + TagUTCTime: "UTC Time", + TagGeneralizedTime: "Generalized Time", + TagGraphicString: "Graphic String", + TagVisibleString: "Visible String", + TagGeneralString: "General String", + TagUniversalString: "Universal String", + TagCharacterString: "Character String", + TagBMPString: "BMP String", +} + +type Class uint8 + +const ( + ClassUniversal Class = 0 // 00xxxxxxb + ClassApplication Class = 64 // 01xxxxxxb + ClassContext Class = 128 // 10xxxxxxb + ClassPrivate Class = 192 // 11xxxxxxb + ClassBitmask Class = 192 // 11xxxxxxb +) + +var ClassMap = map[Class]string{ + ClassUniversal: "Universal", + ClassApplication: "Application", + ClassContext: "Context", + ClassPrivate: "Private", +} + +type Type uint8 + +const ( + TypePrimitive Type = 0 // xx0xxxxxb + TypeConstructed Type = 32 // xx1xxxxxb + TypeBitmask Type = 32 // xx1xxxxxb +) + +var TypeMap = map[Type]string{ + TypePrimitive: "Primitive", + TypeConstructed: "Constructed", +} + +var Debug bool = false + +func PrintBytes(out io.Writer, buf []byte, indent string) { + data_lines := make([]string, (len(buf)/30)+1) + num_lines := make([]string, (len(buf)/30)+1) + + for i, b := range buf { + data_lines[i/30] += fmt.Sprintf("%02x ", b) + num_lines[i/30] += fmt.Sprintf("%02d ", (i+1)%100) + } + + for i := 0; i < len(data_lines); i++ { + out.Write([]byte(indent + data_lines[i] + "\n")) + out.Write([]byte(indent + num_lines[i] + "\n\n")) + } +} + +func PrintPacket(p *Packet) { + printPacket(os.Stdout, p, 0, false) +} + +func printPacket(out io.Writer, p *Packet, indent int, printBytes bool) { + indent_str := "" + + for len(indent_str) != indent { + indent_str += " " + } + + class_str := ClassMap[p.ClassType] + + tagtype_str := TypeMap[p.TagType] + + tag_str := fmt.Sprintf("0x%02X", p.Tag) + + if p.ClassType == ClassUniversal { + tag_str = tagMap[p.Tag] + } + + value := fmt.Sprint(p.Value) + description := "" + + if p.Description != "" { + description = p.Description + ": " + } + + fmt.Fprintf(out, "%s%s(%s, %s, %s) Len=%d %q\n", indent_str, description, class_str, tagtype_str, tag_str, p.Data.Len(), value) + + if printBytes { + PrintBytes(out, p.Bytes(), indent_str) + } + + for _, child := range p.Children { + printPacket(out, child, indent+1, printBytes) + } +} + +// ReadPacket reads a single Packet from the reader +func ReadPacket(reader io.Reader) (*Packet, error) { + p, _, err := readPacket(reader) + if err != nil { + return nil, err + } + return p, nil +} + +func DecodeString(data []byte) string { + return string(data) +} + +func parseInt64(bytes []byte) (ret int64, err error) { + if len(bytes) > 8 { + // We'll overflow an int64 in this case. + err = fmt.Errorf("integer too large") + return + } + for bytesRead := 0; bytesRead < len(bytes); bytesRead++ { + ret <<= 8 + ret |= int64(bytes[bytesRead]) + } + + // Shift up and down in order to sign extend the result. + ret <<= 64 - uint8(len(bytes))*8 + ret >>= 64 - uint8(len(bytes))*8 + return +} + +func encodeInteger(i int64) []byte { + n := int64Length(i) + out := make([]byte, n) + + var j int + for ; n > 0; n-- { + out[j] = (byte(i >> uint((n-1)*8))) + j++ + } + + return out +} + +func int64Length(i int64) (numBytes int) { + numBytes = 1 + + for i > 127 { + numBytes++ + i >>= 8 + } + + for i < -128 { + numBytes++ + i >>= 8 + } + + return +} + +// DecodePacket decodes the given bytes into a single Packet +// If a decode error is encountered, nil is returned. +func DecodePacket(data []byte) *Packet { + p, _, _ := readPacket(bytes.NewBuffer(data)) + + return p +} + +// DecodePacketErr decodes the given bytes into a single Packet +// If a decode error is encountered, nil is returned +func DecodePacketErr(data []byte) (*Packet, error) { + p, _, err := readPacket(bytes.NewBuffer(data)) + if err != nil { + return nil, err + } + return p, nil +} + +// readPacket reads a single Packet from the reader, returning the number of bytes read +func readPacket(reader io.Reader) (*Packet, int, error) { + identifier, length, read, err := readHeader(reader) + if err != nil { + return nil, read, err + } + + p := &Packet{ + Identifier: identifier, + } + + p.Data = new(bytes.Buffer) + p.Children = make([]*Packet, 0, 2) + p.Value = nil + + if p.TagType == TypeConstructed { + // TODO: if universal, ensure tag type is allowed to be constructed + + // Track how much content we've read + contentRead := 0 + for { + if length != LengthIndefinite { + // End if we've read what we've been told to + if contentRead == length { + break + } + // Detect if a packet boundary didn't fall on the expected length + if contentRead > length { + return nil, read, fmt.Errorf("expected to read %d bytes, read %d", length, contentRead) + } + } + + // Read the next packet + child, r, err := readPacket(reader) + if err != nil { + return nil, read, err + } + contentRead += r + read += r + + // Test is this is the EOC marker for our packet + if isEOCPacket(child) { + if length == LengthIndefinite { + break + } + return nil, read, errors.New("eoc child not allowed with definite length") + } + + // Append and continue + p.AppendChild(child) + } + return p, read, nil + } + + if length == LengthIndefinite { + return nil, read, errors.New("indefinite length used with primitive type") + } + + // Read definite-length content + content := make([]byte, length, length) + if length > 0 { + _, err := io.ReadFull(reader, content) + if err != nil { + if err == io.EOF { + return nil, read, io.ErrUnexpectedEOF + } + return nil, read, err + } + read += length + } + + if p.ClassType == ClassUniversal { + p.Data.Write(content) + p.ByteValue = content + + switch p.Tag { + case TagEOC: + case TagBoolean: + val, _ := parseInt64(content) + + p.Value = val != 0 + case TagInteger: + p.Value, _ = parseInt64(content) + case TagBitString: + case TagOctetString: + // the actual string encoding is not known here + // (e.g. for LDAP content is already an UTF8-encoded + // string). Return the data without further processing + p.Value = DecodeString(content) + case TagNULL: + case TagObjectIdentifier: + case TagObjectDescriptor: + case TagExternal: + case TagRealFloat: + case TagEnumerated: + p.Value, _ = parseInt64(content) + case TagEmbeddedPDV: + case TagUTF8String: + p.Value = DecodeString(content) + case TagRelativeOID: + case TagSequence: + case TagSet: + case TagNumericString: + case TagPrintableString: + p.Value = DecodeString(content) + case TagT61String: + case TagVideotexString: + case TagIA5String: + case TagUTCTime: + case TagGeneralizedTime: + case TagGraphicString: + case TagVisibleString: + case TagGeneralString: + case TagUniversalString: + case TagCharacterString: + case TagBMPString: + } + } else { + p.Data.Write(content) + } + + return p, read, nil +} + +func (p *Packet) Bytes() []byte { + var out bytes.Buffer + + out.Write(encodeIdentifier(p.Identifier)) + out.Write(encodeLength(p.Data.Len())) + out.Write(p.Data.Bytes()) + + return out.Bytes() +} + +func (p *Packet) AppendChild(child *Packet) { + p.Data.Write(child.Bytes()) + p.Children = append(p.Children, child) +} + +func Encode(ClassType Class, TagType Type, Tag Tag, Value interface{}, Description string) *Packet { + p := new(Packet) + + p.ClassType = ClassType + p.TagType = TagType + p.Tag = Tag + p.Data = new(bytes.Buffer) + + p.Children = make([]*Packet, 0, 2) + + p.Value = Value + p.Description = Description + + if Value != nil { + v := reflect.ValueOf(Value) + + if ClassType == ClassUniversal { + switch Tag { + case TagOctetString: + sv, ok := v.Interface().(string) + + if ok { + p.Data.Write([]byte(sv)) + } + } + } + } + + return p +} + +func NewSequence(Description string) *Packet { + return Encode(ClassUniversal, TypeConstructed, TagSequence, nil, Description) +} + +func NewBoolean(ClassType Class, TagType Type, Tag Tag, Value bool, Description string) *Packet { + intValue := int64(0) + + if Value { + intValue = 1 + } + + p := Encode(ClassType, TagType, Tag, nil, Description) + + p.Value = Value + p.Data.Write(encodeInteger(intValue)) + + return p +} + +func NewInteger(ClassType Class, TagType Type, Tag Tag, Value interface{}, Description string) *Packet { + p := Encode(ClassType, TagType, Tag, nil, Description) + + p.Value = Value + switch v := Value.(type) { + case int: + p.Data.Write(encodeInteger(int64(v))) + case uint: + p.Data.Write(encodeInteger(int64(v))) + case int64: + p.Data.Write(encodeInteger(v)) + case uint64: + // TODO : check range or add encodeUInt... + p.Data.Write(encodeInteger(int64(v))) + case int32: + p.Data.Write(encodeInteger(int64(v))) + case uint32: + p.Data.Write(encodeInteger(int64(v))) + case int16: + p.Data.Write(encodeInteger(int64(v))) + case uint16: + p.Data.Write(encodeInteger(int64(v))) + case int8: + p.Data.Write(encodeInteger(int64(v))) + case uint8: + p.Data.Write(encodeInteger(int64(v))) + default: + // TODO : add support for big.Int ? + panic(fmt.Sprintf("Invalid type %T, expected {u|}int{64|32|16|8}", v)) + } + + return p +} + +func NewString(ClassType Class, TagType Type, Tag Tag, Value, Description string) *Packet { + p := Encode(ClassType, TagType, Tag, nil, Description) + + p.Value = Value + p.Data.Write([]byte(Value)) + + return p +} diff --git a/vendor/gopkg.in/asn1-ber.v1/content_int.go b/vendor/gopkg.in/asn1-ber.v1/content_int.go new file mode 100644 index 00000000..1858b74b --- /dev/null +++ b/vendor/gopkg.in/asn1-ber.v1/content_int.go @@ -0,0 +1,25 @@ +package ber + +func encodeUnsignedInteger(i uint64) []byte { + n := uint64Length(i) + out := make([]byte, n) + + var j int + for ; n > 0; n-- { + out[j] = (byte(i >> uint((n-1)*8))) + j++ + } + + return out +} + +func uint64Length(i uint64) (numBytes int) { + numBytes = 1 + + for i > 255 { + numBytes++ + i >>= 8 + } + + return +} diff --git a/vendor/gopkg.in/asn1-ber.v1/header.go b/vendor/gopkg.in/asn1-ber.v1/header.go new file mode 100644 index 00000000..123744e9 --- /dev/null +++ b/vendor/gopkg.in/asn1-ber.v1/header.go @@ -0,0 +1,29 @@ +package ber + +import ( + "errors" + "io" +) + +func readHeader(reader io.Reader) (identifier Identifier, length int, read int, err error) { + if i, c, err := readIdentifier(reader); err != nil { + return Identifier{}, 0, read, err + } else { + identifier = i + read += c + } + + if l, c, err := readLength(reader); err != nil { + return Identifier{}, 0, read, err + } else { + length = l + read += c + } + + // Validate length type with identifier (x.600, 8.1.3.2.a) + if length == LengthIndefinite && identifier.TagType == TypePrimitive { + return Identifier{}, 0, read, errors.New("indefinite length used with primitive type") + } + + return identifier, length, read, nil +} diff --git a/vendor/gopkg.in/asn1-ber.v1/identifier.go b/vendor/gopkg.in/asn1-ber.v1/identifier.go new file mode 100644 index 00000000..f7672a84 --- /dev/null +++ b/vendor/gopkg.in/asn1-ber.v1/identifier.go @@ -0,0 +1,103 @@ +package ber + +import ( + "errors" + "fmt" + "io" + "math" +) + +func readIdentifier(reader io.Reader) (Identifier, int, error) { + identifier := Identifier{} + read := 0 + + // identifier byte + b, err := readByte(reader) + if err != nil { + if Debug { + fmt.Printf("error reading identifier byte: %v\n", err) + } + return Identifier{}, read, err + } + read++ + + identifier.ClassType = Class(b) & ClassBitmask + identifier.TagType = Type(b) & TypeBitmask + + if tag := Tag(b) & TagBitmask; tag != HighTag { + // short-form tag + identifier.Tag = tag + return identifier, read, nil + } + + // high-tag-number tag + tagBytes := 0 + for { + b, err := readByte(reader) + if err != nil { + if Debug { + fmt.Printf("error reading high-tag-number tag byte %d: %v\n", tagBytes, err) + } + return Identifier{}, read, err + } + tagBytes++ + read++ + + // Lowest 7 bits get appended to the tag value (x.690, 8.1.2.4.2.b) + identifier.Tag <<= 7 + identifier.Tag |= Tag(b) & HighTagValueBitmask + + // First byte may not be all zeros (x.690, 8.1.2.4.2.c) + if tagBytes == 1 && identifier.Tag == 0 { + return Identifier{}, read, errors.New("invalid first high-tag-number tag byte") + } + // Overflow of int64 + // TODO: support big int tags? + if tagBytes > 9 { + return Identifier{}, read, errors.New("high-tag-number tag overflow") + } + + // Top bit of 0 means this is the last byte in the high-tag-number tag (x.690, 8.1.2.4.2.a) + if Tag(b)&HighTagContinueBitmask == 0 { + break + } + } + + return identifier, read, nil +} + +func encodeIdentifier(identifier Identifier) []byte { + b := []byte{0x0} + b[0] |= byte(identifier.ClassType) + b[0] |= byte(identifier.TagType) + + if identifier.Tag < HighTag { + // Short-form + b[0] |= byte(identifier.Tag) + } else { + // high-tag-number + b[0] |= byte(HighTag) + + tag := identifier.Tag + + highBit := uint(63) + for { + if tag&(1<= 0; i-- { + offset := uint(i) * 7 + mask := Tag(0x7f) << offset + tagByte := (tag & mask) >> offset + if i != 0 { + tagByte |= 0x80 + } + b = append(b, byte(tagByte)) + } + } + return b +} diff --git a/vendor/gopkg.in/asn1-ber.v1/length.go b/vendor/gopkg.in/asn1-ber.v1/length.go new file mode 100644 index 00000000..750e8f44 --- /dev/null +++ b/vendor/gopkg.in/asn1-ber.v1/length.go @@ -0,0 +1,81 @@ +package ber + +import ( + "errors" + "fmt" + "io" +) + +func readLength(reader io.Reader) (length int, read int, err error) { + // length byte + b, err := readByte(reader) + if err != nil { + if Debug { + fmt.Printf("error reading length byte: %v\n", err) + } + return 0, 0, err + } + read++ + + switch { + case b == 0xFF: + // Invalid 0xFF (x.600, 8.1.3.5.c) + return 0, read, errors.New("invalid length byte 0xff") + + case b == LengthLongFormBitmask: + // Indefinite form, we have to decode packets until we encounter an EOC packet (x.600, 8.1.3.6) + length = LengthIndefinite + + case b&LengthLongFormBitmask == 0: + // Short definite form, extract the length from the bottom 7 bits (x.600, 8.1.3.4) + length = int(b) & LengthValueBitmask + + case b&LengthLongFormBitmask != 0: + // Long definite form, extract the number of length bytes to follow from the bottom 7 bits (x.600, 8.1.3.5.b) + lengthBytes := int(b) & LengthValueBitmask + // Protect against overflow + // TODO: support big int length? + if lengthBytes > 8 { + return 0, read, errors.New("long-form length overflow") + } + + // Accumulate into a 64-bit variable + var length64 int64 + for i := 0; i < lengthBytes; i++ { + b, err = readByte(reader) + if err != nil { + if Debug { + fmt.Printf("error reading long-form length byte %d: %v\n", i, err) + } + return 0, read, err + } + read++ + + // x.600, 8.1.3.5 + length64 <<= 8 + length64 |= int64(b) + } + + // Cast to a platform-specific integer + length = int(length64) + // Ensure we didn't overflow + if int64(length) != length64 { + return 0, read, errors.New("long-form length overflow") + } + + default: + return 0, read, errors.New("invalid length byte") + } + + return length, read, nil +} + +func encodeLength(length int) []byte { + length_bytes := encodeUnsignedInteger(uint64(length)) + if length > 127 || len(length_bytes) > 1 { + longFormBytes := []byte{(LengthLongFormBitmask | byte(len(length_bytes)))} + longFormBytes = append(longFormBytes, length_bytes...) + length_bytes = longFormBytes + } + return length_bytes +} diff --git a/vendor/gopkg.in/asn1-ber.v1/util.go b/vendor/gopkg.in/asn1-ber.v1/util.go new file mode 100644 index 00000000..3e56b66c --- /dev/null +++ b/vendor/gopkg.in/asn1-ber.v1/util.go @@ -0,0 +1,24 @@ +package ber + +import "io" + +func readByte(reader io.Reader) (byte, error) { + bytes := make([]byte, 1, 1) + _, err := io.ReadFull(reader, bytes) + if err != nil { + if err == io.EOF { + return 0, io.ErrUnexpectedEOF + } + return 0, err + } + return bytes[0], nil +} + +func isEOCPacket(p *Packet) bool { + return p != nil && + p.Tag == TagEOC && + p.ClassType == ClassUniversal && + p.TagType == TypePrimitive && + len(p.ByteValue) == 0 && + len(p.Children) == 0 +} diff --git a/vendor/gopkg.in/ldap.v2/.gitignore b/vendor/gopkg.in/ldap.v2/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/vendor/gopkg.in/ldap.v2/.travis.yml b/vendor/gopkg.in/ldap.v2/.travis.yml new file mode 100644 index 00000000..9782c9ba --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/.travis.yml @@ -0,0 +1,31 @@ +language: go +env: + global: + - VET_VERSIONS="1.6 1.7 1.8 1.9 tip" + - LINT_VERSIONS="1.6 1.7 1.8 1.9 tip" +go: + - 1.2 + - 1.3 + - 1.4 + - 1.5 + - 1.6 + - 1.7 + - 1.8 + - 1.9 + - tip +matrix: + fast_finish: true + allow_failures: + - go: tip +go_import_path: gopkg.in/ldap.v2 +install: + - go get gopkg.in/asn1-ber.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 get github.com/golang/lint/golint || true + - go build -v ./... +script: + - make test + - make fmt + - if [[ "$VET_VERSIONS" == *"$TRAVIS_GO_VERSION"* ]]; then make vet; fi + - if [[ "$LINT_VERSIONS" == *"$TRAVIS_GO_VERSION"* ]]; then make lint; fi diff --git a/vendor/github.com/andygrunwald/go-jira/LICENSE b/vendor/gopkg.in/ldap.v2/LICENSE similarity index 90% rename from vendor/github.com/andygrunwald/go-jira/LICENSE rename to vendor/gopkg.in/ldap.v2/LICENSE index 692f6bea..6c0ed4b3 100644 --- a/vendor/github.com/andygrunwald/go-jira/LICENSE +++ b/vendor/gopkg.in/ldap.v2/LICENSE @@ -1,6 +1,7 @@ The MIT License (MIT) -Copyright (c) 2015 Andy Grunwald +Copyright (c) 2011-2015 Michael Mitton (mmitton@gmail.com) +Portions copyright (c) 2015-2016 go-ldap Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,4 +20,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/vendor/gopkg.in/ldap.v2/Makefile b/vendor/gopkg.in/ldap.v2/Makefile new file mode 100644 index 00000000..a9d351c7 --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/Makefile @@ -0,0 +1,52 @@ +.PHONY: default install build test quicktest fmt vet lint + +GO_VERSION := $(shell go version | cut -d' ' -f3 | cut -d. -f2) + +# Only use the `-race` flag on newer versions of Go +IS_OLD_GO := $(shell test $(GO_VERSION) -le 2 && echo true) +ifeq ($(IS_OLD_GO),true) + RACE_FLAG := +else + RACE_FLAG := -race -cpu 1,2,4 +endif + +default: fmt vet lint build quicktest + +install: + go get -t -v ./... + +build: + go build -v ./... + +test: + go test -v $(RACE_FLAG) -cover ./... + +quicktest: + go test ./... + +# Capture output and force failure when there is non-empty output +fmt: + @echo gofmt -l . + @OUTPUT=`gofmt -l . 2>&1`; \ + if [ "$$OUTPUT" ]; then \ + echo "gofmt must be run on the following files:"; \ + echo "$$OUTPUT"; \ + exit 1; \ + fi + +# Only run on go1.5+ +vet: + go tool vet -atomic -bool -copylocks -nilfunc -printf -shadow -rangeloops -unreachable -unsafeptr -unusedresult . + +# https://github.com/golang/lint +# go get github.com/golang/lint/golint +# Capture output and force failure when there is non-empty output +# Only run on go1.5+ +lint: + @echo golint ./... + @OUTPUT=`golint ./... 2>&1`; \ + if [ "$$OUTPUT" ]; then \ + echo "golint errors:"; \ + echo "$$OUTPUT"; \ + exit 1; \ + fi diff --git a/vendor/gopkg.in/ldap.v2/README.md b/vendor/gopkg.in/ldap.v2/README.md new file mode 100644 index 00000000..a26ed2d8 --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/README.md @@ -0,0 +1,53 @@ +[![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. + +## 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 + +## Features: + + - Connecting to LDAP server (non-TLS, TLS, STARTTLS) + - Binding to LDAP server + - Searching for entries + - Filter Compile / Decompile + - Paging Search Results + - Modify Requests / Responses + - Add Requests / Responses + - Delete Requests / Responses + +## Examples: + + - search + - modify + +## Contributing: + +Bug reports and pull requests are welcome! + +Before submitting a pull request, please make sure tests and verification scripts pass: +``` +make all +``` + +To set up a pre-push hook to run the tests and verify scripts before pushing: +``` +ln -s ../../.githooks/pre-push .git/hooks/pre-push +``` + +--- +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 diff --git a/vendor/gopkg.in/ldap.v2/add.go b/vendor/gopkg.in/ldap.v2/add.go new file mode 100644 index 00000000..0e5f6cdb --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/add.go @@ -0,0 +1,113 @@ +// +// 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" +) + +// Attribute represents an LDAP attribute +type Attribute struct { + // Type is the name of the LDAP attribute + Type string + // Vals are the LDAP attribute values + Vals []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.Type, "Type")) + set := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "AttributeValue") + for _, value := range a.Vals { + set.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Vals")) + } + seq.AppendChild(set) + return seq +} + +// AddRequest represents an LDAP AddRequest operation +type AddRequest struct { + // DN identifies the entry being added + DN string + // Attributes list the attributes of the new entry + 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 +} + +// Attribute adds an attribute with the given type and values +func (a *AddRequest) Attribute(attrType string, attrVals []string) { + a.Attributes = append(a.Attributes, Attribute{Type: attrType, Vals: attrVals}) +} + +// NewAddRequest returns an AddRequest for the given DN, with no attributes +func NewAddRequest(dn string) *AddRequest { + return &AddRequest{ + DN: dn, + } + +} + +// Add performs the given AddRequest +func (l *Conn) Add(addRequest *AddRequest) error { + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) + packet.AppendChild(addRequest.encode()) + + l.Debug.PrintPacket(packet) + + msgCtx, err := l.sendMessage(packet) + if err != nil { + return err + } + defer l.finishMessage(msgCtx) + + l.Debug.Printf("%d: waiting for response", msgCtx.id) + packetResponse, ok := <-msgCtx.responses + if !ok { + return NewError(ErrorNetwork, errors.New("ldap: response channel closed")) + } + packet, err = packetResponse.ReadPacket() + l.Debug.Printf("%d: got response %p", msgCtx.id, packet) + if err != nil { + return err + } + + 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", msgCtx.id) + return nil +} diff --git a/vendor/gopkg.in/ldap.v2/atomic_value.go b/vendor/gopkg.in/ldap.v2/atomic_value.go new file mode 100644 index 00000000..bccf7573 --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/atomic_value.go @@ -0,0 +1,13 @@ +// +build go1.4 + +package ldap + +import ( + "sync/atomic" +) + +// For compilers that support it, we just use the underlying sync/atomic.Value +// type. +type atomicValue struct { + atomic.Value +} diff --git a/vendor/gopkg.in/ldap.v2/atomic_value_go13.go b/vendor/gopkg.in/ldap.v2/atomic_value_go13.go new file mode 100644 index 00000000..04920bb2 --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/atomic_value_go13.go @@ -0,0 +1,28 @@ +// +build !go1.4 + +package ldap + +import ( + "sync" +) + +// This is a helper type that emulates the use of the "sync/atomic.Value" +// struct that's available in Go 1.4 and up. +type atomicValue struct { + value interface{} + lock sync.RWMutex +} + +func (av *atomicValue) Store(val interface{}) { + av.lock.Lock() + av.value = val + av.lock.Unlock() +} + +func (av *atomicValue) Load() interface{} { + av.lock.RLock() + ret := av.value + av.lock.RUnlock() + + return ret +} diff --git a/vendor/gopkg.in/ldap.v2/bind.go b/vendor/gopkg.in/ldap.v2/bind.go new file mode 100644 index 00000000..26b3cc72 --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/bind.go @@ -0,0 +1,143 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ldap + +import ( + "errors" + + "gopkg.in/asn1-ber.v1" +) + +// SimpleBindRequest represents a username/password bind operation +type SimpleBindRequest struct { + // Username is the name of the Directory object that the client wishes to bind as + Username string + // Password is the credentials to bind with + Password string + // Controls are optional controls to send with the bind request + Controls []Control +} + +// SimpleBindResult contains the response from the server +type SimpleBindResult struct { + Controls []Control +} + +// NewSimpleBindRequest returns a bind request +func NewSimpleBindRequest(username string, password string, controls []Control) *SimpleBindRequest { + return &SimpleBindRequest{ + Username: username, + Password: password, + Controls: controls, + } +} + +func (bindRequest *SimpleBindRequest) encode() *ber.Packet { + request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") + request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) + request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, bindRequest.Username, "User Name")) + request.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, bindRequest.Password, "Password")) + + request.AppendChild(encodeControls(bindRequest.Controls)) + + return request +} + +// SimpleBind performs the simple bind operation defined in the given request +func (l *Conn) SimpleBind(simpleBindRequest *SimpleBindRequest) (*SimpleBindResult, error) { + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) + encodedBindRequest := simpleBindRequest.encode() + packet.AppendChild(encodedBindRequest) + + if l.Debug { + ber.PrintPacket(packet) + } + + msgCtx, err := l.sendMessage(packet) + if err != nil { + return nil, err + } + defer l.finishMessage(msgCtx) + + packetResponse, ok := <-msgCtx.responses + if !ok { + return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) + } + packet, err = packetResponse.ReadPacket() + l.Debug.Printf("%d: got response %p", msgCtx.id, packet) + if err != nil { + return nil, err + } + + if l.Debug { + if err := addLDAPDescriptions(packet); err != nil { + return nil, err + } + ber.PrintPacket(packet) + } + + result := &SimpleBindResult{ + Controls: make([]Control, 0), + } + + if len(packet.Children) == 3 { + for _, child := range packet.Children[2].Children { + result.Controls = append(result.Controls, DecodeControl(child)) + } + } + + resultCode, resultDescription := getLDAPResultCode(packet) + if resultCode != 0 { + return result, NewError(resultCode, errors.New(resultDescription)) + } + + return result, nil +} + +// Bind performs a bind with the given username and password +func (l *Conn) Bind(username, password string) error { + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) + bindRequest := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationBindRequest, nil, "Bind Request") + bindRequest.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 3, "Version")) + bindRequest.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, username, "User Name")) + bindRequest.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, password, "Password")) + packet.AppendChild(bindRequest) + + if l.Debug { + ber.PrintPacket(packet) + } + + msgCtx, err := l.sendMessage(packet) + if err != nil { + return err + } + defer l.finishMessage(msgCtx) + + packetResponse, ok := <-msgCtx.responses + if !ok { + return NewError(ErrorNetwork, errors.New("ldap: response channel closed")) + } + packet, err = packetResponse.ReadPacket() + l.Debug.Printf("%d: got response %p", msgCtx.id, packet) + if err != nil { + return err + } + + if l.Debug { + if err := addLDAPDescriptions(packet); err != nil { + return err + } + ber.PrintPacket(packet) + } + + resultCode, resultDescription := getLDAPResultCode(packet) + if resultCode != 0 { + return NewError(resultCode, errors.New(resultDescription)) + } + + return nil +} diff --git a/vendor/gopkg.in/ldap.v2/client.go b/vendor/gopkg.in/ldap.v2/client.go new file mode 100644 index 00000000..055b27b5 --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/client.go @@ -0,0 +1,27 @@ +package ldap + +import ( + "crypto/tls" + "time" +) + +// Client knows how to interact with an LDAP server +type Client interface { + Start() + StartTLS(config *tls.Config) error + Close() + SetTimeout(time.Duration) + + 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) +} diff --git a/vendor/gopkg.in/ldap.v2/compare.go b/vendor/gopkg.in/ldap.v2/compare.go new file mode 100644 index 00000000..cc6d2af5 --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/compare.go @@ -0,0 +1,85 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// File contains Compare functionality +// +// https://tools.ietf.org/html/rfc4511 +// +// CompareRequest ::= [APPLICATION 14] SEQUENCE { +// entry LDAPDN, +// ava AttributeValueAssertion } +// +// AttributeValueAssertion ::= SEQUENCE { +// attributeDesc AttributeDescription, +// assertionValue AssertionValue } +// +// AttributeDescription ::= LDAPString +// -- Constrained to +// -- [RFC4512] +// +// AttributeValue ::= OCTET STRING +// + +package ldap + +import ( + "errors" + "fmt" + + "gopkg.in/asn1-ber.v1" +) + +// Compare checks to see if the attribute of the dn matches value. Returns true if it does otherwise +// false with any error that occurs if any. +func (l *Conn) Compare(dn, attribute, value string) (bool, error) { + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) + + request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationCompareRequest, nil, "Compare Request") + request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, dn, "DN")) + + ava := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "AttributeValueAssertion") + ava.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "AttributeDesc")) + ava.AppendChild(ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagOctetString, value, "AssertionValue")) + request.AppendChild(ava) + packet.AppendChild(request) + + l.Debug.PrintPacket(packet) + + msgCtx, err := l.sendMessage(packet) + if err != nil { + return false, err + } + defer l.finishMessage(msgCtx) + + l.Debug.Printf("%d: waiting for response", msgCtx.id) + packetResponse, ok := <-msgCtx.responses + if !ok { + return false, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) + } + packet, err = packetResponse.ReadPacket() + l.Debug.Printf("%d: got response %p", msgCtx.id, packet) + if err != nil { + return false, err + } + + if l.Debug { + if err := addLDAPDescriptions(packet); err != nil { + return false, err + } + ber.PrintPacket(packet) + } + + if packet.Children[1].Tag == ApplicationCompareResponse { + resultCode, resultDescription := getLDAPResultCode(packet) + if resultCode == LDAPResultCompareTrue { + return true, nil + } else if resultCode == LDAPResultCompareFalse { + return false, nil + } else { + return false, NewError(resultCode, errors.New(resultDescription)) + } + } + return false, fmt.Errorf("Unexpected Response: %d", packet.Children[1].Tag) +} diff --git a/vendor/gopkg.in/ldap.v2/conn.go b/vendor/gopkg.in/ldap.v2/conn.go new file mode 100644 index 00000000..eb28eb47 --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/conn.go @@ -0,0 +1,470 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ldap + +import ( + "crypto/tls" + "errors" + "fmt" + "log" + "net" + "sync" + "sync/atomic" + "time" + + "gopkg.in/asn1-ber.v1" +) + +const ( + // MessageQuit causes the processMessages loop to exit + MessageQuit = 0 + // MessageRequest sends a request to the server + MessageRequest = 1 + // MessageResponse receives a response from the server + MessageResponse = 2 + // MessageFinish indicates the client considers a particular message ID to be finished + MessageFinish = 3 + // MessageTimeout indicates the client-specified timeout for a particular message ID has been reached + MessageTimeout = 4 +) + +// PacketResponse contains the packet or error encountered reading a response +type PacketResponse struct { + // Packet is the packet read from the server + Packet *ber.Packet + // Error is an error encountered while reading + Error error +} + +// ReadPacket returns the packet or an error +func (pr *PacketResponse) ReadPacket() (*ber.Packet, error) { + if (pr == nil) || (pr.Packet == nil && pr.Error == nil) { + return nil, NewError(ErrorNetwork, errors.New("ldap: could not retrieve response")) + } + return pr.Packet, pr.Error +} + +type messageContext struct { + id int64 + // close(done) should only be called from finishMessage() + done chan struct{} + // close(responses) should only be called from processMessages(), and only sent to from sendResponse() + responses chan *PacketResponse +} + +// sendResponse should only be called within the processMessages() loop which +// is also responsible for closing the responses channel. +func (msgCtx *messageContext) sendResponse(packet *PacketResponse) { + select { + case msgCtx.responses <- packet: + // Successfully sent packet to message handler. + case <-msgCtx.done: + // The request handler is done and will not receive more + // packets. + } +} + +type messagePacket struct { + Op int + MessageID int64 + Packet *ber.Packet + Context *messageContext +} + +type sendMessageFlags uint + +const ( + startTLS sendMessageFlags = 1 << iota +) + +// Conn represents an LDAP Connection +type Conn struct { + conn net.Conn + isTLS bool + closing uint32 + closeErr atomicValue + isStartingTLS bool + Debug debugging + chanConfirm chan struct{} + messageContexts map[int64]*messageContext + chanMessage chan *messagePacket + chanMessageID chan int64 + wgClose sync.WaitGroup + outstandingRequests uint + messageMutex sync.Mutex + requestTimeout int64 +} + +var _ Client = &Conn{} + +// DefaultTimeout is a package-level variable that sets the timeout value +// used for the Dial and DialTLS methods. +// +// WARNING: since this is a package-level variable, setting this value from +// multiple places will probably result in undesired behaviour. +var DefaultTimeout = 60 * time.Second + +// Dial connects to the given address on the given network using net.Dial +// and then returns a new Conn for the connection. +func Dial(network, addr string) (*Conn, error) { + c, err := net.DialTimeout(network, addr, DefaultTimeout) + if err != nil { + return nil, NewError(ErrorNetwork, err) + } + conn := NewConn(c, false) + conn.Start() + return conn, nil +} + +// DialTLS connects to the given address on the given network using tls.Dial +// and then returns a new Conn for the connection. +func DialTLS(network, addr string, config *tls.Config) (*Conn, error) { + dc, err := net.DialTimeout(network, addr, DefaultTimeout) + if err != nil { + return nil, NewError(ErrorNetwork, err) + } + c := tls.Client(dc, config) + err = c.Handshake() + if err != nil { + // Handshake error, close the established connection before we return an error + dc.Close() + return nil, NewError(ErrorNetwork, err) + } + conn := NewConn(c, true) + conn.Start() + return conn, nil +} + +// NewConn returns a new Conn using conn for network I/O. +func NewConn(conn net.Conn, isTLS bool) *Conn { + return &Conn{ + conn: conn, + chanConfirm: make(chan struct{}), + chanMessageID: make(chan int64), + chanMessage: make(chan *messagePacket, 10), + messageContexts: map[int64]*messageContext{}, + requestTimeout: 0, + isTLS: isTLS, + } +} + +// Start initializes goroutines to read responses and process messages +func (l *Conn) Start() { + go l.reader() + go l.processMessages() + l.wgClose.Add(1) +} + +// isClosing returns whether or not we're currently closing. +func (l *Conn) isClosing() bool { + return atomic.LoadUint32(&l.closing) == 1 +} + +// setClosing sets the closing value to true +func (l *Conn) setClosing() bool { + return atomic.CompareAndSwapUint32(&l.closing, 0, 1) +} + +// Close closes the connection. +func (l *Conn) Close() { + l.messageMutex.Lock() + defer l.messageMutex.Unlock() + + if l.setClosing() { + l.Debug.Printf("Sending quit message and waiting for confirmation") + l.chanMessage <- &messagePacket{Op: MessageQuit} + <-l.chanConfirm + close(l.chanMessage) + + l.Debug.Printf("Closing network connection") + if err := l.conn.Close(); err != nil { + log.Println(err) + } + + l.wgClose.Done() + } + l.wgClose.Wait() +} + +// SetTimeout sets the time after a request is sent that a MessageTimeout triggers +func (l *Conn) SetTimeout(timeout time.Duration) { + if timeout > 0 { + atomic.StoreInt64(&l.requestTimeout, int64(timeout)) + } +} + +// Returns the next available messageID +func (l *Conn) nextMessageID() int64 { + if messageID, ok := <-l.chanMessageID; ok { + return messageID + } + return 0 +} + +// StartTLS sends the command to start a TLS session and then creates a new TLS Client +func (l *Conn) StartTLS(config *tls.Config) error { + if l.isTLS { + return NewError(ErrorNetwork, errors.New("ldap: already encrypted")) + } + + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) + request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Start TLS") + request.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, "1.3.6.1.4.1.1466.20037", "TLS Extended Command")) + packet.AppendChild(request) + l.Debug.PrintPacket(packet) + + msgCtx, err := l.sendMessageWithFlags(packet, startTLS) + if err != nil { + return err + } + defer l.finishMessage(msgCtx) + + l.Debug.Printf("%d: waiting for response", msgCtx.id) + + packetResponse, ok := <-msgCtx.responses + if !ok { + return NewError(ErrorNetwork, errors.New("ldap: response channel closed")) + } + packet, err = packetResponse.ReadPacket() + l.Debug.Printf("%d: got response %p", msgCtx.id, packet) + if err != nil { + return err + } + + if l.Debug { + if err := addLDAPDescriptions(packet); err != nil { + l.Close() + return err + } + ber.PrintPacket(packet) + } + + if resultCode, message := getLDAPResultCode(packet); resultCode == LDAPResultSuccess { + conn := tls.Client(l.conn, config) + + if err := conn.Handshake(); err != nil { + l.Close() + return NewError(ErrorNetwork, fmt.Errorf("TLS handshake failed (%v)", err)) + } + + l.isTLS = true + l.conn = conn + } else { + return NewError(resultCode, fmt.Errorf("ldap: cannot StartTLS (%s)", message)) + } + go l.reader() + + return nil +} + +func (l *Conn) sendMessage(packet *ber.Packet) (*messageContext, error) { + return l.sendMessageWithFlags(packet, 0) +} + +func (l *Conn) sendMessageWithFlags(packet *ber.Packet, flags sendMessageFlags) (*messageContext, error) { + if l.isClosing() { + return nil, NewError(ErrorNetwork, errors.New("ldap: connection closed")) + } + l.messageMutex.Lock() + l.Debug.Printf("flags&startTLS = %d", flags&startTLS) + if l.isStartingTLS { + l.messageMutex.Unlock() + return nil, NewError(ErrorNetwork, errors.New("ldap: connection is in startls phase")) + } + if flags&startTLS != 0 { + if l.outstandingRequests != 0 { + l.messageMutex.Unlock() + return nil, NewError(ErrorNetwork, errors.New("ldap: cannot StartTLS with outstanding requests")) + } + l.isStartingTLS = true + } + l.outstandingRequests++ + + l.messageMutex.Unlock() + + responses := make(chan *PacketResponse) + messageID := packet.Children[0].Value.(int64) + message := &messagePacket{ + Op: MessageRequest, + MessageID: messageID, + Packet: packet, + Context: &messageContext{ + id: messageID, + done: make(chan struct{}), + responses: responses, + }, + } + l.sendProcessMessage(message) + return message.Context, nil +} + +func (l *Conn) finishMessage(msgCtx *messageContext) { + close(msgCtx.done) + + if l.isClosing() { + return + } + + l.messageMutex.Lock() + l.outstandingRequests-- + if l.isStartingTLS { + l.isStartingTLS = false + } + l.messageMutex.Unlock() + + message := &messagePacket{ + Op: MessageFinish, + MessageID: msgCtx.id, + } + l.sendProcessMessage(message) +} + +func (l *Conn) sendProcessMessage(message *messagePacket) bool { + l.messageMutex.Lock() + defer l.messageMutex.Unlock() + if l.isClosing() { + return false + } + l.chanMessage <- message + return true +} + +func (l *Conn) processMessages() { + defer func() { + if err := recover(); err != nil { + log.Printf("ldap: recovered panic in processMessages: %v", err) + } + for messageID, msgCtx := range l.messageContexts { + // If we are closing due to an error, inform anyone who + // is waiting about the error. + if l.isClosing() && l.closeErr.Load() != nil { + msgCtx.sendResponse(&PacketResponse{Error: l.closeErr.Load().(error)}) + } + l.Debug.Printf("Closing channel for MessageID %d", messageID) + close(msgCtx.responses) + delete(l.messageContexts, messageID) + } + close(l.chanMessageID) + close(l.chanConfirm) + }() + + var messageID int64 = 1 + for { + select { + case l.chanMessageID <- messageID: + messageID++ + case message := <-l.chanMessage: + switch message.Op { + case MessageQuit: + l.Debug.Printf("Shutting down - quit message received") + return + case MessageRequest: + // Add to message list and write to network + l.Debug.Printf("Sending message %d", message.MessageID) + + buf := message.Packet.Bytes() + _, err := l.conn.Write(buf) + if err != nil { + l.Debug.Printf("Error Sending Message: %s", err.Error()) + message.Context.sendResponse(&PacketResponse{Error: fmt.Errorf("unable to send request: %s", err)}) + close(message.Context.responses) + break + } + + // Only add to messageContexts if we were able to + // successfully write the message. + l.messageContexts[message.MessageID] = message.Context + + // Add timeout if defined + requestTimeout := time.Duration(atomic.LoadInt64(&l.requestTimeout)) + if requestTimeout > 0 { + go func() { + defer func() { + if err := recover(); err != nil { + log.Printf("ldap: recovered panic in RequestTimeout: %v", err) + } + }() + time.Sleep(requestTimeout) + timeoutMessage := &messagePacket{ + Op: MessageTimeout, + MessageID: message.MessageID, + } + l.sendProcessMessage(timeoutMessage) + }() + } + case MessageResponse: + l.Debug.Printf("Receiving message %d", message.MessageID) + if msgCtx, ok := l.messageContexts[message.MessageID]; ok { + msgCtx.sendResponse(&PacketResponse{message.Packet, nil}) + } else { + log.Printf("Received unexpected message %d, %v", message.MessageID, l.isClosing()) + ber.PrintPacket(message.Packet) + } + case MessageTimeout: + // Handle the timeout by closing the channel + // All reads will return immediately + if msgCtx, ok := l.messageContexts[message.MessageID]; ok { + l.Debug.Printf("Receiving message timeout for %d", message.MessageID) + msgCtx.sendResponse(&PacketResponse{message.Packet, errors.New("ldap: connection timed out")}) + delete(l.messageContexts, message.MessageID) + close(msgCtx.responses) + } + case MessageFinish: + l.Debug.Printf("Finished message %d", message.MessageID) + if msgCtx, ok := l.messageContexts[message.MessageID]; ok { + delete(l.messageContexts, message.MessageID) + close(msgCtx.responses) + } + } + } + } +} + +func (l *Conn) reader() { + cleanstop := false + defer func() { + if err := recover(); err != nil { + log.Printf("ldap: recovered panic in reader: %v", err) + } + if !cleanstop { + l.Close() + } + }() + + for { + if cleanstop { + l.Debug.Printf("reader clean stopping (without closing the connection)") + return + } + packet, err := ber.ReadPacket(l.conn) + if err != nil { + // A read error is expected here if we are closing the connection... + if !l.isClosing() { + l.closeErr.Store(fmt.Errorf("unable to read LDAP response packet: %s", err)) + l.Debug.Printf("reader error: %s", err.Error()) + } + return + } + addLDAPDescriptions(packet) + if len(packet.Children) == 0 { + l.Debug.Printf("Received bad ldap packet") + continue + } + l.messageMutex.Lock() + if l.isStartingTLS { + cleanstop = true + } + l.messageMutex.Unlock() + message := &messagePacket{ + Op: MessageResponse, + MessageID: packet.Children[0].Value.(int64), + Packet: packet, + } + if !l.sendProcessMessage(message) { + return + } + } +} diff --git a/vendor/gopkg.in/ldap.v2/control.go b/vendor/gopkg.in/ldap.v2/control.go new file mode 100644 index 00000000..342f325c --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/control.go @@ -0,0 +1,420 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ldap + +import ( + "fmt" + "strconv" + + "gopkg.in/asn1-ber.v1" +) + +const ( + // ControlTypePaging - https://www.ietf.org/rfc/rfc2696.txt + ControlTypePaging = "1.2.840.113556.1.4.319" + // ControlTypeBeheraPasswordPolicy - https://tools.ietf.org/html/draft-behera-ldap-password-policy-10 + ControlTypeBeheraPasswordPolicy = "1.3.6.1.4.1.42.2.27.8.5.1" + // ControlTypeVChuPasswordMustChange - https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00 + ControlTypeVChuPasswordMustChange = "2.16.840.1.113730.3.4.4" + // ControlTypeVChuPasswordWarning - https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00 + ControlTypeVChuPasswordWarning = "2.16.840.1.113730.3.4.5" + // ControlTypeManageDsaIT - https://tools.ietf.org/html/rfc3296 + ControlTypeManageDsaIT = "2.16.840.1.113730.3.4.2" +) + +// ControlTypeMap maps controls to text descriptions +var ControlTypeMap = map[string]string{ + ControlTypePaging: "Paging", + ControlTypeBeheraPasswordPolicy: "Password Policy - Behera Draft", + ControlTypeManageDsaIT: "Manage DSA IT", +} + +// Control defines an interface controls provide to encode and describe themselves +type Control interface { + // GetControlType returns the OID + GetControlType() string + // Encode returns the ber packet representation + Encode() *ber.Packet + // String returns a human-readable description + String() string +} + +// ControlString implements the Control interface for simple controls +type ControlString struct { + ControlType string + Criticality bool + ControlValue string +} + +// GetControlType returns the OID +func (c *ControlString) GetControlType() string { + return c.ControlType +} + +// Encode returns the ber packet representation +func (c *ControlString) Encode() *ber.Packet { + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, c.ControlType, "Control Type ("+ControlTypeMap[c.ControlType]+")")) + if c.Criticality { + packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, c.Criticality, "Criticality")) + } + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, string(c.ControlValue), "Control Value")) + return packet +} + +// String returns a human-readable description +func (c *ControlString) String() string { + return fmt.Sprintf("Control Type: %s (%q) Criticality: %t Control Value: %s", ControlTypeMap[c.ControlType], c.ControlType, c.Criticality, c.ControlValue) +} + +// ControlPaging implements the paging control described in https://www.ietf.org/rfc/rfc2696.txt +type ControlPaging struct { + // PagingSize indicates the page size + PagingSize uint32 + // Cookie is an opaque value returned by the server to track a paging cursor + Cookie []byte +} + +// GetControlType returns the OID +func (c *ControlPaging) GetControlType() string { + return ControlTypePaging +} + +// Encode returns the ber packet representation +func (c *ControlPaging) Encode() *ber.Packet { + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypePaging, "Control Type ("+ControlTypeMap[ControlTypePaging]+")")) + + p2 := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (Paging)") + seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Search Control Value") + seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, int64(c.PagingSize), "Paging Size")) + cookie := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Cookie") + cookie.Value = c.Cookie + cookie.Data.Write(c.Cookie) + seq.AppendChild(cookie) + p2.AppendChild(seq) + + packet.AppendChild(p2) + return packet +} + +// String returns a human-readable description +func (c *ControlPaging) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t PagingSize: %d Cookie: %q", + ControlTypeMap[ControlTypePaging], + ControlTypePaging, + false, + c.PagingSize, + c.Cookie) +} + +// SetCookie stores the given cookie in the paging control +func (c *ControlPaging) SetCookie(cookie []byte) { + c.Cookie = cookie +} + +// ControlBeheraPasswordPolicy implements the control described in https://tools.ietf.org/html/draft-behera-ldap-password-policy-10 +type ControlBeheraPasswordPolicy struct { + // Expire contains the number of seconds before a password will expire + Expire int64 + // Grace indicates the remaining number of times a user will be allowed to authenticate with an expired password + Grace int64 + // Error indicates the error code + Error int8 + // ErrorString is a human readable error + ErrorString string +} + +// GetControlType returns the OID +func (c *ControlBeheraPasswordPolicy) GetControlType() string { + return ControlTypeBeheraPasswordPolicy +} + +// Encode returns the ber packet representation +func (c *ControlBeheraPasswordPolicy) Encode() *ber.Packet { + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, ControlTypeBeheraPasswordPolicy, "Control Type ("+ControlTypeMap[ControlTypeBeheraPasswordPolicy]+")")) + + return packet +} + +// String returns a human-readable description +func (c *ControlBeheraPasswordPolicy) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t Expire: %d Grace: %d Error: %d, ErrorString: %s", + ControlTypeMap[ControlTypeBeheraPasswordPolicy], + ControlTypeBeheraPasswordPolicy, + false, + c.Expire, + c.Grace, + c.Error, + c.ErrorString) +} + +// ControlVChuPasswordMustChange implements the control described in https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00 +type ControlVChuPasswordMustChange struct { + // MustChange indicates if the password is required to be changed + MustChange bool +} + +// GetControlType returns the OID +func (c *ControlVChuPasswordMustChange) GetControlType() string { + return ControlTypeVChuPasswordMustChange +} + +// Encode returns the ber packet representation +func (c *ControlVChuPasswordMustChange) Encode() *ber.Packet { + return nil +} + +// String returns a human-readable description +func (c *ControlVChuPasswordMustChange) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t MustChange: %v", + ControlTypeMap[ControlTypeVChuPasswordMustChange], + ControlTypeVChuPasswordMustChange, + false, + c.MustChange) +} + +// ControlVChuPasswordWarning implements the control described in https://tools.ietf.org/html/draft-vchu-ldap-pwd-policy-00 +type ControlVChuPasswordWarning struct { + // Expire indicates the time in seconds until the password expires + Expire int64 +} + +// GetControlType returns the OID +func (c *ControlVChuPasswordWarning) GetControlType() string { + return ControlTypeVChuPasswordWarning +} + +// Encode returns the ber packet representation +func (c *ControlVChuPasswordWarning) Encode() *ber.Packet { + return nil +} + +// String returns a human-readable description +func (c *ControlVChuPasswordWarning) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t Expire: %b", + ControlTypeMap[ControlTypeVChuPasswordWarning], + ControlTypeVChuPasswordWarning, + false, + c.Expire) +} + +// ControlManageDsaIT implements the control described in https://tools.ietf.org/html/rfc3296 +type ControlManageDsaIT struct { + // Criticality indicates if this control is required + Criticality bool +} + +// GetControlType returns the OID +func (c *ControlManageDsaIT) GetControlType() string { + return ControlTypeManageDsaIT +} + +// Encode returns the ber packet representation +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 +} + +// String returns a human-readable description +func (c *ControlManageDsaIT) String() string { + return fmt.Sprintf( + "Control Type: %s (%q) Criticality: %t", + ControlTypeMap[ControlTypeManageDsaIT], + ControlTypeManageDsaIT, + c.Criticality) +} + +// NewControlManageDsaIT returns a ControlManageDsaIT control +func NewControlManageDsaIT(Criticality bool) *ControlManageDsaIT { + return &ControlManageDsaIT{Criticality: Criticality} +} + +// FindControl returns the first control of the given type in the list, or nil +func FindControl(controls []Control, controlType string) Control { + for _, c := range controls { + if c.GetControlType() == controlType { + return c + } + } + return nil +} + +// DecodeControl returns a control read from the given packet, or nil if no recognized control can be made +func DecodeControl(packet *ber.Packet) Control { + var ( + ControlType = "" + Criticality = false + value *ber.Packet + ) + + switch len(packet.Children) { + case 0: + // at least one child is required for control type + return nil + + case 1: + // just type, no criticality or value + packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")" + ControlType = packet.Children[0].Value.(string) + + case 2: + packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")" + ControlType = packet.Children[0].Value.(string) + + // Children[1] could be criticality or value (both are optional) + // duck-type on whether this is a boolean + if _, ok := packet.Children[1].Value.(bool); ok { + packet.Children[1].Description = "Criticality" + Criticality = packet.Children[1].Value.(bool) + } else { + packet.Children[1].Description = "Control Value" + value = packet.Children[1] + } + + case 3: + packet.Children[0].Description = "Control Type (" + ControlTypeMap[ControlType] + ")" + ControlType = packet.Children[0].Value.(string) + + packet.Children[1].Description = "Criticality" + Criticality = packet.Children[1].Value.(bool) + + packet.Children[2].Description = "Control Value" + value = packet.Children[2] + + default: + // more than 3 children is invalid + return nil + } + + switch ControlType { + case ControlTypeManageDsaIT: + return NewControlManageDsaIT(Criticality) + case ControlTypePaging: + value.Description += " (Paging)" + c := new(ControlPaging) + if value.Value != nil { + valueChildren := ber.DecodePacket(value.Data.Bytes()) + value.Data.Truncate(0) + value.Value = nil + value.AppendChild(valueChildren) + } + value = value.Children[0] + value.Description = "Search Control Value" + value.Children[0].Description = "Paging Size" + value.Children[1].Description = "Cookie" + c.PagingSize = uint32(value.Children[0].Value.(int64)) + c.Cookie = value.Children[1].Data.Bytes() + value.Children[1].Value = c.Cookie + return c + case ControlTypeBeheraPasswordPolicy: + value.Description += " (Password Policy - Behera)" + c := NewControlBeheraPasswordPolicy() + if value.Value != nil { + valueChildren := ber.DecodePacket(value.Data.Bytes()) + value.Data.Truncate(0) + value.Value = nil + value.AppendChild(valueChildren) + } + + sequence := value.Children[0] + + for _, child := range sequence.Children { + if child.Tag == 0 { + //Warning + warningPacket := child.Children[0] + packet := ber.DecodePacket(warningPacket.Data.Bytes()) + val, ok := packet.Value.(int64) + if ok { + if warningPacket.Tag == 0 { + //timeBeforeExpiration + c.Expire = val + warningPacket.Value = c.Expire + } else if warningPacket.Tag == 1 { + //graceAuthNsRemaining + c.Grace = val + warningPacket.Value = c.Grace + } + } + } else if child.Tag == 1 { + // Error + packet := ber.DecodePacket(child.Data.Bytes()) + val, ok := packet.Value.(int8) + if !ok { + // what to do? + val = -1 + } + c.Error = val + child.Value = c.Error + c.ErrorString = BeheraPasswordPolicyErrorMap[c.Error] + } + } + return c + case ControlTypeVChuPasswordMustChange: + c := &ControlVChuPasswordMustChange{MustChange: true} + return c + case ControlTypeVChuPasswordWarning: + c := &ControlVChuPasswordWarning{Expire: -1} + expireStr := ber.DecodeString(value.Data.Bytes()) + + expire, err := strconv.ParseInt(expireStr, 10, 64) + if err != nil { + return nil + } + c.Expire = expire + value.Value = c.Expire + + return c + default: + c := new(ControlString) + c.ControlType = ControlType + c.Criticality = Criticality + if value != nil { + c.ControlValue = value.Value.(string) + } + return c + } +} + +// NewControlString returns a generic control +func NewControlString(controlType string, criticality bool, controlValue string) *ControlString { + return &ControlString{ + ControlType: controlType, + Criticality: criticality, + ControlValue: controlValue, + } +} + +// NewControlPaging returns a paging control +func NewControlPaging(pagingSize uint32) *ControlPaging { + return &ControlPaging{PagingSize: pagingSize} +} + +// NewControlBeheraPasswordPolicy returns a ControlBeheraPasswordPolicy +func NewControlBeheraPasswordPolicy() *ControlBeheraPasswordPolicy { + return &ControlBeheraPasswordPolicy{ + Expire: -1, + Grace: -1, + Error: -1, + } +} + +func encodeControls(controls []Control) *ber.Packet { + packet := ber.Encode(ber.ClassContext, ber.TypeConstructed, 0, nil, "Controls") + for _, control := range controls { + packet.AppendChild(control.Encode()) + } + return packet +} diff --git a/vendor/gopkg.in/ldap.v2/debug.go b/vendor/gopkg.in/ldap.v2/debug.go new file mode 100644 index 00000000..7279fc25 --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/debug.go @@ -0,0 +1,24 @@ +package ldap + +import ( + "log" + + "gopkg.in/asn1-ber.v1" +) + +// debugging type +// - has a Printf method to write the debug output +type debugging bool + +// write debug output +func (debug debugging) Printf(format string, args ...interface{}) { + if debug { + log.Printf(format, args...) + } +} + +func (debug debugging) PrintPacket(packet *ber.Packet) { + if debug { + ber.PrintPacket(packet) + } +} diff --git a/vendor/gopkg.in/ldap.v2/del.go b/vendor/gopkg.in/ldap.v2/del.go new file mode 100644 index 00000000..4fd63dc3 --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/del.go @@ -0,0 +1,84 @@ +// +// https://tools.ietf.org/html/rfc4511 +// +// DelRequest ::= [APPLICATION 10] LDAPDN + +package ldap + +import ( + "errors" + "log" + + "gopkg.in/asn1-ber.v1" +) + +// DelRequest implements an LDAP deletion request +type DelRequest struct { + // DN is the name of the directory entry to delete + DN string + // Controls hold optional controls to send with the request + 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 +} + +// NewDelRequest creates a delete request for the given DN and controls +func NewDelRequest(DN string, + Controls []Control) *DelRequest { + return &DelRequest{ + DN: DN, + Controls: Controls, + } +} + +// Del executes the given delete request +func (l *Conn) Del(delRequest *DelRequest) error { + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) + packet.AppendChild(delRequest.encode()) + if delRequest.Controls != nil { + packet.AppendChild(encodeControls(delRequest.Controls)) + } + + l.Debug.PrintPacket(packet) + + msgCtx, err := l.sendMessage(packet) + if err != nil { + return err + } + defer l.finishMessage(msgCtx) + + l.Debug.Printf("%d: waiting for response", msgCtx.id) + packetResponse, ok := <-msgCtx.responses + if !ok { + return NewError(ErrorNetwork, errors.New("ldap: response channel closed")) + } + packet, err = packetResponse.ReadPacket() + l.Debug.Printf("%d: got response %p", msgCtx.id, packet) + if err != nil { + return err + } + + 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", msgCtx.id) + return nil +} diff --git a/vendor/gopkg.in/ldap.v2/dn.go b/vendor/gopkg.in/ldap.v2/dn.go new file mode 100644 index 00000000..34e9023a --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/dn.go @@ -0,0 +1,247 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// File contains DN parsing functionality +// +// https://tools.ietf.org/html/rfc4514 +// +// distinguishedName = [ relativeDistinguishedName +// *( COMMA relativeDistinguishedName ) ] +// relativeDistinguishedName = attributeTypeAndValue +// *( PLUS attributeTypeAndValue ) +// attributeTypeAndValue = attributeType EQUALS attributeValue +// attributeType = descr / numericoid +// attributeValue = string / hexstring +// +// ; The following characters are to be escaped when they appear +// ; in the value to be encoded: ESC, one of , leading +// ; SHARP or SPACE, trailing SPACE, and NULL. +// string = [ ( leadchar / pair ) [ *( stringchar / pair ) +// ( trailchar / pair ) ] ] +// +// leadchar = LUTF1 / UTFMB +// LUTF1 = %x01-1F / %x21 / %x24-2A / %x2D-3A / +// %x3D / %x3F-5B / %x5D-7F +// +// trailchar = TUTF1 / UTFMB +// TUTF1 = %x01-1F / %x21 / %x23-2A / %x2D-3A / +// %x3D / %x3F-5B / %x5D-7F +// +// stringchar = SUTF1 / UTFMB +// SUTF1 = %x01-21 / %x23-2A / %x2D-3A / +// %x3D / %x3F-5B / %x5D-7F +// +// pair = ESC ( ESC / special / hexpair ) +// special = escaped / SPACE / SHARP / EQUALS +// escaped = DQUOTE / PLUS / COMMA / SEMI / LANGLE / RANGLE +// hexstring = SHARP 1*hexpair +// hexpair = HEX HEX +// +// where the productions , , , , +// , , , , , , , , +// , , and are defined in [RFC4512]. +// + +package ldap + +import ( + "bytes" + enchex "encoding/hex" + "errors" + "fmt" + "strings" + + "gopkg.in/asn1-ber.v1" +) + +// AttributeTypeAndValue represents an attributeTypeAndValue from https://tools.ietf.org/html/rfc4514 +type AttributeTypeAndValue struct { + // Type is the attribute type + Type string + // Value is the attribute value + Value string +} + +// RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514 +type RelativeDN struct { + Attributes []*AttributeTypeAndValue +} + +// DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514 +type DN struct { + RDNs []*RelativeDN +} + +// ParseDN returns a distinguishedName or an error +func ParseDN(str string) (*DN, error) { + dn := new(DN) + dn.RDNs = make([]*RelativeDN, 0) + rdn := new(RelativeDN) + rdn.Attributes = make([]*AttributeTypeAndValue, 0) + buffer := bytes.Buffer{} + attribute := new(AttributeTypeAndValue) + escaping := false + + unescapedTrailingSpaces := 0 + stringFromBuffer := func() string { + s := buffer.String() + s = s[0 : len(s)-unescapedTrailingSpaces] + buffer.Reset() + unescapedTrailingSpaces = 0 + return s + } + + for i := 0; i < len(str); i++ { + char := str[i] + if escaping { + unescapedTrailingSpaces = 0 + escaping = false + switch char { + case ' ', '"', '#', '+', ',', ';', '<', '=', '>', '\\': + buffer.WriteByte(char) + continue + } + // Not a special character, assume hex encoded octet + if len(str) == i+1 { + return nil, errors.New("Got corrupted escaped character") + } + + dst := []byte{0} + n, err := enchex.Decode([]byte(dst), []byte(str[i:i+2])) + if err != nil { + return nil, fmt.Errorf("Failed to decode escaped character: %s", err) + } else if n != 1 { + return nil, fmt.Errorf("Expected 1 byte when un-escaping, got %d", n) + } + buffer.WriteByte(dst[0]) + i++ + } else if char == '\\' { + unescapedTrailingSpaces = 0 + escaping = true + } else if char == '=' { + attribute.Type = stringFromBuffer() + // Special case: If the first character in the value is # the + // following data is BER encoded so we can just fast forward + // and decode. + if len(str) > i+1 && str[i+1] == '#' { + i += 2 + index := strings.IndexAny(str[i:], ",+") + data := str + if index > 0 { + data = str[i : i+index] + } else { + data = str[i:] + } + rawBER, err := enchex.DecodeString(data) + if err != nil { + return nil, fmt.Errorf("Failed to decode BER encoding: %s", err) + } + packet := ber.DecodePacket(rawBER) + buffer.WriteString(packet.Data.String()) + i += len(data) - 1 + } + } else if char == ',' || char == '+' { + // We're done with this RDN or value, push it + if len(attribute.Type) == 0 { + return nil, errors.New("incomplete type, value pair") + } + attribute.Value = stringFromBuffer() + rdn.Attributes = append(rdn.Attributes, attribute) + attribute = new(AttributeTypeAndValue) + if char == ',' { + dn.RDNs = append(dn.RDNs, rdn) + rdn = new(RelativeDN) + rdn.Attributes = make([]*AttributeTypeAndValue, 0) + } + } else if char == ' ' && buffer.Len() == 0 { + // ignore unescaped leading spaces + continue + } else { + if char == ' ' { + // Track unescaped spaces in case they are trailing and we need to remove them + unescapedTrailingSpaces++ + } else { + // Reset if we see a non-space char + unescapedTrailingSpaces = 0 + } + buffer.WriteByte(char) + } + } + if buffer.Len() > 0 { + if len(attribute.Type) == 0 { + return nil, errors.New("DN ended with incomplete type, value pair") + } + attribute.Value = stringFromBuffer() + rdn.Attributes = append(rdn.Attributes, attribute) + dn.RDNs = append(dn.RDNs, rdn) + } + return dn, nil +} + +// Equal returns true if the DNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). +// Returns true if they have the same number of relative distinguished names +// and corresponding relative distinguished names (by position) are the same. +func (d *DN) Equal(other *DN) bool { + if len(d.RDNs) != len(other.RDNs) { + return false + } + for i := range d.RDNs { + if !d.RDNs[i].Equal(other.RDNs[i]) { + return false + } + } + return true +} + +// AncestorOf returns true if the other DN consists of at least one RDN followed by all the RDNs of the current DN. +// "ou=widgets,o=acme.com" is an ancestor of "ou=sprockets,ou=widgets,o=acme.com" +// "ou=widgets,o=acme.com" is not an ancestor of "ou=sprockets,ou=widgets,o=foo.com" +// "ou=widgets,o=acme.com" is not an ancestor of "ou=widgets,o=acme.com" +func (d *DN) AncestorOf(other *DN) bool { + if len(d.RDNs) >= len(other.RDNs) { + return false + } + // Take the last `len(d.RDNs)` RDNs from the other DN to compare against + otherRDNs := other.RDNs[len(other.RDNs)-len(d.RDNs):] + for i := range d.RDNs { + if !d.RDNs[i].Equal(otherRDNs[i]) { + return false + } + } + return true +} + +// Equal returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). +// Relative distinguished names are the same if and only if they have the same number of AttributeTypeAndValues +// and each attribute of the first RDN is the same as the attribute of the second RDN with the same attribute type. +// The order of attributes is not significant. +// Case of attribute types is not significant. +func (r *RelativeDN) Equal(other *RelativeDN) bool { + if len(r.Attributes) != len(other.Attributes) { + return false + } + return r.hasAllAttributes(other.Attributes) && other.hasAllAttributes(r.Attributes) +} + +func (r *RelativeDN) hasAllAttributes(attrs []*AttributeTypeAndValue) bool { + for _, attr := range attrs { + found := false + for _, myattr := range r.Attributes { + if myattr.Equal(attr) { + found = true + break + } + } + if !found { + return false + } + } + return true +} + +// Equal returns true if the AttributeTypeAndValue is equivalent to the specified AttributeTypeAndValue +// Case of the attribute type is not significant +func (a *AttributeTypeAndValue) Equal(other *AttributeTypeAndValue) bool { + return strings.EqualFold(a.Type, other.Type) && a.Value == other.Value +} diff --git a/vendor/gopkg.in/ldap.v2/doc.go b/vendor/gopkg.in/ldap.v2/doc.go new file mode 100644 index 00000000..f20d39bc --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/doc.go @@ -0,0 +1,4 @@ +/* +Package ldap provides basic LDAP v3 functionality. +*/ +package ldap diff --git a/vendor/gopkg.in/ldap.v2/error.go b/vendor/gopkg.in/ldap.v2/error.go new file mode 100644 index 00000000..4cccb537 --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/error.go @@ -0,0 +1,155 @@ +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 +) + +// LDAPResultCodeMap contains string descriptions for LDAP error codes +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", + + ErrorNetwork: "Network Error", + ErrorFilterCompile: "Filter Compile Error", + ErrorFilterDecompile: "Filter Decompile Error", + ErrorDebugging: "Debugging Error", + ErrorUnexpectedMessage: "Unexpected Message", + ErrorUnexpectedResponse: "Unexpected Response", +} + +func getLDAPResultCode(packet *ber.Packet) (code uint8, description string) { + if packet == nil { + return ErrorUnexpectedResponse, "Empty packet" + } else if len(packet.Children) >= 2 { + response := packet.Children[1] + if response == nil { + return ErrorUnexpectedResponse, "Empty response in packet" + } + 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" +} + +// Error holds LDAP error information +type Error struct { + // Err is the underlying error + Err error + // ResultCode is the LDAP error code + ResultCode uint8 +} + +func (e *Error) Error() string { + return fmt.Sprintf("LDAP Result Code %d %q: %s", e.ResultCode, LDAPResultCodeMap[e.ResultCode], e.Err.Error()) +} + +// NewError creates an LDAP error with the given code and underlying error +func NewError(resultCode uint8, err error) error { + return &Error{ResultCode: resultCode, Err: err} +} + +// IsErrorWithCode returns true if the given error is an LDAP error with the given result code +func IsErrorWithCode(err error, desiredResultCode uint8) bool { + if err == nil { + return false + } + + serverError, ok := err.(*Error) + if !ok { + return false + } + + return serverError.ResultCode == desiredResultCode +} diff --git a/vendor/gopkg.in/ldap.v2/filter.go b/vendor/gopkg.in/ldap.v2/filter.go new file mode 100644 index 00000000..3858a286 --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/filter.go @@ -0,0 +1,469 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ldap + +import ( + "bytes" + hexpac "encoding/hex" + "errors" + "fmt" + "strings" + "unicode/utf8" + + "gopkg.in/asn1-ber.v1" +) + +// Filter choices +const ( + FilterAnd = 0 + FilterOr = 1 + FilterNot = 2 + FilterEqualityMatch = 3 + FilterSubstrings = 4 + FilterGreaterOrEqual = 5 + FilterLessOrEqual = 6 + FilterPresent = 7 + FilterApproxMatch = 8 + FilterExtensibleMatch = 9 +) + +// FilterMap contains human readable descriptions of Filter choices +var FilterMap = map[uint64]string{ + FilterAnd: "And", + FilterOr: "Or", + FilterNot: "Not", + FilterEqualityMatch: "Equality Match", + FilterSubstrings: "Substrings", + FilterGreaterOrEqual: "Greater Or Equal", + FilterLessOrEqual: "Less Or Equal", + FilterPresent: "Present", + FilterApproxMatch: "Approx Match", + FilterExtensibleMatch: "Extensible Match", +} + +// SubstringFilter options +const ( + FilterSubstringsInitial = 0 + FilterSubstringsAny = 1 + FilterSubstringsFinal = 2 +) + +// FilterSubstringsMap contains human readable descriptions of SubstringFilter choices +var FilterSubstringsMap = map[uint64]string{ + FilterSubstringsInitial: "Substrings Initial", + FilterSubstringsAny: "Substrings Any", + FilterSubstringsFinal: "Substrings Final", +} + +// MatchingRuleAssertion choices +const ( + MatchingRuleAssertionMatchingRule = 1 + MatchingRuleAssertionType = 2 + MatchingRuleAssertionMatchValue = 3 + MatchingRuleAssertionDNAttributes = 4 +) + +// MatchingRuleAssertionMap contains human readable descriptions of MatchingRuleAssertion choices +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", +} + +// CompileFilter converts a string representation of a filter into a BER-encoded packet +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 '('")) + } + packet, pos, err := compileFilter(filter, 1) + if err != nil { + return nil, err + } + switch { + case pos > len(filter): + return nil, NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) + case pos < len(filter): + return nil, NewError(ErrorFilterCompile, errors.New("ldap: finished compiling filter with extra at end: "+fmt.Sprint(filter[pos:]))) + } + return packet, nil +} + +// DecompileFilter converts a packet representation of a filter into a string representation +func DecompileFilter(packet *ber.Packet) (ret string, err error) { + defer func() { + if r := recover(); r != nil { + err = NewError(ErrorFilterDecompile, errors.New("ldap: error decompiling filter")) + } + }() + ret = "(" + err = nil + childStr := "" + + switch packet.Tag { + case FilterAnd: + ret += "&" + for _, child := range packet.Children { + childStr, err = DecompileFilter(child) + if err != nil { + return + } + ret += childStr + } + case FilterOr: + ret += "|" + for _, child := range packet.Children { + childStr, err = DecompileFilter(child) + if err != nil { + return + } + ret += childStr + } + case FilterNot: + ret += "!" + childStr, err = DecompileFilter(packet.Children[0]) + if err != nil { + return + } + ret += childStr + + case FilterSubstrings: + ret += ber.DecodeString(packet.Children[0].Data.Bytes()) + ret += "=" + for i, child := range packet.Children[1].Children { + if i == 0 && child.Tag != FilterSubstringsInitial { + ret += "*" + } + ret += EscapeFilter(ber.DecodeString(child.Data.Bytes())) + if child.Tag != FilterSubstringsFinal { + ret += "*" + } + } + case FilterEqualityMatch: + ret += ber.DecodeString(packet.Children[0].Data.Bytes()) + ret += "=" + ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes())) + case FilterGreaterOrEqual: + ret += ber.DecodeString(packet.Children[0].Data.Bytes()) + ret += ">=" + ret += EscapeFilter(ber.DecodeString(packet.Children[1].Data.Bytes())) + case FilterLessOrEqual: + ret += ber.DecodeString(packet.Children[0].Data.Bytes()) + ret += "<=" + 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 += 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 += ")" + return +} + +func compileFilterSet(filter string, pos int, parent *ber.Packet) (int, error) { + for pos < len(filter) && filter[pos] == '(' { + child, newPos, err := compileFilter(filter, pos+1) + if err != nil { + return pos, err + } + pos = newPos + parent.AppendChild(child) + } + if pos == len(filter) { + return pos, NewError(ErrorFilterCompile, errors.New("ldap: unexpected end of filter")) + } + + return pos + 1, nil +} + +func compileFilter(filter string, pos int) (*ber.Packet, int, 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 + + 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+currentWidth) + newPos++ + return packet, newPos, err + case '&': + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterAnd, nil, FilterMap[FilterAnd]) + 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+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+currentWidth) + packet.AppendChild(child) + return packet, newPos, err + default: + const ( + stateReadingAttr = 0 + stateReadingExtensibleMatchingRule = 1 + stateReadingCondition = 2 + ) + + state := stateReadingAttr + + attribute := "" + extensibleDNAttributes := false + extensibleMatchingRule := "" + condition := "" + + 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 stateReadingAttr: + 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 = stateReadingCondition + 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 = stateReadingExtensibleMatchingRule + newPos += 4 + + // Extensible rule, with attr only + case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) + state = stateReadingCondition + newPos += 2 + + // Extensible rule, with no DN attribute matching + case currentRune == ':': + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterExtensibleMatch, nil, FilterMap[FilterExtensibleMatch]) + state = stateReadingExtensibleMatchingRule + newPos++ + + // Equality condition + case currentRune == '=': + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterEqualityMatch, nil, FilterMap[FilterEqualityMatch]) + state = stateReadingCondition + newPos++ + + // Greater-than or equal + case currentRune == '>' && strings.HasPrefix(remainingFilter, ">="): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterGreaterOrEqual, nil, FilterMap[FilterGreaterOrEqual]) + state = stateReadingCondition + newPos += 2 + + // Less-than or equal + case currentRune == '<' && strings.HasPrefix(remainingFilter, "<="): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterLessOrEqual, nil, FilterMap[FilterLessOrEqual]) + state = stateReadingCondition + newPos += 2 + + // Approx + case currentRune == '~' && strings.HasPrefix(remainingFilter, "~="): + packet = ber.Encode(ber.ClassContext, ber.TypeConstructed, FilterApproxMatch, nil, FilterMap[FilterApproxMatch]) + state = stateReadingCondition + newPos += 2 + + // Still reading the attribute name + default: + attribute += fmt.Sprintf("%c", currentRune) + newPos += currentWidth + } + + case stateReadingExtensibleMatchingRule: + switch { + + // Matching rule OID is done + case currentRune == ':' && strings.HasPrefix(remainingFilter, ":="): + state = stateReadingCondition + newPos += 2 + + // Still reading the matching rule oid + default: + extensibleMatchingRule += fmt.Sprintf("%c", currentRune) + newPos += currentWidth + } + + case stateReadingCondition: + // 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 + } + if packet == nil { + err = NewError(ErrorFilterCompile, errors.New("ldap: error parsing filter")) + return packet, newPos, err + } + + 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, encodeErr := escapedStringToEncodedBytes(condition) + if encodeErr != nil { + return packet, newPos, encodeErr + } + 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, "*"): + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute")) + packet.Tag = FilterSubstrings + packet.Description = FilterMap[uint64(packet.Tag)] + seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Substrings") + parts := strings.Split(condition, "*") + for i, part := range parts { + if part == "" { + continue + } + var tag ber.Tag + switch i { + case 0: + tag = FilterSubstringsInitial + case len(parts) - 1: + tag = FilterSubstringsFinal + default: + tag = FilterSubstringsAny + } + encodedString, encodeErr := escapedStringToEncodedBytes(part) + if encodeErr != nil { + return packet, newPos, encodeErr + } + seq.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, tag, encodedString, FilterSubstringsMap[uint64(tag)])) + } + packet.AppendChild(seq) + default: + encodedString, encodeErr := escapedStringToEncodedBytes(condition) + if encodeErr != nil { + return packet, newPos, encodeErr + } + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute")) + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, encodedString, "Condition")) + } + + 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")) + } + escByte, decodeErr := hexpac.DecodeString(escapedString[i+1 : i+3]) + if decodeErr != nil { + return "", NewError(ErrorFilterCompile, errors.New("ldap: invalid characters for escape in filter")) + } + 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 +} diff --git a/vendor/gopkg.in/ldap.v2/ldap.go b/vendor/gopkg.in/ldap.v2/ldap.go new file mode 100644 index 00000000..49692475 --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/ldap.go @@ -0,0 +1,320 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ldap + +import ( + "errors" + "io/ioutil" + "os" + + "gopkg.in/asn1-ber.v1" +) + +// LDAP Application Codes +const ( + ApplicationBindRequest = 0 + ApplicationBindResponse = 1 + ApplicationUnbindRequest = 2 + ApplicationSearchRequest = 3 + ApplicationSearchResultEntry = 4 + ApplicationSearchResultDone = 5 + ApplicationModifyRequest = 6 + ApplicationModifyResponse = 7 + ApplicationAddRequest = 8 + ApplicationAddResponse = 9 + ApplicationDelRequest = 10 + ApplicationDelResponse = 11 + ApplicationModifyDNRequest = 12 + ApplicationModifyDNResponse = 13 + ApplicationCompareRequest = 14 + ApplicationCompareResponse = 15 + ApplicationAbandonRequest = 16 + ApplicationSearchResultReference = 19 + ApplicationExtendedRequest = 23 + ApplicationExtendedResponse = 24 +) + +// ApplicationMap contains human readable descriptions of LDAP Application Codes +var ApplicationMap = map[uint8]string{ + ApplicationBindRequest: "Bind Request", + ApplicationBindResponse: "Bind Response", + ApplicationUnbindRequest: "Unbind Request", + ApplicationSearchRequest: "Search Request", + ApplicationSearchResultEntry: "Search Result Entry", + ApplicationSearchResultDone: "Search Result Done", + ApplicationModifyRequest: "Modify Request", + ApplicationModifyResponse: "Modify Response", + ApplicationAddRequest: "Add Request", + ApplicationAddResponse: "Add Response", + ApplicationDelRequest: "Del Request", + ApplicationDelResponse: "Del Response", + ApplicationModifyDNRequest: "Modify DN Request", + ApplicationModifyDNResponse: "Modify DN Response", + ApplicationCompareRequest: "Compare Request", + ApplicationCompareResponse: "Compare Response", + ApplicationAbandonRequest: "Abandon Request", + ApplicationSearchResultReference: "Search Result Reference", + ApplicationExtendedRequest: "Extended Request", + ApplicationExtendedResponse: "Extended Response", +} + +// Ldap Behera Password Policy Draft 10 (https://tools.ietf.org/html/draft-behera-ldap-password-policy-10) +const ( + BeheraPasswordExpired = 0 + BeheraAccountLocked = 1 + BeheraChangeAfterReset = 2 + BeheraPasswordModNotAllowed = 3 + BeheraMustSupplyOldPassword = 4 + BeheraInsufficientPasswordQuality = 5 + BeheraPasswordTooShort = 6 + BeheraPasswordTooYoung = 7 + BeheraPasswordInHistory = 8 +) + +// BeheraPasswordPolicyErrorMap contains human readable descriptions of Behera Password Policy error codes +var BeheraPasswordPolicyErrorMap = map[int8]string{ + BeheraPasswordExpired: "Password expired", + BeheraAccountLocked: "Account locked", + BeheraChangeAfterReset: "Password must be changed", + BeheraPasswordModNotAllowed: "Policy prevents password modification", + BeheraMustSupplyOldPassword: "Policy requires old password in order to change password", + BeheraInsufficientPasswordQuality: "Password fails quality checks", + BeheraPasswordTooShort: "Password is too short for policy", + BeheraPasswordTooYoung: "Password has been changed too recently", + BeheraPasswordInHistory: "New password is in list of old passwords", +} + +// Adds descriptions to an LDAP Response packet for debugging +func addLDAPDescriptions(packet *ber.Packet) (err error) { + defer func() { + if r := recover(); r != nil { + err = NewError(ErrorDebugging, errors.New("ldap: cannot process packet to add descriptions")) + } + }() + packet.Description = "LDAP Response" + packet.Children[0].Description = "Message ID" + + application := uint8(packet.Children[1].Tag) + packet.Children[1].Description = ApplicationMap[application] + + switch application { + case ApplicationBindRequest: + addRequestDescriptions(packet) + case ApplicationBindResponse: + addDefaultLDAPResponseDescriptions(packet) + case ApplicationUnbindRequest: + addRequestDescriptions(packet) + case ApplicationSearchRequest: + addRequestDescriptions(packet) + case ApplicationSearchResultEntry: + packet.Children[1].Children[0].Description = "Object Name" + packet.Children[1].Children[1].Description = "Attributes" + for _, child := range packet.Children[1].Children[1].Children { + child.Description = "Attribute" + child.Children[0].Description = "Attribute Name" + child.Children[1].Description = "Attribute Values" + for _, grandchild := range child.Children[1].Children { + grandchild.Description = "Attribute Value" + } + } + if len(packet.Children) == 3 { + addControlDescriptions(packet.Children[2]) + } + case ApplicationSearchResultDone: + addDefaultLDAPResponseDescriptions(packet) + case ApplicationModifyRequest: + addRequestDescriptions(packet) + case ApplicationModifyResponse: + case ApplicationAddRequest: + addRequestDescriptions(packet) + case ApplicationAddResponse: + case ApplicationDelRequest: + addRequestDescriptions(packet) + case ApplicationDelResponse: + case ApplicationModifyDNRequest: + addRequestDescriptions(packet) + case ApplicationModifyDNResponse: + case ApplicationCompareRequest: + addRequestDescriptions(packet) + case ApplicationCompareResponse: + case ApplicationAbandonRequest: + addRequestDescriptions(packet) + case ApplicationSearchResultReference: + case ApplicationExtendedRequest: + addRequestDescriptions(packet) + case ApplicationExtendedResponse: + } + + return nil +} + +func addControlDescriptions(packet *ber.Packet) { + packet.Description = "Controls" + for _, child := range packet.Children { + var value *ber.Packet + controlType := "" + child.Description = "Control" + switch len(child.Children) { + case 0: + // at least one child is required for control type + continue + + case 1: + // just type, no criticality or value + controlType = child.Children[0].Value.(string) + child.Children[0].Description = "Control Type (" + ControlTypeMap[controlType] + ")" + + case 2: + controlType = child.Children[0].Value.(string) + child.Children[0].Description = "Control Type (" + ControlTypeMap[controlType] + ")" + // Children[1] could be criticality or value (both are optional) + // duck-type on whether this is a boolean + if _, ok := child.Children[1].Value.(bool); ok { + child.Children[1].Description = "Criticality" + } else { + child.Children[1].Description = "Control Value" + value = child.Children[1] + } + + case 3: + // criticality and value present + controlType = child.Children[0].Value.(string) + child.Children[0].Description = "Control Type (" + ControlTypeMap[controlType] + ")" + child.Children[1].Description = "Criticality" + child.Children[2].Description = "Control Value" + value = child.Children[2] + + default: + // more than 3 children is invalid + continue + } + if value == nil { + continue + } + switch controlType { + case ControlTypePaging: + value.Description += " (Paging)" + if value.Value != nil { + valueChildren := ber.DecodePacket(value.Data.Bytes()) + value.Data.Truncate(0) + value.Value = nil + valueChildren.Children[1].Value = valueChildren.Children[1].Data.Bytes() + value.AppendChild(valueChildren) + } + value.Children[0].Description = "Real Search Control Value" + value.Children[0].Children[0].Description = "Paging Size" + value.Children[0].Children[1].Description = "Cookie" + + case ControlTypeBeheraPasswordPolicy: + value.Description += " (Password Policy - Behera Draft)" + if value.Value != nil { + valueChildren := ber.DecodePacket(value.Data.Bytes()) + value.Data.Truncate(0) + value.Value = nil + value.AppendChild(valueChildren) + } + sequence := value.Children[0] + for _, child := range sequence.Children { + if child.Tag == 0 { + //Warning + warningPacket := child.Children[0] + packet := ber.DecodePacket(warningPacket.Data.Bytes()) + val, ok := packet.Value.(int64) + if ok { + if warningPacket.Tag == 0 { + //timeBeforeExpiration + value.Description += " (TimeBeforeExpiration)" + warningPacket.Value = val + } else if warningPacket.Tag == 1 { + //graceAuthNsRemaining + value.Description += " (GraceAuthNsRemaining)" + warningPacket.Value = val + } + } + } else if child.Tag == 1 { + // Error + packet := ber.DecodePacket(child.Data.Bytes()) + val, ok := packet.Value.(int8) + if !ok { + val = -1 + } + child.Description = "Error" + child.Value = val + } + } + } + } +} + +func addRequestDescriptions(packet *ber.Packet) { + packet.Description = "LDAP Request" + packet.Children[0].Description = "Message ID" + packet.Children[1].Description = ApplicationMap[uint8(packet.Children[1].Tag)] + if len(packet.Children) == 3 { + addControlDescriptions(packet.Children[2]) + } +} + +func addDefaultLDAPResponseDescriptions(packet *ber.Packet) { + 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 { + packet.Children[1].Children[3].Description = "Referral" + } + if len(packet.Children) == 3 { + addControlDescriptions(packet.Children[2]) + } +} + +// DebugBinaryFile reads and prints packets from the given filename +func DebugBinaryFile(fileName string) error { + file, err := ioutil.ReadFile(fileName) + if err != nil { + return NewError(ErrorDebugging, err) + } + ber.PrintBytes(os.Stdout, file, "") + packet := ber.DecodePacket(file) + addLDAPDescriptions(packet) + ber.PrintPacket(packet) + + return nil +} + +var hex = "0123456789abcdef" + +func mustEscape(c byte) bool { + return c > 0x7f || c == '(' || c == ')' || c == '\\' || c == '*' || c == 0 +} + +// EscapeFilter escapes from the provided LDAP filter string the special +// characters in the set `()*\` and those out of the range 0 < c < 0x80, +// as defined in RFC4515. +func EscapeFilter(filter string) string { + escape := 0 + for i := 0; i < len(filter); i++ { + if mustEscape(filter[i]) { + escape++ + } + } + if escape == 0 { + return filter + } + buf := make([]byte, len(filter)+escape*2) + for i, j := 0, 0; i < len(filter); i++ { + c := filter[i] + if mustEscape(c) { + buf[j+0] = '\\' + buf[j+1] = hex[c>>4] + buf[j+2] = hex[c&0xf] + j += 3 + } else { + buf[j] = c + j++ + } + } + return string(buf) +} diff --git a/vendor/gopkg.in/ldap.v2/modify.go b/vendor/gopkg.in/ldap.v2/modify.go new file mode 100644 index 00000000..e4ab6cef --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/modify.go @@ -0,0 +1,170 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// File contains Modify functionality +// +// https://tools.ietf.org/html/rfc4511 +// +// ModifyRequest ::= [APPLICATION 6] SEQUENCE { +// object LDAPDN, +// changes SEQUENCE OF change SEQUENCE { +// operation ENUMERATED { +// add (0), +// delete (1), +// replace (2), +// ... }, +// modification PartialAttribute } } +// +// PartialAttribute ::= SEQUENCE { +// type AttributeDescription, +// vals SET OF value AttributeValue } +// +// AttributeDescription ::= LDAPString +// -- Constrained to +// -- [RFC4512] +// +// AttributeValue ::= OCTET STRING +// + +package ldap + +import ( + "errors" + "log" + + "gopkg.in/asn1-ber.v1" +) + +// Change operation choices +const ( + AddAttribute = 0 + DeleteAttribute = 1 + ReplaceAttribute = 2 +) + +// PartialAttribute for a ModifyRequest as defined in https://tools.ietf.org/html/rfc4511 +type PartialAttribute struct { + // Type is the type of the partial attribute + Type string + // Vals are the values of the partial attribute + Vals []string +} + +func (p *PartialAttribute) encode() *ber.Packet { + seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "PartialAttribute") + seq.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, p.Type, "Type")) + set := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSet, nil, "AttributeValue") + for _, value := range p.Vals { + set.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, value, "Vals")) + } + seq.AppendChild(set) + return seq +} + +// ModifyRequest as defined in https://tools.ietf.org/html/rfc4511 +type ModifyRequest struct { + // DN is the distinguishedName of the directory entry to modify + DN string + // AddAttributes contain the attributes to add + AddAttributes []PartialAttribute + // DeleteAttributes contain the attributes to delete + DeleteAttributes []PartialAttribute + // ReplaceAttributes contain the attributes to replace + ReplaceAttributes []PartialAttribute +} + +// Add inserts the given attribute to the list of attributes to add +func (m *ModifyRequest) Add(attrType string, attrVals []string) { + m.AddAttributes = append(m.AddAttributes, PartialAttribute{Type: attrType, Vals: attrVals}) +} + +// Delete inserts the given attribute to the list of attributes to delete +func (m *ModifyRequest) Delete(attrType string, attrVals []string) { + m.DeleteAttributes = append(m.DeleteAttributes, PartialAttribute{Type: attrType, Vals: attrVals}) +} + +// Replace inserts the given attribute to the list of attributes to replace +func (m *ModifyRequest) Replace(attrType string, attrVals []string) { + m.ReplaceAttributes = append(m.ReplaceAttributes, PartialAttribute{Type: attrType, Vals: attrVals}) +} + +func (m ModifyRequest) encode() *ber.Packet { + request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationModifyRequest, nil, "Modify Request") + request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, m.DN, "DN")) + changes := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Changes") + for _, attribute := range m.AddAttributes { + change := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Change") + change.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(AddAttribute), "Operation")) + change.AppendChild(attribute.encode()) + changes.AppendChild(change) + } + for _, attribute := range m.DeleteAttributes { + change := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Change") + change.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(DeleteAttribute), "Operation")) + change.AppendChild(attribute.encode()) + changes.AppendChild(change) + } + for _, attribute := range m.ReplaceAttributes { + change := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Change") + change.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(ReplaceAttribute), "Operation")) + change.AppendChild(attribute.encode()) + changes.AppendChild(change) + } + request.AppendChild(changes) + return request +} + +// NewModifyRequest creates a modify request for the given DN +func NewModifyRequest( + dn string, +) *ModifyRequest { + return &ModifyRequest{ + DN: dn, + } +} + +// Modify performs the ModifyRequest +func (l *Conn) Modify(modifyRequest *ModifyRequest) error { + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) + packet.AppendChild(modifyRequest.encode()) + + l.Debug.PrintPacket(packet) + + msgCtx, err := l.sendMessage(packet) + if err != nil { + return err + } + defer l.finishMessage(msgCtx) + + l.Debug.Printf("%d: waiting for response", msgCtx.id) + packetResponse, ok := <-msgCtx.responses + if !ok { + return NewError(ErrorNetwork, errors.New("ldap: response channel closed")) + } + packet, err = packetResponse.ReadPacket() + l.Debug.Printf("%d: got response %p", msgCtx.id, packet) + if err != nil { + return err + } + + if l.Debug { + if err := addLDAPDescriptions(packet); err != nil { + return err + } + ber.PrintPacket(packet) + } + + if packet.Children[1].Tag == ApplicationModifyResponse { + 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", msgCtx.id) + return nil +} diff --git a/vendor/gopkg.in/ldap.v2/passwdmodify.go b/vendor/gopkg.in/ldap.v2/passwdmodify.go new file mode 100644 index 00000000..7d8246fd --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/passwdmodify.go @@ -0,0 +1,148 @@ +// This file contains the password modify extended operation as specified in rfc 3062 +// +// https://tools.ietf.org/html/rfc3062 +// + +package ldap + +import ( + "errors" + "fmt" + + "gopkg.in/asn1-ber.v1" +) + +const ( + passwordModifyOID = "1.3.6.1.4.1.4203.1.11.1" +) + +// PasswordModifyRequest implements the Password Modify Extended Operation as defined in https://www.ietf.org/rfc/rfc3062.txt +type PasswordModifyRequest struct { + // UserIdentity is an optional string representation of the user associated with the request. + // This string may or may not be an LDAPDN [RFC2253]. + // If no UserIdentity field is present, the request acts up upon the password of the user currently associated with the LDAP session + UserIdentity string + // OldPassword, if present, contains the user's current password + OldPassword string + // NewPassword, if present, contains the desired password for this user + NewPassword string +} + +// PasswordModifyResult holds the server response to a PasswordModifyRequest +type PasswordModifyResult struct { + // GeneratedPassword holds a password generated by the server, if present + GeneratedPassword string +} + +func (r *PasswordModifyRequest) encode() (*ber.Packet, error) { + request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationExtendedRequest, nil, "Password Modify Extended Operation") + request.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, passwordModifyOID, "Extended Request Name: Password Modify OID")) + extendedRequestValue := ber.Encode(ber.ClassContext, ber.TypePrimitive, 1, nil, "Extended Request Value: Password Modify Request") + passwordModifyRequestValue := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Password Modify Request") + if r.UserIdentity != "" { + passwordModifyRequestValue.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 0, r.UserIdentity, "User Identity")) + } + if r.OldPassword != "" { + passwordModifyRequestValue.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 1, r.OldPassword, "Old Password")) + } + if r.NewPassword != "" { + passwordModifyRequestValue.AppendChild(ber.NewString(ber.ClassContext, ber.TypePrimitive, 2, r.NewPassword, "New Password")) + } + + extendedRequestValue.AppendChild(passwordModifyRequestValue) + request.AppendChild(extendedRequestValue) + + return request, nil +} + +// NewPasswordModifyRequest creates a new PasswordModifyRequest +// +// According to the RFC 3602: +// userIdentity is a string representing the user associated with the request. +// This string may or may not be an LDAPDN (RFC 2253). +// If userIdentity is empty then the operation will act on the user associated +// with the session. +// +// oldPassword is the current user's password, it can be empty or it can be +// needed depending on the session user access rights (usually an administrator +// can change a user's password without knowing the current one) and the +// password policy (see pwdSafeModify password policy's attribute) +// +// newPassword is the desired user's password. If empty the server can return +// an error or generate a new password that will be available in the +// PasswordModifyResult.GeneratedPassword +// +func NewPasswordModifyRequest(userIdentity string, oldPassword string, newPassword string) *PasswordModifyRequest { + return &PasswordModifyRequest{ + UserIdentity: userIdentity, + OldPassword: oldPassword, + NewPassword: newPassword, + } +} + +// PasswordModify performs the modification request +func (l *Conn) PasswordModify(passwordModifyRequest *PasswordModifyRequest) (*PasswordModifyResult, error) { + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) + + encodedPasswordModifyRequest, err := passwordModifyRequest.encode() + if err != nil { + return nil, err + } + packet.AppendChild(encodedPasswordModifyRequest) + + l.Debug.PrintPacket(packet) + + msgCtx, err := l.sendMessage(packet) + if err != nil { + return nil, err + } + defer l.finishMessage(msgCtx) + + result := &PasswordModifyResult{} + + l.Debug.Printf("%d: waiting for response", msgCtx.id) + packetResponse, ok := <-msgCtx.responses + if !ok { + return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) + } + packet, err = packetResponse.ReadPacket() + l.Debug.Printf("%d: got response %p", msgCtx.id, packet) + if err != nil { + return nil, err + } + + if packet == nil { + return nil, NewError(ErrorNetwork, errors.New("ldap: could not retrieve message")) + } + + if l.Debug { + if err := addLDAPDescriptions(packet); err != nil { + return nil, err + } + ber.PrintPacket(packet) + } + + if packet.Children[1].Tag == ApplicationExtendedResponse { + resultCode, resultDescription := getLDAPResultCode(packet) + if resultCode != 0 { + return nil, NewError(resultCode, errors.New(resultDescription)) + } + } else { + return nil, NewError(ErrorUnexpectedResponse, fmt.Errorf("Unexpected Response: %d", packet.Children[1].Tag)) + } + + extendedResponse := packet.Children[1] + for _, child := range extendedResponse.Children { + if child.Tag == 11 { + passwordModifyResponseValue := ber.DecodePacket(child.Data.Bytes()) + if len(passwordModifyResponseValue.Children) == 1 { + if passwordModifyResponseValue.Children[0].Tag == 0 { + result.GeneratedPassword = ber.DecodeString(passwordModifyResponseValue.Children[0].Data.Bytes()) + } + } + } + } + + return result, nil +} diff --git a/vendor/gopkg.in/ldap.v2/search.go b/vendor/gopkg.in/ldap.v2/search.go new file mode 100644 index 00000000..2a99894c --- /dev/null +++ b/vendor/gopkg.in/ldap.v2/search.go @@ -0,0 +1,450 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// File contains Search functionality +// +// https://tools.ietf.org/html/rfc4511 +// +// SearchRequest ::= [APPLICATION 3] SEQUENCE { +// baseObject LDAPDN, +// scope ENUMERATED { +// baseObject (0), +// singleLevel (1), +// wholeSubtree (2), +// ... }, +// derefAliases ENUMERATED { +// neverDerefAliases (0), +// derefInSearching (1), +// derefFindingBaseObj (2), +// derefAlways (3) }, +// sizeLimit INTEGER (0 .. maxInt), +// timeLimit INTEGER (0 .. maxInt), +// typesOnly BOOLEAN, +// filter Filter, +// attributes AttributeSelection } +// +// AttributeSelection ::= SEQUENCE OF selector LDAPString +// -- The LDAPString is constrained to +// -- in Section 4.5.1.8 +// +// Filter ::= CHOICE { +// and [0] SET SIZE (1..MAX) OF filter Filter, +// or [1] SET SIZE (1..MAX) OF filter Filter, +// not [2] Filter, +// equalityMatch [3] AttributeValueAssertion, +// substrings [4] SubstringFilter, +// greaterOrEqual [5] AttributeValueAssertion, +// lessOrEqual [6] AttributeValueAssertion, +// present [7] AttributeDescription, +// approxMatch [8] AttributeValueAssertion, +// extensibleMatch [9] MatchingRuleAssertion, +// ... } +// +// SubstringFilter ::= SEQUENCE { +// type AttributeDescription, +// substrings SEQUENCE SIZE (1..MAX) OF substring CHOICE { +// initial [0] AssertionValue, -- can occur at most once +// any [1] AssertionValue, +// final [2] AssertionValue } -- can occur at most once +// } +// +// MatchingRuleAssertion ::= SEQUENCE { +// matchingRule [1] MatchingRuleId OPTIONAL, +// type [2] AttributeDescription OPTIONAL, +// matchValue [3] AssertionValue, +// dnAttributes [4] BOOLEAN DEFAULT FALSE } +// +// + +package ldap + +import ( + "errors" + "fmt" + "sort" + "strings" + + "gopkg.in/asn1-ber.v1" +) + +// scope choices +const ( + ScopeBaseObject = 0 + ScopeSingleLevel = 1 + ScopeWholeSubtree = 2 +) + +// ScopeMap contains human readable descriptions of scope choices +var ScopeMap = map[int]string{ + ScopeBaseObject: "Base Object", + ScopeSingleLevel: "Single Level", + ScopeWholeSubtree: "Whole Subtree", +} + +// derefAliases +const ( + NeverDerefAliases = 0 + DerefInSearching = 1 + DerefFindingBaseObj = 2 + DerefAlways = 3 +) + +// DerefMap contains human readable descriptions of derefAliases choices +var DerefMap = map[int]string{ + NeverDerefAliases: "NeverDerefAliases", + DerefInSearching: "DerefInSearching", + DerefFindingBaseObj: "DerefFindingBaseObj", + 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, + } +} + +// Entry represents a single search result entry +type Entry struct { + // DN is the distinguished name of the entry + DN string + // Attributes are the returned attributes for the entry + Attributes []*EntryAttribute +} + +// GetAttributeValues returns the values for the named attribute, or an empty list +func (e *Entry) GetAttributeValues(attribute string) []string { + for _, attr := range e.Attributes { + if attr.Name == attribute { + return attr.Values + } + } + return []string{} +} + +// GetRawAttributeValues returns the byte values for the named attribute, or an empty list +func (e *Entry) GetRawAttributeValues(attribute string) [][]byte { + for _, attr := range e.Attributes { + if attr.Name == attribute { + return attr.ByteValues + } + } + return [][]byte{} +} + +// GetAttributeValue returns the first value for the named attribute, or "" +func (e *Entry) GetAttributeValue(attribute string) string { + values := e.GetAttributeValues(attribute) + if len(values) == 0 { + return "" + } + return values[0] +} + +// GetRawAttributeValue returns the first value for the named attribute, or an empty slice +func (e *Entry) GetRawAttributeValue(attribute string) []byte { + values := e.GetRawAttributeValues(attribute) + if len(values) == 0 { + return []byte{} + } + return values[0] +} + +// Print outputs a human-readable description +func (e *Entry) Print() { + fmt.Printf("DN: %s\n", e.DN) + for _, attr := range e.Attributes { + attr.Print() + } +} + +// PrettyPrint outputs a human-readable description indenting +func (e *Entry) PrettyPrint(indent int) { + fmt.Printf("%sDN: %s\n", strings.Repeat(" ", indent), e.DN) + for _, attr := range e.Attributes { + attr.PrettyPrint(indent + 2) + } +} + +// 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, + } +} + +// EntryAttribute holds a single attribute +type EntryAttribute struct { + // Name is the name of the attribute + Name string + // Values contain the string values of the attribute + Values []string + // ByteValues contain the raw values of the attribute + ByteValues [][]byte +} + +// Print outputs a human-readable description +func (e *EntryAttribute) Print() { + fmt.Printf("%s: %s\n", e.Name, e.Values) +} + +// PrettyPrint outputs a human-readable description with indenting +func (e *EntryAttribute) PrettyPrint(indent int) { + fmt.Printf("%s%s: %s\n", strings.Repeat(" ", indent), e.Name, e.Values) +} + +// SearchResult holds the server's response to a search request +type SearchResult struct { + // Entries are the returned entries + Entries []*Entry + // Referrals are the returned referrals + Referrals []string + // Controls are the returned controls + Controls []Control +} + +// Print outputs a human-readable description +func (s *SearchResult) Print() { + for _, entry := range s.Entries { + entry.Print() + } +} + +// PrettyPrint outputs a human-readable description with indenting +func (s *SearchResult) PrettyPrint(indent int) { + for _, entry := range s.Entries { + entry.PrettyPrint(indent) + } +} + +// SearchRequest represents a search request to send to the server +type SearchRequest struct { + BaseDN string + Scope int + DerefAliases int + SizeLimit int + TimeLimit int + TypesOnly bool + Filter string + Attributes []string + Controls []Control +} + +func (s *SearchRequest) encode() (*ber.Packet, error) { + request := ber.Encode(ber.ClassApplication, ber.TypeConstructed, ApplicationSearchRequest, nil, "Search Request") + request.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, s.BaseDN, "Base DN")) + request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(s.Scope), "Scope")) + request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagEnumerated, uint64(s.DerefAliases), "Deref Aliases")) + request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, uint64(s.SizeLimit), "Size Limit")) + request.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, uint64(s.TimeLimit), "Time Limit")) + request.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, s.TypesOnly, "Types Only")) + // compile and encode filter + filterPacket, err := CompileFilter(s.Filter) + if err != nil { + return nil, err + } + request.AppendChild(filterPacket) + // encode attributes + attributesPacket := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Attributes") + for _, attribute := range s.Attributes { + attributesPacket.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, attribute, "Attribute")) + } + request.AppendChild(attributesPacket) + return request, nil +} + +// NewSearchRequest creates a new search request +func NewSearchRequest( + BaseDN string, + Scope, DerefAliases, SizeLimit, TimeLimit int, + TypesOnly bool, + Filter string, + Attributes []string, + Controls []Control, +) *SearchRequest { + return &SearchRequest{ + BaseDN: BaseDN, + Scope: Scope, + DerefAliases: DerefAliases, + SizeLimit: SizeLimit, + TimeLimit: TimeLimit, + TypesOnly: TypesOnly, + Filter: Filter, + Attributes: Attributes, + Controls: Controls, + } +} + +// 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) { + 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 + } + + searchResult := new(SearchResult) + for { + result, err := l.Search(searchRequest) + l.Debug.Printf("Looking for Paging Control...") + if err != nil { + return searchResult, err + } + if result == nil { + return searchResult, NewError(ErrorNetwork, errors.New("ldap: packet not received")) + } + + for _, entry := range result.Entries { + searchResult.Entries = append(searchResult.Entries, entry) + } + for _, referral := range result.Referrals { + searchResult.Referrals = append(searchResult.Referrals, referral) + } + for _, control := range result.Controls { + searchResult.Controls = append(searchResult.Controls, control) + } + + l.Debug.Printf("Looking for Paging Control...") + pagingResult := FindControl(result.Controls, ControlTypePaging) + if pagingResult == nil { + pagingControl = nil + l.Debug.Printf("Could not find paging control. Breaking...") + break + } + + cookie := pagingResult.(*ControlPaging).Cookie + if len(cookie) == 0 { + pagingControl = nil + l.Debug.Printf("Could not find cookie. Breaking...") + break + } + pagingControl.SetCookie(cookie) + } + + if pagingControl != nil { + l.Debug.Printf("Abandoning Paging...") + pagingControl.PagingSize = 0 + l.Search(searchRequest) + } + + return searchResult, nil +} + +// Search performs the given search request +func (l *Conn) Search(searchRequest *SearchRequest) (*SearchResult, error) { + packet := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "LDAP Request") + packet.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, l.nextMessageID(), "MessageID")) + // encode search request + encodedSearchRequest, err := searchRequest.encode() + if err != nil { + return nil, err + } + packet.AppendChild(encodedSearchRequest) + // encode search controls + if searchRequest.Controls != nil { + packet.AppendChild(encodeControls(searchRequest.Controls)) + } + + l.Debug.PrintPacket(packet) + + msgCtx, err := l.sendMessage(packet) + if err != nil { + return nil, err + } + defer l.finishMessage(msgCtx) + + result := &SearchResult{ + Entries: make([]*Entry, 0), + Referrals: make([]string, 0), + Controls: make([]Control, 0)} + + foundSearchResultDone := false + for !foundSearchResultDone { + l.Debug.Printf("%d: waiting for response", msgCtx.id) + packetResponse, ok := <-msgCtx.responses + if !ok { + return nil, NewError(ErrorNetwork, errors.New("ldap: response channel closed")) + } + packet, err = packetResponse.ReadPacket() + l.Debug.Printf("%d: got response %p", msgCtx.id, packet) + if err != nil { + return nil, err + } + + if l.Debug { + if err := addLDAPDescriptions(packet); err != nil { + return nil, err + } + ber.PrintPacket(packet) + } + + switch packet.Children[1].Tag { + case 4: + entry := new(Entry) + entry.DN = packet.Children[1].Children[0].Value.(string) + for _, child := range packet.Children[1].Children[1].Children { + attr := new(EntryAttribute) + attr.Name = child.Children[0].Value.(string) + for _, value := range child.Children[1].Children { + attr.Values = append(attr.Values, value.Value.(string)) + attr.ByteValues = append(attr.ByteValues, value.ByteValue) + } + entry.Attributes = append(entry.Attributes, attr) + } + result.Entries = append(result.Entries, entry) + case 5: + resultCode, resultDescription := getLDAPResultCode(packet) + if resultCode != 0 { + return result, NewError(resultCode, errors.New(resultDescription)) + } + if len(packet.Children) == 3 { + for _, child := range packet.Children[2].Children { + result.Controls = append(result.Controls, DecodeControl(child)) + } + } + foundSearchResultDone = true + case 19: + result.Referrals = append(result.Referrals, packet.Children[1].Children[0].Value.(string)) + } + } + l.Debug.Printf("%d: returning", msgCtx.id) + return result, nil +}