1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-20 21:59:42 +02:00

Merge pull request #167 from documize/ldap

Native LDAP and Active Directory sync and authentication
This commit is contained in:
Saul S 2018-09-08 12:52:25 +01:00 committed by GitHub
commit 53e4861ded
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
109 changed files with 7003 additions and 4943 deletions

26
Gopkg.lock generated
View file

@ -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

View file

@ -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"

View file

@ -58,9 +58,9 @@ Space view.
## Latest version
[Community edition: v1.69.2](https://github.com/documize/community/releases)
[Community edition: v1.70.0](https://github.com/documize/community/releases)
[Enterprise edition: v1.71.2](https://documize.com/downloads)
[Enterprise edition: v1.72.0](https://documize.com/downloads)
## OS support
@ -99,6 +99,10 @@ Coming soon, PostgreSQL and Microsoft SQL Server database support.
Besides email/password login, you can also leverage the following options.
### LDAP / Active Directory
Connect and sync Documize with any LDAP v3 compliant provider including Microsoft Active Directory.
### Keycloak Integration
Documize provides out-of-the-box integration with [Redhat Keycloak](http://www.keycloak.org) for open source identity and access management.

90
domain/auth/add.go Normal file
View file

@ -0,0 +1,90 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. 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 <sales@documize.com>.
//
// https://documize.com
package auth
import (
"database/sql"
"github.com/documize/community/core/env"
"github.com/documize/community/core/uniqueid"
"github.com/documize/community/domain"
usr "github.com/documize/community/domain/user"
"github.com/documize/community/model/account"
"github.com/documize/community/model/user"
)
// AddExternalUser method to setup user account in Documize using Keycloak/LDAP provided user data.
func AddExternalUser(ctx domain.RequestContext, rt *env.Runtime, store *domain.Store, u user.User, addSpace bool) (nu user.User, err error) {
// only create account if not dupe
addUser := true
addAccount := true
var userID string
userDupe, err := store.User.GetByEmail(ctx, u.Email)
if err != nil && err != sql.ErrNoRows {
return
}
if u.Email == userDupe.Email {
addUser = false
userID = userDupe.RefID
}
ctx.Transaction, err = rt.Db.Beginx()
if err != nil {
return
}
if addUser {
userID = uniqueid.Generate()
u.RefID = userID
err = store.User.Add(ctx, u)
if err != nil {
ctx.Transaction.Rollback()
return
}
} else {
usr.AttachUserAccounts(ctx, *store, ctx.OrgID, &userDupe)
for _, a := range userDupe.Accounts {
if a.OrgID == ctx.OrgID {
addAccount = false
break
}
}
}
// set up user account for the org
if addAccount {
var a account.Account
a.UserID = userID
a.OrgID = ctx.OrgID
a.Editor = addSpace
a.Admin = false
accountID := uniqueid.Generate()
a.RefID = accountID
a.Active = true
err = store.Account.Add(ctx, a)
if err != nil {
ctx.Transaction.Rollback()
return
}
}
ctx.Transaction.Commit()
nu, err = store.User.Get(ctx, userID)
return
}

View file

@ -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
@ -129,7 +130,7 @@ func (h *Handler) Sync(w http.ResponseWriter, r *http.Request) {
if len(u.Email) == 0 {
missing++
} else {
err = addUser(ctx, h.Runtime, h.Store, u, c.DefaultPermissionAddSpace)
_, err = auth.AddExternalUser(ctx, h.Runtime, h.Store, u, c.DefaultPermissionAddSpace)
}
}
@ -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")
@ -239,7 +240,7 @@ func (h *Handler) Authenticate(w http.ResponseWriter, r *http.Request) {
u.Salt = secrets.GenerateSalt()
u.Password = secrets.GeneratePassword(secrets.GenerateRandomPassword(), u.Salt)
err = addUser(ctx, h.Runtime, h.Store, u, ac.DefaultPermissionAddSpace)
u, err = auth.AddExternalUser(ctx, h.Runtime, h.Store, u, ac.DefaultPermissionAddSpace)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)

View file

@ -13,7 +13,6 @@ package keycloak
import (
"bytes"
"database/sql"
"encoding/json"
"fmt"
"io/ioutil"
@ -22,18 +21,14 @@ import (
"strconv"
"strings"
"github.com/documize/community/core/env"
"github.com/documize/community/core/stringutil"
"github.com/documize/community/core/uniqueid"
"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 +66,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 +109,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")
@ -135,70 +130,3 @@ func Fetch(c keycloakConfig) (users []user.User, err error) {
return users, nil
}
// Helper method to setup user account in Documize using Keycloak provided user data.
func addUser(ctx domain.RequestContext, rt *env.Runtime, store *domain.Store, u user.User, addSpace bool) (err error) {
// only create account if not dupe
addUser := true
addAccount := true
var userID string
userDupe, err := store.User.GetByEmail(ctx, u.Email)
if err != nil && err != sql.ErrNoRows {
return err
}
if u.Email == userDupe.Email {
addUser = false
userID = userDupe.RefID
}
ctx.Transaction, err = rt.Db.Beginx()
if err != nil {
return err
}
if addUser {
userID = uniqueid.Generate()
u.RefID = userID
err = store.User.Add(ctx, u)
if err != nil {
ctx.Transaction.Rollback()
return err
}
} else {
usr.AttachUserAccounts(ctx, *store, ctx.OrgID, &userDupe)
for _, a := range userDupe.Accounts {
if a.OrgID == ctx.OrgID {
addAccount = false
break
}
}
}
// set up user account for the org
if addAccount {
var a account.Account
a.UserID = userID
a.OrgID = ctx.OrgID
a.Editor = addSpace
a.Admin = false
accountID := uniqueid.Generate()
a.RefID = accountID
a.Active = true
err = store.Account.Add(ctx, a)
if err != nil {
ctx.Transaction.Rollback()
return err
}
}
ctx.Transaction.Commit()
u, err = store.User.Get(ctx, userID)
return err
}

View file

@ -1,52 +0,0 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. 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 <sales@documize.com>.
//
// 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"`
}

123
domain/auth/ldap/ad_test.go Normal file
View file

@ -0,0 +1,123 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. 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 <sales@documize.com>.
//
// https://documize.com
package ldap
import (
"testing"
lm "github.com/documize/community/model/auth"
)
// Works against AD server in Azure configured using:
//
// https://auth0.com/docs/connector/test-dc
//
// Ensure VM network settings open up ports 389 and 636.
var testConfigPublicAD = lm.LDAPConfig{
ServerType: lm.ServerTypeAD,
ServerHost: "documize-ad.eastus.cloudapp.azure.com",
ServerPort: 389,
EncryptionType: lm.EncryptionTypeNone,
BaseDN: "DC=mycompany,DC=local",
BindDN: "CN=Mr Manager,CN=Users,DC=mycompany,DC=local",
BindPassword: "Pass@word1!",
UserFilter: "(|(objectCategory=person)(objectClass=user)(objectClass=inetOrgPerson))",
GroupFilter: "(|(cn=Accounting)(cn=IT))",
AttributeUserRDN: "sAMAccountName",
AttributeUserFirstname: "givenName",
AttributeUserLastname: "sn",
AttributeUserEmail: "mail",
AttributeUserDisplayName: "",
AttributeUserGroupName: "",
AttributeGroupMember: "member",
}
func TestUserFilter_PublicAD(t *testing.T) {
e, err := executeUserFilter(testConfigPublicAD)
if err != nil {
t.Error("unable to exeucte user filter", err.Error())
return
}
if len(e) == 0 {
t.Error("Received zero user LDAP search entries")
return
}
t.Logf("LDAP search entries found: %d", len(e))
for _, u := range e {
t.Logf("[%s] %s (%s %s) @ %s\n",
u.RemoteID, u.CN, u.Firstname, u.Lastname, u.Email)
}
}
func TestGroupFilter_PublicAD(t *testing.T) {
e, err := executeGroupFilter(testConfigPublicAD)
if err != nil {
t.Error("unable to exeucte group filter", err.Error())
return
}
if len(e) == 0 {
t.Error("Received zero group LDAP search entries")
return
}
t.Logf("LDAP group search entries found: %d", len(e))
for _, u := range e {
t.Logf("[%s] %s (%s %s) @ %s\n",
u.RemoteID, u.CN, u.Firstname, u.Lastname, u.Email)
}
}
func TestAuthenticate_PublicAD(t *testing.T) {
l, err := connect(testConfigPublicAD)
if err != nil {
t.Error("Error: unable to dial LDAP server: ", err.Error())
return
}
defer l.Close()
user, ok, err := authenticate(l, testConfigPublicAD, "bob.johnson", "Pass@word1!")
if err != nil {
t.Error("error during LDAP authentication: ", err.Error())
return
}
if !ok {
t.Error("failed LDAP authentication")
return
}
t.Log("Authenticated", user.Email)
}
func TestNotAuthenticate_PublicAD(t *testing.T) {
l, err := connect(testConfigPublicAD)
if err != nil {
t.Error("Error: unable to dial LDAP server: ", err.Error())
return
}
defer l.Close()
_, ok, err := authenticate(l, testConfigPublicAD, "junk", "junk")
if err != nil {
t.Error("error during LDAP authentication: ", err.Error())
return
}
if ok {
t.Error("incorrect LDAP authentication")
return
}
t.Log("Not authenticated")
}

View file

@ -0,0 +1,373 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. 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 <sales@documize.com>.
//
// https://documize.com
package ldap
import (
"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/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"
"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
}
// Preview connects to LDAP using paylaod and returns first 50 users.
func (h *Handler) Preview(w http.ResponseWriter, r *http.Request) {
h.Runtime.Log.Info("Sync'ing with LDAP")
ctx := domain.GetRequestContext(r)
if !ctx.Administrator {
response.WriteForbiddenError(w)
return
}
var result struct {
Message string `json:"message"`
IsError bool `json:"isError"`
Users []user.User `json:"users"`
Count int `json:"count"`
}
result.IsError = true
result.Users = []user.User{}
// Read the request.
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
result.Message = "Error: unable read request body"
result.IsError = true
response.WriteJSON(w, result)
h.Runtime.Log.Error(result.Message, err)
return
}
// Decode LDAP config.
c := lm.LDAPConfig{}
err = json.Unmarshal(body, &c)
if err != nil {
result.Message = "Error: unable read LDAP configuration payload"
result.IsError = true
response.WriteJSON(w, result)
h.Runtime.Log.Error(result.Message, err)
return
}
if c.ServerPort == 0 && len(c.ServerHost) == 0 {
result.Message = "Missing LDAP server details"
result.IsError = true
response.WriteJSON(w, result)
return
}
if len(c.BindDN) == 0 && len(c.BindPassword) == 0 {
result.Message = "Missing LDAP bind credentials"
result.IsError = true
response.WriteJSON(w, result)
return
}
if len(c.UserFilter) == 0 && len(c.GroupFilter) == 0 {
result.Message = "Missing LDAP search filters"
result.IsError = true
response.WriteJSON(w, result)
return
}
h.Runtime.Log.Info("Fetching LDAP users")
users, err := fetchUsers(c)
if err != nil {
result.Message = "Error: unable fetch users from LDAP"
result.IsError = true
response.WriteJSON(w, result)
h.Runtime.Log.Error(result.Message, err)
return
}
result.IsError = false
result.Message = fmt.Sprintf("Previewing LDAP, found %d users", len(users))
result.Count = len(users)
result.Users = users
// Preview does not require more than 50 users.
if len(users) > 50 {
result.Users = users[:50]
}
h.Runtime.Log.Info(result.Message)
response.WriteJSON(w, result)
}
// 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"`
}
result.IsError = true
result.Message = "Unable to connect to LDAP"
// 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
}
// Get 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
}
// Get user list from LDAP.
ldapUsers, err := fetchUsers(c)
if err != nil {
result.Message = "Error: unable to fetch LDAP users: " + err.Error()
result.IsError = true
response.WriteJSON(w, result)
h.Runtime.Log.Error(result.Message, err)
return
}
// Get 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(ldapUsers, func(i, j int) bool { return ldapUsers[i].Email < ldapUsers[j].Email })
sort.Slice(dmzUsers, func(i, j int) bool { return dmzUsers[i].Email < dmzUsers[j].Email })
insert := []user.User{}
for _, k := range ldapUsers {
exists := false
for _, d := range dmzUsers {
if k.Email == d.Email {
exists = true
}
}
if !exists {
insert = append(insert, k)
}
}
// Track the number of LDAP users with missing data.
missing := 0
// Insert new users into Documize
for _, u := range insert {
if len(u.Email) == 0 {
missing++
} else {
_, err = auth.AddExternalUser(ctx, h.Runtime, h.Store, u, c.DefaultPermissionAddSpace)
}
}
result.IsError = false
result.Message = "Sync complete with LDAP server"
result.Message = fmt.Sprintf(
"LDAP sync found %d users, %d new users added, %d users with missing data ignored",
len(ldapUsers), len(insert), missing)
h.Runtime.Log.Info(result.Message)
response.WriteJSON(w, result)
}
// Authenticate checks LDAP authentication credentials.
func (h *Handler) Authenticate(w http.ResponseWriter, r *http.Request) {
method := "ldap.authenticate"
ctx := domain.GetRequestContext(r)
// check for http header
authHeader := r.Header.Get("Authorization")
if len(authHeader) == 0 {
response.WriteBadRequestError(w, method, "Missing Authorization header")
return
}
// decode what we received
data := strings.Replace(authHeader, "Basic ", "", 1)
decodedBytes, err := secrets.DecodeBase64([]byte(data))
if err != nil {
response.WriteBadRequestError(w, method, "Unable to decode authentication token")
h.Runtime.Log.Error("decode auth header", err)
return
}
decoded := string(decodedBytes)
// check that we have domain:username:password (but allow for : in password field!)
credentials := strings.SplitN(decoded, ":", 3)
if len(credentials) != 3 {
response.WriteBadRequestError(w, method, "Bad authentication token, expecting domain:username:password")
h.Runtime.Log.Error("bad auth token", err)
return
}
dom := strings.TrimSpace(strings.ToLower(credentials[0]))
username := strings.TrimSpace(strings.ToLower(credentials[1]))
password := credentials[2]
// Check for required fields.
if len(username) == 0 || len(password) == 0 {
response.WriteUnauthorizedError(w)
return
}
dom = h.Store.Organization.CheckDomain(ctx, dom) // TODO optimize by removing this once js allows empty domains
h.Runtime.Log.Info("LDAP login request " + username + " @ " + dom)
// Get the org and it's associated LDAP config.
org, err := h.Store.Organization.GetOrganizationByDomain(dom)
if err != nil {
response.WriteUnauthorizedError(w)
h.Runtime.Log.Error("bad auth organization", err)
return
}
lc := lm.LDAPConfig{}
err = json.Unmarshal([]byte(org.AuthConfig), &lc)
if err != nil {
response.WriteBadRequestError(w, method, "unable to read LDAP config during authorization")
h.Runtime.Log.Error(method, err)
return
}
ctx.OrgID = org.RefID
l, err := connect(lc)
if err != nil {
response.WriteBadRequestError(w, method, "unable to dial LDAP server")
h.Runtime.Log.Error(method, err)
return
}
defer l.Close()
lu, ok, err := authenticate(l, lc, username, password)
if err != nil {
response.WriteBadRequestError(w, method, "error during LDAP authentication")
h.Runtime.Log.Error(method, err)
return
}
if !ok {
response.WriteUnauthorizedError(w)
return
}
h.Runtime.Log.Info("LDAP logon completed " + lu.Email)
u, err := h.Store.User.GetByDomain(ctx, dom, lu.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("Adding new LDAP user " + lu.Email + " @ " + dom)
u = convertUser(lc, lu)
u.Salt = secrets.GenerateSalt()
u.Password = secrets.GeneratePassword(secrets.GenerateRandomPassword(), u.Salt)
u, err = auth.AddExternalUser(ctx, h.Runtime, h.Store, u, lc.DefaultPermissionAddSpace)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
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, dom)
authModel.User = u
response.WriteJSON(w, authModel)
}

297
domain/auth/ldap/ldap.go Normal file
View file

@ -0,0 +1,297 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. 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 <sales@documize.com>.
//
// https://documize.com
package ldap
import (
"crypto/tls"
"fmt"
"math"
"strings"
"github.com/documize/community/core/stringutil"
lm "github.com/documize/community/model/auth"
"github.com/documize/community/model/user"
"github.com/pkg/errors"
ld "gopkg.in/ldap.v2"
)
// Connect establishes connection to LDAP server.
func connect(c lm.LDAPConfig) (l *ld.Conn, err error) {
address := fmt.Sprintf("%s:%d", c.ServerHost, c.ServerPort)
fmt.Println("Connecting to LDAP server", address)
l, err = ld.Dial("tcp", address)
if err != nil {
err = errors.Wrap(err, "unable to dial LDAP server")
return
}
if c.EncryptionType == lm.EncryptionTypeStartTLS {
fmt.Println("Using StartTLS with LDAP server")
err = l.StartTLS(&tls.Config{InsecureSkipVerify: true})
if err != nil {
err = errors.Wrap(err, "unable to startTLS with LDAP server")
return
}
}
return
}
// Authenticate user against LDAP provider.
func authenticate(l *ld.Conn, c lm.LDAPConfig, username, pwd string) (lu lm.LDAPUser, success bool, err error) {
success = false
err = nil
userAttrs := c.GetUserFilterAttributes()
filter := fmt.Sprintf("(%s=%s)", c.AttributeUserRDN, username)
searchRequest := ld.NewSearchRequest(
c.BaseDN,
ld.ScopeWholeSubtree, ld.NeverDerefAliases, 0, 0, false,
filter,
userAttrs,
nil,
)
err = l.Bind(c.BindDN, c.BindPassword)
if err != nil {
errors.Wrap(err, "unable to bind admin user")
return
}
sr, err := l.Search(searchRequest)
if err != nil {
err = errors.Wrap(err, "unable to execute LDAP search during authentication")
return
}
if len(sr.Entries) == 0 {
err = errors.Wrap(err, "user not found during LDAP authentication")
return
}
if len(sr.Entries) != 1 {
err = errors.Wrap(err, "dupe users found during LDAP authentication")
return
}
userdn := sr.Entries[0].DN
// Bind as the user to verify their password
err = l.Bind(userdn, pwd)
if err != nil {
return
}
lu = extractUser(c, sr.Entries[0])
success = true
return
}
// ExecuteUserFilter returns all matching LDAP users.
func executeUserFilter(c lm.LDAPConfig) (u []lm.LDAPUser, err error) {
l, err := connect(c)
if err != nil {
err = errors.Wrap(err, "unable to dial LDAP server")
return
}
defer l.Close()
// Authenticate with LDAP server using admin credentials.
err = l.Bind(c.BindDN, c.BindPassword)
if err != nil {
errors.Wrap(err, "unable to bind admin user")
return
}
searchRequest := ld.NewSearchRequest(
c.BaseDN,
ld.ScopeWholeSubtree, ld.NeverDerefAliases, math.MaxInt32, 0, false,
c.UserFilter,
c.GetUserFilterAttributes(),
nil,
)
sr, err := l.SearchWithPaging(searchRequest, lm.MaxPageSize)
if err != nil {
errors.Wrap(err, "unable to execute directory search for user filter "+c.UserFilter)
return
}
for _, e := range sr.Entries {
u = append(u, extractUser(c, e))
}
return
}
// ExecuteGroupFilter returns all matching LDAP users that are paft of specified groups.
func executeGroupFilter(c lm.LDAPConfig) (u []lm.LDAPUser, err error) {
l, err := connect(c)
if err != nil {
err = errors.Wrap(err, "unable to dial LDAP server")
return
}
defer l.Close()
// Authenticate with LDAP server using admin credentials.
err = l.Bind(c.BindDN, c.BindPassword)
if err != nil {
errors.Wrap(err, "unable to bind admin user")
return
}
searchRequest := ld.NewSearchRequest(
c.BaseDN,
ld.ScopeWholeSubtree, ld.NeverDerefAliases, math.MaxInt32, 0, false,
c.GroupFilter,
c.GetGroupFilterAttributes(),
nil,
)
sr, err := l.SearchWithPaging(searchRequest, lm.MaxPageSize)
if err != nil {
errors.Wrap(err, "unable to execute directory search for user filter "+c.GroupFilter)
return
}
for _, g := range sr.Entries {
rawMembers := g.GetAttributeValues(c.AttributeGroupMember)
if len(rawMembers) == 0 {
continue
}
for _, entry := range rawMembers {
// get CN element from DN
parts := strings.Split(entry, ",")
if len(parts) == 0 {
continue
}
filter := fmt.Sprintf("(%s)", parts[0])
usr := ld.NewSearchRequest(
c.BaseDN,
ld.ScopeWholeSubtree, ld.NeverDerefAliases, 0, 0, false,
filter,
c.GetUserFilterAttributes(),
nil,
)
ue, err := l.Search(usr)
if err != nil {
continue
}
if len(ue.Entries) > 0 {
for _, ur := range ue.Entries {
u = append(u, extractUser(c, ur))
}
}
}
}
return
}
// extractUser build user record from LDAP result attributes.
func extractUser(c lm.LDAPConfig, e *ld.Entry) (u lm.LDAPUser) {
u.Firstname = e.GetAttributeValue(c.AttributeUserFirstname)
u.Lastname = e.GetAttributeValue(c.AttributeUserLastname)
u.Email = strings.ToLower(e.GetAttributeValue(c.AttributeUserEmail))
u.RemoteID = e.GetAttributeValue(c.AttributeUserRDN)
u.CN = e.GetAttributeValue("cn")
// Make name elements from DisplayName if we can.
if (len(u.Firstname) == 0 || len(u.Firstname) == 0) &&
len(e.GetAttributeValue(c.AttributeUserDisplayName)) > 0 {
}
if len(u.Firstname) == 0 {
u.Firstname = "LDAP"
}
if len(u.Lastname) == 0 {
u.Lastname = "User"
}
return
}
// ConvertUsers creates a unique list of users using email as primary key.
// The result is a collection of Documize user objects.
func convertUsers(c lm.LDAPConfig, lu []lm.LDAPUser) (du []user.User) {
for _, i := range lu {
add := true
for _, j := range du {
if len(j.Email) > 0 && i.Email == j.Email {
add = false
break
}
}
// skip if empty email address
add = len(i.Email) > 0
if add {
du = append(du, convertUser(c, i))
}
}
return
}
// ConvertUser turns LDAP user into Documize user.
func convertUser(c lm.LDAPConfig, lu lm.LDAPUser) (du user.User) {
du = user.User{}
du.Editor = c.DefaultPermissionAddSpace
du.Active = true
du.Email = lu.Email
du.ViewUsers = false
du.Analytics = false
du.Admin = false
du.Global = false
du.Firstname = lu.Firstname
du.Lastname = lu.Lastname
du.Initials = stringutil.MakeInitials(lu.Firstname, lu.Lastname)
return
}
// FetchUsers from LDAP server using both User and Group filters.
func fetchUsers(c lm.LDAPConfig) (du []user.User, err error) {
du = []user.User{}
e1 := []lm.LDAPUser{}
e2 := []lm.LDAPUser{}
e3 := []lm.LDAPUser{}
if len(c.UserFilter) > 0 {
e1, err = executeUserFilter(c)
if err != nil {
err = errors.Wrap(err, "unable to execute user filter")
return
}
}
if len(c.GroupFilter) > 0 {
e2, err = executeGroupFilter(c)
if err != nil {
err = errors.Wrap(err, "unable to execute group filter")
return
}
}
// convert users from LDAP format to Documize format.
e3 = append(e3, e1...)
e3 = append(e3, e2...)
du = convertUsers(c, e3)
return
}

View file

@ -0,0 +1,139 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. 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 <sales@documize.com>.
//
// https://documize.com
package ldap
import (
"testing"
lm "github.com/documize/community/model/auth"
)
// Works against https://github.com/rroemhild/docker-test-openldap
// Use docker run --privileged -d -p 389:389 rroemhild/test-openldap
var testConfigLocalLDAP = lm.LDAPConfig{
ServerType: lm.ServerTypeLDAP,
ServerHost: "127.0.0.1",
ServerPort: 389,
EncryptionType: lm.EncryptionTypeStartTLS,
BaseDN: "ou=people,dc=planetexpress,dc=com",
BindDN: "cn=admin,dc=planetexpress,dc=com",
BindPassword: "GoodNewsEveryone",
UserFilter: "(|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))",
GroupFilter: "(&(objectClass=group)(|(cn=ship_crew)(cn=admin_staff)))",
AttributeUserRDN: "uid",
AttributeUserFirstname: "givenName",
AttributeUserLastname: "sn",
AttributeUserEmail: "mail",
AttributeUserDisplayName: "",
AttributeUserGroupName: "",
AttributeGroupMember: "member",
}
func TestUserFilter_LocalLDAP(t *testing.T) {
e, err := executeUserFilter(testConfigLocalLDAP)
if err != nil {
t.Error("unable to exeucte user filter", err.Error())
return
}
if len(e) == 0 {
t.Error("Received ZERO LDAP search entries")
return
}
t.Logf("LDAP search entries found: %d", len(e))
for _, u := range e {
t.Logf("[%s] %s (%s %s) @ %s\n",
u.RemoteID, u.CN, u.Firstname, u.Lastname, u.Email)
}
}
func TestDualFilters_LocalLDAP(t *testing.T) {
e1, err := executeUserFilter(testConfigLocalLDAP)
if err != nil {
t.Error("unable to exeucte user filter", err.Error())
return
}
e2, err := executeGroupFilter(testConfigLocalLDAP)
if err != nil {
t.Error("unable to exeucte group filter", err.Error())
return
}
e3 := []lm.LDAPUser{}
e3 = append(e3, e1...)
e3 = append(e3, e2...)
users := convertUsers(testConfigLocalLDAP, e3)
for _, u := range users {
t.Logf("(%s %s) @ %s\n",
u.Firstname, u.Lastname, u.Email)
}
}
func TestGroupFilter_LocalLDAP(t *testing.T) {
e, err := executeGroupFilter(testConfigLocalLDAP)
if err != nil {
t.Error("unable to exeucte group filter", err.Error())
return
}
if len(e) == 0 {
t.Error("Received zero group LDAP search entries")
return
}
t.Logf("LDAP group search entries found: %d", len(e))
}
func TestAuthenticate_LocalLDAP(t *testing.T) {
l, err := connect(testConfigLocalLDAP)
if err != nil {
t.Error("Error: unable to dial LDAP server: ", err.Error())
return
}
defer l.Close()
user, ok, err := authenticate(l, testConfigLocalLDAP, "professor", "professor")
if err != nil {
t.Error("error during LDAP authentication: ", err.Error())
return
}
if !ok {
t.Error("failed LDAP authentication")
return
}
t.Log("Authenticated", user.Email)
}
func TestNotAuthenticate_LocalLDAP(t *testing.T) {
l, err := connect(testConfigLocalLDAP)
if err != nil {
t.Error("Error: unable to dial LDAP server: ", err.Error())
return
}
defer l.Close()
_, ok, err := authenticate(l, testConfigLocalLDAP, "junk", "junk")
if err != nil {
t.Error("error during LDAP authentication: ", err.Error())
return
}
if ok {
t.Error("incorrect LDAP authentication")
return
}
t.Log("Not authenticated")
}

View file

@ -0,0 +1,123 @@
// Copyright 2016 Documize IntestConfigPublicLDAP. <legal@documize.com>. 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 <sales@documize.com>.
//
// https://documize.com
package ldap
import (
"testing"
lm "github.com/documize/community/model/auth"
)
// Works against https://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/
var testConfigPublicLDAP = lm.LDAPConfig{
ServerType: lm.ServerTypeLDAP,
ServerHost: "ldap.forumsys.com",
ServerPort: 389,
EncryptionType: lm.EncryptionTypeNone,
BaseDN: "dc=example,dc=com",
BindDN: "cn=read-only-admin,dc=example,dc=com",
BindPassword: "password",
UserFilter: "",
GroupFilter: "",
AttributeUserRDN: "uid",
AttributeUserFirstname: "givenName",
AttributeUserLastname: "sn",
AttributeUserEmail: "mail",
AttributeUserDisplayName: "",
AttributeUserGroupName: "",
AttributeGroupMember: "uniqueMember",
}
func TestUserFilter_PublicLDAP(t *testing.T) {
testConfigPublicLDAP.UserFilter = "(|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))"
e, err := executeUserFilter(testConfigPublicLDAP)
if err != nil {
t.Error("unable to exeucte user filter", err.Error())
return
}
if len(e) == 0 {
t.Error("Received ZERO LDAP search entries")
return
}
t.Logf("LDAP search entries found: %d", len(e))
for _, u := range e {
t.Logf("[%s] %s (%s %s) @ %s\n",
u.RemoteID, u.CN, u.Firstname, u.Lastname, u.Email)
}
}
func TestGroupFilter_PublicLDAP(t *testing.T) {
testConfigPublicLDAP.GroupFilter = "(|(ou=mathematicians)(ou=chemists))"
e, err := executeGroupFilter(testConfigPublicLDAP)
if err != nil {
t.Error("unable to exeucte group filter", err.Error())
return
}
if len(e) == 0 {
t.Error("Received zero group LDAP search entries")
return
}
t.Logf("LDAP group search entries found: %d", len(e))
for _, u := range e {
t.Logf("[%s] %s (%s %s) @ %s\n",
u.RemoteID, u.CN, u.Firstname, u.Lastname, u.Email)
}
}
func TestAuthenticate_PublicLDAP(t *testing.T) {
l, err := connect(testConfigPublicLDAP)
if err != nil {
t.Error("Error: unable to dial LDAP server: ", err.Error())
return
}
defer l.Close()
user, ok, err := authenticate(l, testConfigPublicLDAP, "newton", "password")
if err != nil {
t.Error("error during LDAP authentication: ", err.Error())
return
}
if !ok {
t.Error("failed LDAP authentication")
return
}
t.Log("Authenticated", user.Email)
}
func TestNotAuthenticate_PublicLDAP(t *testing.T) {
l, err := connect(testConfigPublicLDAP)
if err != nil {
t.Error("Error: unable to dial LDAP server: ", err.Error())
return
}
defer l.Close()
_, ok, err := authenticate(l, testConfigPublicLDAP, "junk", "junk")
if err != nil {
t.Error("error during LDAP authentication: ", err.Error())
return
}
if ok {
t.Error("incorrect LDAP authentication")
return
}
t.Log("Not authenticated")
}

View file

@ -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

View file

@ -23,7 +23,7 @@ func startRuntime() (rt *env.Runtime, s *domain.Store) {
rt = new(env.Runtime)
s = new(domain.Store)
rt.Log = logging.NewLogger()
rt.Log = logging.NewLogger(false)
web.Embed = embed.NewEmbedder()
rt.Product = env.ProdInfo{}
@ -61,3 +61,5 @@ func setupContext() domain.RequestContext {
ctx.OrgID = "test"
return ctx
}
// For dummy user data https://www.mockaroo.com

View file

@ -41,8 +41,8 @@ func main() {
// product details
rt.Product = env.ProdInfo{}
rt.Product.Major = "1"
rt.Product.Minor = "69"
rt.Product.Patch = "2"
rt.Product.Minor = "70"
rt.Product.Patch = "0"
rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch)
rt.Product.Edition = "Community"
rt.Product.Title = fmt.Sprintf("%s Edition", rt.Product.Edition)

File diff suppressed because one or more lines are too long

View file

@ -1,16 +1,15 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// 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 <sales@documize.com>.
// by contacting <sales@documize.com>.
//
// https://documize.com
import { resolve } from 'rsvp';
import Base from 'ember-simple-auth/authenticators/base';
export default Base.extend({
@ -20,4 +19,4 @@ export default Base.extend({
authenticate(data) {
return resolve(data);
}
});
});

View file

@ -1,21 +1,20 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// 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 <sales@documize.com>.
// by contacting <sales@documize.com>.
//
// https://documize.com
import { isPresent } from '@ember/utils';
import { reject, resolve } from 'rsvp';
import { inject as service } from '@ember/service';
import Base from 'ember-simple-auth/authenticators/base';
import encodingUtil from '../utils/encoding';
import netUtil from '../utils/net';
import Base from 'ember-simple-auth/authenticators/base';
export default Base.extend({
ajax: service(),
@ -27,7 +26,7 @@ export default Base.extend({
if (data) {
return resolve(data);
}
return reject();
},
@ -55,7 +54,7 @@ export default Base.extend({
},
invalidate() {
this.get('localStorage').clearAll();
this.get('localStorage').clearAll();
return resolve();
}
});
});

View file

@ -10,11 +10,10 @@
// https://documize.com
import { isPresent } from '@ember/utils';
import { reject, resolve } from 'rsvp';
import { inject as service } from '@ember/service';
import Base from 'ember-simple-auth/authenticators/base';
import netUtil from '../utils/net';
import Base from 'ember-simple-auth/authenticators/base';
export default Base.extend({
ajax: service(),

View file

@ -0,0 +1,60 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. 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 <sales@documize.com>.
//
// https://documize.com
import { isPresent } from '@ember/utils';
import { reject, resolve } from 'rsvp';
import { inject as service } from '@ember/service';
import encodingUtil from '../utils/encoding';
import netUtil from '../utils/net';
import Base from 'ember-simple-auth/authenticators/base';
export default Base.extend({
ajax: service(),
appMeta: service(),
localStorage: service(),
restore(data) {
// TODO: verify authentication data
if (data) {
return resolve(data);
}
return reject();
},
authenticate(credentials) {
let domain = netUtil.getSubdomain();
let encoded;
if (typeof credentials === 'object') {
let { password, username } = credentials;
if (!isPresent(password) || !isPresent(username)) {
return reject("invalid");
}
encoded = encodingUtil.Base64.encode(`${domain}:${username}:${password}`);
} else if (typeof credentials === 'string') {
encoded = credentials;
} else {
return reject("invalid");
}
let headers = { 'Authorization': 'Basic ' + encoded };
return this.get('ajax').post('public/authenticate/ldap', { headers });
},
invalidate() {
this.get('localStorage').clearAll();
return resolve();
}
});

View file

@ -15,17 +15,24 @@ import { set } from '@ember/object';
import { copy } from '@ember/object/internals';
import { inject as service } from '@ember/service';
import Notifier from '../../mixins/notifier';
import ModalMixin from '../../mixins/modal';
import encoding from '../../utils/encoding';
import Component from '@ember/component';
export default Component.extend(Notifier, {
export default Component.extend(ModalMixin, Notifier, {
appMeta: service(),
globalSvc: service('global'),
isDocumizeProvider: computed('authProvider', function() {
return this.get('authProvider') === this.get('constants').AuthProvider.Documize;
}),
isKeycloakProvider: computed('authProvider', function() {
return this.get('authProvider') === this.get('constants').AuthProvider.Keycloak;
}),
isLDAPProvider: computed('authProvider', function() {
return this.get('authProvider') === this.get('constants').AuthProvider.LDAP;
}),
KeycloakUrlError: empty('keycloakConfig.url'),
KeycloakRealmError: empty('keycloakConfig.realm'),
KeycloakClientIdError: empty('keycloakConfig.clientId'),
@ -34,8 +41,28 @@ export default Component.extend(Notifier, {
KeycloakAdminPasswordError: empty('keycloakConfig.adminPassword'),
keycloakFailure: '',
ldapErrorServerHost: empty('ldapConfig.serverHost'),
ldapErrorServerPort: computed('ldapConfig.serverPort', function() {
return is.empty(this.get('ldapConfig.serverPort')) || is.not.number(parseInt(this.get('ldapConfig.serverPort')));
}),
ldapErrorBindDN: empty('ldapConfig.bindDN'),
ldapErrorBindPassword: empty('ldapConfig.bindPassword'),
ldapErrorNoFilter: computed('ldapConfig.{userFilter,groupFilter}', function() {
return is.empty(this.get('ldapConfig.userFilter')) && is.empty(this.get('ldapConfig.groupFilter'));
}),
ldapErrorAttributeUserRDN: empty('ldapConfig.attributeUserRDN'),
ldapErrorAttributeUserFirstname: empty('ldapConfig.attributeUserFirstname'),
ldapErrorAttributeUserLastname: empty('ldapConfig.attributeUserLastname'),
ldapErrorAttributeUserEmail: empty('ldapConfig.attributeUserEmail'),
ldapErrorAttributeGroupMember: computed('ldapConfig.{groupFilter,attributeGroupMember}', function() {
return is.not.empty(this.get('ldapConfig.groupFilter')) && is.empty(this.get('ldapConfig.attributeGroupMember'));
}),
ldapPreview: null,
ldapConfig: null,
init() {
this._super(...arguments);
this.keycloakConfig = {
url: '',
realm: '',
@ -55,10 +82,13 @@ export default Component.extend(Notifier, {
let provider = this.get('authProvider');
let constants = this.get('constants');
this.set('ldapPreview', {isError: true, message: 'Unable to connect'});
switch (provider) {
case constants.AuthProvider.Documize:
// nothing to do
break;
case constants.AuthProvider.Keycloak: // eslint-disable-line no-case-declarations
let config = this.get('authConfig');
@ -73,6 +103,20 @@ export default Component.extend(Notifier, {
this.set('keycloakConfig', config);
break;
case constants.AuthProvider.LDAP: // eslint-disable-line no-case-declarations
let ldapConfig = this.get('authConfig');
if (is.undefined(ldapConfig) || is.null(ldapConfig) || is.empty(ldapConfig) ) {
ldapConfig = {};
} else {
ldapConfig = JSON.parse(ldapConfig);
ldapConfig.defaultPermissionAddSpace = ldapConfig.hasOwnProperty('defaultPermissionAddSpace') ? ldapConfig.defaultPermissionAddSpace : false;
ldapConfig.disableLogout = ldapConfig.hasOwnProperty('disableLogout') ? ldapConfig.disableLogout : true;
}
this.set('ldapConfig', ldapConfig);
break;
}
},
@ -87,6 +131,28 @@ export default Component.extend(Notifier, {
this.set('authProvider', constants.AuthProvider.Keycloak);
},
onLDAP() {
let constants = this.get('constants');
this.set('authProvider', constants.AuthProvider.LDAP);
},
onLDAPEncryption(e) {
this.set('ldapConfig.encryptionType', e);
},
onLDAPPreview() {
this.showWait();
let config = this.get('ldapConfig');
config.serverPort = parseInt(this.get('ldapConfig.serverPort'));
this.get('globalSvc').previewLDAP(config).then((preview) => {
this.set('ldapPreview', preview);
this.modalOpen("#ldap-preview-modal", {"show": true});
this.showDone();
});
},
onSave() {
let constants = this.get('constants');
let provider = this.get('authProvider');
@ -98,6 +164,7 @@ export default Component.extend(Notifier, {
case constants.AuthProvider.Documize:
config = {};
break;
case constants.AuthProvider.Keycloak:
if (this.get('KeycloakUrlError')) {
this.$("#keycloak-url").focus();
@ -141,6 +208,27 @@ export default Component.extend(Notifier, {
set(config, 'publicKey', encoding.Base64.encode(this.get('keycloakConfig.publicKey')));
break;
case constants.AuthProvider.LDAP:
if (this.get('ldapErrorServerHost')) {
this.$("#ldap-host").focus();
return;
}
if (this.get('ldapErrorServerPort')) {
this.$("#ldap-port").focus();
return;
}
config = copy(this.get('ldapConfig'));
config.serverHost = config.serverHost.trim();
config.serverPort = parseInt(this.get('ldapConfig.serverPort'));
if (is.not.empty(config.groupFilter) && is.empty(config.attributeGroupMember)) {
this.$('#ldap-attributeGroupMember').focus();
return;
}
break;
}
this.showWait();
@ -148,8 +236,11 @@ export default Component.extend(Notifier, {
let data = { authProvider: provider, authConfig: JSON.stringify(config) };
this.get('onSave')(data).then(() => {
// Without sync we cannot log in
// Keycloak sync process
if (data.authProvider === constants.AuthProvider.Keycloak) {
this.get('onSync')().then((response) => {
this.get('onSyncKeycloak')().then((response) => {
if (response.isError) {
this.set('keycloakFailure', response.message);
console.log(response.message); // eslint-disable-line no-console
@ -164,6 +255,25 @@ export default Component.extend(Notifier, {
}
});
}
// LDAP sync process
if (data.authProvider === constants.AuthProvider.LDAP) {
this.get('onSyncLDAP')().then((response) => {
if (response.isError) {
this.set('keycloakFailure', response.message);
console.log(response.message); // eslint-disable-line no-console
data.authProvider = constants.AuthProvider.Documize;
this.get('onSave')(data).then(() => {});
} else {
if (data.authProvider === this.get('appMeta.authProvider')) {
console.log(response.message); // eslint-disable-line no-console
} else {
this.get('onChange')(data);
}
}
});
}
this.showDone();
});
}

View file

@ -240,8 +240,12 @@ export default Component.extend(AuthProvider, ModalMixin, TooltipMixin, {
});
},
onSync() {
this.get('onSync')();
onSyncKeycloak() {
this.get('onSyncKeycloak')();
},
onSyncLDAP() {
this.get('onSyncLDAP')();
},
onLimit(limit) {

View file

@ -12,8 +12,8 @@
import $ from 'jquery';
import { empty, and } from '@ember/object/computed';
import { set } from '@ember/object';
import Component from '@ember/component';
import { isEmpty } from '@ember/utils';
import Component from '@ember/component';
export default Component.extend({
email: "",

View file

@ -36,7 +36,7 @@ export default Component.extend(ModalMixin, {
this.pins = [];
if (this.get('appMeta.authProvider') === constants.AuthProvider.Keycloak) {
if (this.get('appMeta.authProvider') !== constants.AuthProvider.Documize) {
let config = this.get('appMeta.authConfig');
config = JSON.parse(config);
this.set('enableLogout', !config.disableLogout);

View file

@ -23,7 +23,12 @@ let constants = EmberObject.extend({
AuthProvider: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
Documize: 'documize',
Keycloak: 'keycloak'
Keycloak: 'keycloak',
LDAP: 'ldap',
ServerTypeLDAP: 'ldap',
ServerTypeAD: 'ad',
EncryptionTypeNone: 'none',
EncryptionTypeStartTLS: 'starttls'
},
DocumentActionType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects

View file

@ -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);
}
});

View file

@ -10,11 +10,11 @@
// https://documize.com
import { inject as service } from '@ember/service';
import Controller from '@ember/controller';
export default Controller.extend({
userService: service('user'),
appMeta: service('app-meta'),
actions: {
forgot: function (email) {

View file

@ -19,7 +19,7 @@ export default Route.extend({
beforeModel() {
let constants = this.get('constants');
if (this.get('appMeta.authProvider') === constants.AuthProvider.Keycloak) {
if (this.get('appMeta.authProvider') !== constants.AuthProvider.Documize) {
this.transitionTo('auth.login');
}
},

View file

@ -2,6 +2,7 @@
<div class="auth-box">
<div class="logo">
<img src="/assets/img/logo-purple.png" title="Documize" alt="Documize" class="img-fluid" />
<div class="url">Authenticate with {{appMeta.appHost}}</div>
</div>
<div class="login-form">
{{forgot-password forgot=(action 'forgot')}}

View file

@ -10,8 +10,8 @@
// https://documize.com
import { inject as service } from '@ember/service';
import Controller from '@ember/controller';
import AuthProvider from '../../../mixins/auth';
import Controller from '@ember/controller';
export default Controller.extend(AuthProvider, {
appMeta: service('app-meta'),
@ -19,10 +19,19 @@ export default Controller.extend(AuthProvider, {
invalidCredentials: false,
reset() {
this.setProperties({
email: '',
password: ''
});
if (this.get('sAuthProviderDocumize')) {
this.setProperties({
email: '',
password: ''
});
}
if (this.get('sAuthProviderLDAP')) {
this.setProperties({
username: '',
password: ''
});
}
let dbhash = document.head.querySelector("[property=dbhash]").content;
if (dbhash.length > 0 && dbhash !== "{{.DBhash}}") {
@ -32,14 +41,27 @@ export default Controller.extend(AuthProvider, {
actions: {
login() {
let creds = this.getProperties('email', 'password');
if (this.get('isAuthProviderDocumize')) {
let creds = this.getProperties('email', 'password');
this.get('session').authenticate('authenticator:documize', creds).then((response) => {
this.transitionToRoute('folders');
return response;
}).catch(() => {
this.set('invalidCredentials', true);
});
this.get('session').authenticate('authenticator:documize', creds).then((response) => {
this.transitionToRoute('folders');
return response;
}).catch(() => {
this.set('invalidCredentials', true);
});
}
if (this.get('isAuthProviderLDAP')) {
let creds = this.getProperties('username', 'password');
this.get('session').authenticate('authenticator:ldap', creds).then((response) => {
this.transitionToRoute('folders');
return response;
}).catch(() => {
this.set('invalidCredentials', true);
});
}
}
}
});

View file

@ -3,22 +3,27 @@
<div class="auth-box">
<div class="logo">
<img src="/assets/img/logo-purple.png" title="Documize" alt="Documize" class="img-fluid" />
<div class="url">Authenticate with {{appMeta.appHost}}</div>
</div>
<form {{action 'login' on="submit"}}>
<div class="form-group">
<label for="authEmail">Email</label>
{{focus-input type="email" value=email id="authEmail" class="form-control mousetrap" placeholder="" autocomplete="username email"}}
{{#if isAuthProviderDocumize}}
<label for="authEmail">Email</label>
{{focus-input type="email" value=email id="authEmail" class="form-control mousetrap" placeholder="" autocomplete="username email"}}
{{/if}}
{{#if isAuthProviderLDAP}}
<label for="authUsername">Network Username</label>
{{focus-input type="text" value=username id="authUsername" class="form-control mousetrap" placeholder="network domain username" autocomplete="username"}}
{{/if}}
</div>
<div class="form-group">
<label for="authPassword">Password</label>
{{input type="password" value=password id="authPassword" class="form-control" autocomplete="current-password"}}
{{input type="password" value=password id="authPassword" class="form-control" placeholder="network password" autocomplete="current-password"}}
</div>
<button type="submit" class="btn btn-success font-weight-bold text-uppercase mt-4">Sign in</button>
<div class="{{unless invalidCredentials "invisible"}} color-red mt-3">Invalid credentials</div>
{{#if isAuthProviderDocumize}}
<div class="mt-5">
{{#link-to 'auth.forgot'}}Forgot your password?{{/link-to}}
</div>
{{#link-to 'auth.forgot'}}Forgot your password?{{/link-to}}
{{/if}}
</form>
</div>

View file

@ -10,9 +10,7 @@
// https://documize.com
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
// import config from 'documize/config/environment';
export default Route.extend({
session: service(),
@ -20,15 +18,6 @@ export default Route.extend({
activate: function () {
this.get('session').invalidate().then(() => {
// if (config.environment === 'test') {
// this.transitionTo('auth.login');
// } else {
// if (this.get("appMeta.allowAnonymousAccess")) {
// this.transitionTo('folders');
// } else {
// this.transitionTo('auth.login');
// }
// }
});
}
});

View file

@ -10,11 +10,11 @@
// https://documize.com
import { inject as service } from '@ember/service';
import Controller from '@ember/controller';
export default Controller.extend({
userService: service('user'),
appMeta: service('app-meta'),
password: "",
passwordConfirm: "",
mustMatch: false,

View file

@ -2,6 +2,7 @@
<div class="auth-box">
<div class="logo">
<img src="/assets/img/logo-purple.png" title="Documize" alt="Documize" class="img-fluid" />
<div class="url">Authenticate with {{appMeta.appHost}}</div>
</div>
{{password-reset reset=(action 'reset')}}
</div>

View file

@ -1,18 +1,18 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// 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 <sales@documize.com>.
// by contacting <sales@documize.com>.
//
// https://documize.com
import $ from 'jquery';
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, {
session: service(),
@ -42,5 +42,5 @@ export default Route.extend(AuthenticatedRouteMixin, {
deactivate() {
$('body').removeClass('background-color-off-white');
}
});
}
});

View file

@ -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(),
@ -32,9 +32,17 @@ export default Controller.extend(NotifierMixin, {
});
},
onSync() {
onSyncKeycloak() {
return new EmberPromise((resolve) => {
this.get('global').syncExternalUsers().then((response) => {
this.get('global').syncKeycloak().then((response) => {
resolve(response);
});
});
},
onSyncLDAP() {
return new EmberPromise((resolve) => {
this.get('global').syncLDAP().then((response) => {
resolve(response);
});
});

View file

@ -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(),
@ -26,19 +26,22 @@ export default Route.extend(AuthenticatedRouteMixin, {
},
model() {
let constants = this.get('constants');
let data = {
authProvider: this.get('appMeta.authProvider'),
authConfig: null,
};
return new EmberPromise((resolve) => {
let constants = this.get('constants');
this.get('global').getAuthConfig().then((config) => {
switch (data.authProvider) {
case constants.AuthProvider.Keycloak:
data.authConfig = config;
break;
case constants.AuthProvider.LDAP:
data.authConfig = config;
break;
case constants.AuthProvider.Documize:
data.authConfig = '';
break;

View file

@ -1,2 +1,7 @@
{{customize/auth-settings authProvider=model.authProvider authConfig=model.authConfig
onSave=(action 'onSave') onSync=(action 'onSync') onChange=(action 'onChange')}}
{{customize/auth-settings
authProvider=model.authProvider
authConfig=model.authConfig
onSave=(action 'onSave')
onSyncLDAP=(action 'onSyncLDAP')
onSyncKeycloak=(action 'onSyncKeycloak')
onChange=(action 'onChange')}}

View file

@ -57,9 +57,17 @@ export default Controller.extend({
this.loadUsers(filter);
},
onSync() {
onSyncKeycloak() {
this.set('syncInProgress', true);
this.get('globalSvc').syncExternalUsers().then(() => {
this.get('globalSvc').syncKeycloak().then(() => {
this.set('syncInProgress', false);
this.loadUsers('');
});
},
onSyncLDAP() {
this.set('syncInProgress', true);
this.get('globalSvc').syncLDAP().then(() => {
this.set('syncInProgress', false);
this.loadUsers('');
});

View file

@ -5,7 +5,8 @@
{{customize/user-list users=model
syncInProgress=syncInProgress
userLimit=userLimit
onSync=(action "onSync")
onSyncKeycloak=(action "onSyncKeycloak")
onSyncLDAP=(action "onSyncLDAP")
onFilter=(action "onFilter")
onDelete=(action "onDelete")
onSave=(action "onSave")

View file

@ -9,11 +9,11 @@
//
// https://documize.com
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';
import netUtil from '../utils/net';
import TooltipMixin from '../mixins/tooltip';
import Route from '@ember/routing/route';
export default Route.extend(ApplicationRouteMixin, TooltipMixin, {
appMeta: service(),
@ -27,7 +27,7 @@ export default Route.extend(ApplicationRouteMixin, TooltipMixin, {
let sa = this.get('session.session.authenticator');
return this.get('appMeta').boot(transition.targetName, window.location.href).then(data => {
if (sa !== "authenticator:documize" && sa !== "authenticator:keycloak" && data.allowAnonymousAccess) {
if (sa !== "authenticator:documize" && sa !== "authenticator:keycloak" && sa !== "authenticator:ldap" && data.allowAnonymousAccess) {
if (!this.get('appMeta.setupMode') && !this.get('appMeta.secureMode')) {
return this.get('session').authenticate('authenticator:anonymous', data);
}

View file

@ -20,6 +20,7 @@ export default Service.extend({
ajax: service(),
localStorage: service(),
kcAuth: service(),
appHost: '',
apiHost: `${config.apiHost}`,
endpoint: `${config.apiHost}/${config.apiNamespace}`,
conversionEndpoint: '',
@ -72,6 +73,7 @@ export default Service.extend({
return this.get('ajax').request('public/meta').then((response) => {
this.setProperties(response);
this.set('version', 'v' + this.get('version'));
this.set('appHost', window.location.host);
if (requestedRoute === 'secure') {
this.setProperties({

View file

@ -82,9 +82,9 @@ 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;
@ -94,6 +94,31 @@ export default Service.extend({
}
},
syncLDAP() {
if(this.get('sessionService.isAdmin')) {
return this.get('ajax').request(`global/ldap/sync`, {
method: 'GET'
}).then((response) => {
return response;
}).catch((error) => {
return error;
});
}
},
previewLDAP(config) {
if(this.get('sessionService.isAdmin')) {
return this.get('ajax').request(`global/ldap/preview`, {
method: 'POST',
data: JSON.stringify(config)
}).then((response) => {
return response;
}).catch((error) => {
return error;
});
}
},
// Returns product license.
searchStatus() {
if (this.get('sessionService.isGlobalAdmin')) {

View file

@ -10,8 +10,8 @@
// https://documize.com
import { Promise as EmberPromise } from 'rsvp';
import Service, { inject as service } from '@ember/service';
import netUtil from '../utils/net';
import Service, { inject as service } from '@ember/service';
export default Service.extend({
sessionService: service('session'),
@ -59,7 +59,7 @@ export default Service.extend({
return resolve();
}).error(() => {
return reject(new Error('login failed'));
});
});
});
});
},

View file

@ -80,9 +80,14 @@ $input-btn-focus-color: rgba($color-primary, .25);
font-weight: 500;
}
> small {
> small, > div[class*="col"] > small {
font-size: 1rem;
}
> small.highlight, > div[class*="col"] > small.highlight {
font-size: 1rem;
color: $color-orange !important;
}
}
// links

View file

@ -39,6 +39,7 @@ $color-red: #9E0D1F;
$color-green: #348A37;
$color-blue: #2667af;
$color-goldy: #FFD700;
$color-yellow: #fff8dc;
$color-orange: #FFAD15;
// widgets

File diff suppressed because one or more lines are too long

View file

@ -23,7 +23,13 @@
> img {
height: 60px;
margin: 40px 0;
margin: 30px 0 0 0;
}
> .url {
margin: 20px 0;
color: $color-gray;
font-weight: 0.9rem;
}
}
}

View file

@ -7,37 +7,79 @@
padding: 0;
> .option {
@include ease-in();
margin: 0 0 5px 0;
@include border-radius(3px);
margin: 0 0 15px 0;
padding: 10px 15px;
color: $color-gray;
background-color: $color-off-white;
border: 1px solid $color-gray;
cursor: pointer;
position: relative;
list-style-type: none;
line-height: 26px;
&:hover {
color: $color-black;
// background-color: $color-primary-light;
> .text-header, > .text {
color: $color-off-black;
}
}
> .text-header {
@include ease-in();
color: $color-gray;
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 80%;
}
> .text {
@include ease-in();
color: $color-gray;
font-size: 1rem;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 80%;
font-weight: bold;
}
> .material-icons {
position: absolute;
top: 10px;
right: 10px;
color: $color-white;
color: $color-green;
font-weight: 700;
font-size: 1.3rem;
}
}
> .selected {
color: $color-white !important;
background-color: $color-link !important;
> .text-header, > .text {
color: $color-off-black;
}
background-color: $color-yellow !important;
border: 1px solid $color-goldy !important;
}
}
}
.widget-list-picker-horiz {
> .options {
> .option {
display: inline-block;
margin: 15px 15px 0 0;
padding: 10px 15px;
width: 30%;
@media only screen and (max-width: 1200px) {
display: block;
width: 100%;
}
}
}
}

View file

@ -9,95 +9,252 @@
<div class="view-customize">
<form class="mt-5">
<div class="form-group row">
<label class="col-sm-2 col-form-label">Provider</label>
<div class="col-sm-10">
{{#ui/ui-radio selected=isDocumizeProvider onClick=(action 'onDocumize')}} Documize &mdash; email/password{{/ui/ui-radio}}
{{#ui/ui-radio selected=isKeycloakProvider onClick=(action 'onKeycloak')}} Keycloak &mdash; bring your own authentication server{{/ui/ui-radio}}
<small class="form-text text-muted">
External authentication servers, services must be accessible from the server running this Documize instance
</small>
</div>
<div class="widget-list-picker widget-list-picker-horiz">
<ul class="options">
<li class="option {{if isDocumizeProvider 'selected'}}" {{action 'onDocumize'}}>
<div class="text-header">Documize</div>
<div class="text">Built-in email/password</div>
{{#if isDocumizeProvider}}
<i class="material-icons">check</i>
{{/if}}
</li>
<li class="option {{if isKeycloakProvider 'selected'}}" {{action 'onKeycloak'}}>
<div class="text-header">Keycloak</div>
<div class="text">Via authentication server</div>
{{#if isKeycloakProvider}}
<i class="material-icons">check</i>
{{/if}}
</li>
<li class="option {{if isLDAPProvider 'selected'}}" {{action 'onLDAP'}}>
<div class="text-header">LDAP</div>
<div class="text">Connect to LDAP/ Active Directory</div>
{{#if isLDAPProvider}}
<i class="material-icons">check</i>
{{/if}}
</li>
</ul>
</div>
<br/>
<br/>
<br/>
{{#if isDocumizeProvider}}
<p>There are no settings.</p>
{{/if}}
{{#if isKeycloakProvider}}
<div class="form-group row">
<label for="keycloak-url" class="col-sm-2 col-form-label">Keycloak Server URL</label>
<div class="col-sm-10">
<label for="keycloak-url" class="col-sm-3 col-form-label">Keycloak Server URL</label>
<div class="col-sm-9">
{{focus-input id="keycloak-url" type="text" value=keycloakConfig.url class=(if KeycloakUrlError 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">e.g. http://localhost:8888/auth</small>
</div>
</div>
<div class="form-group row">
<label for="keycloak-realm" class="col-sm-2 col-form-label">Keycloak Realm</label>
<div class="col-sm-10">
<label for="keycloak-realm" class="col-sm-3 col-form-label">Keycloak Realm</label>
<div class="col-sm-9">
{{input id="keycloak-realm" type="text" value=keycloakConfig.realm class=(if KeycloakRealmError 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">e.g. main</small>
</div>
</div>
<div class="form-group row">
<label for="keycloak-publicKey" class="col-sm-2 col-form-label">Keycloak Realm Public Key</label>
<div class="col-sm-10">
<label for="keycloak-publicKey" class="col-sm-3 col-form-label">Keycloak Realm Public Key</label>
<div class="col-sm-9">
{{textarea id="keycloak-publicKey" type="text" value=keycloakConfig.publicKey rows=7 class=(if KeycloakPublicKeyError 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">Copy the RSA Public Key from Realm Settings &rarr; Keys</small>
</div>
</div>
<div class="form-group row">
<label for="keycloak-clientId" class="col-sm-2 col-form-label">Keycloak OIDC Client ID</label>
<div class="col-sm-10">
<label for="keycloak-clientId" class="col-sm-3 col-form-label">Keycloak OIDC Client ID</label>
<div class="col-sm-9">
{{input id="keycloak-clientId" type="text" value=keycloakConfig.clientId class=(if KeycloakClientIdError 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">e.g. account</small>
</div>
</div>
<div class="form-group row">
<label for="keycloak-group" class="col-sm-2 col-form-label">Keycloak Group ID (Optional)</label>
<div class="col-sm-10">
<label for="keycloak-group" class="col-sm-3 col-form-label">Keycloak Group ID (Optional)</label>
<div class="col-sm-9">
{{input id="keycloak-group" type="text" value=keycloakConfig.group class="form-control"}}
<small class="form-text text-muted">If you want to sync users in a particular Group (e.g. 'Documize Users'), provide the Group ID (e.g. 511d8b61-1ec8-45f6-bc8d-5de64d54c9d2)</small>
</div>
</div>
<div class="form-group row">
<label for="keycloak-admin-user" class="col-sm-2 col-form-label">Keycloak Username</label>
<div class="col-sm-10">
<label for="keycloak-admin-user" class="col-sm-3 col-form-label">Keycloak Username</label>
<div class="col-sm-9">
{{input id="keycloak-admin-user" type="text" value=keycloakConfig.adminUser class=(if KeycloakAdminUserError 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">Used to connect with Keycloak and sync users with Documize (create user under Master Realm and assign 'view-users' role
against Realm specified above)</small>
</div>
</div>
<div class="form-group row">
<label for="keycloak-admin-password" class="col-sm-2 col-form-label">Keycloak Password</label>
<div class="col-sm-10">
<label for="keycloak-admin-password" class="col-sm-3 col-form-label">Keycloak Password</label>
<div class="col-sm-9">
{{input id="keycloak-admin-password" type="password" value=keycloakConfig.adminPassword class=(if KeycloakAdminPasswordError 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">Used to connect with Keycloak and sync users with Documize</small>
</div>
</div>
<div class="form-group row">
<label for="keycloak-admin-password" class="col-sm-2 col-form-label">Logout</label>
<div class="col-sm-10">
<div class="form-check">
{{input type="checkbox" class="form-check-input" id="keycloak-logout" checked=keycloakConfig.disableLogout}}
<label class="form-check-label" for="keycloak-logout">
Hide the logout button for Keycloak users
</label>
</div>
<label class="col-sm-3 col-form-label">Disable Logout</label>
<div class="col-sm-9">
{{x-toggle value=keycloakConfig.disableLogout size="medium" theme="light" onToggle=(action (mut keycloakConfig.disableLogout))}}
</div>
</div>
<div class="form-group row">
<label for="keycloak-admin-password" class="col-sm-2 col-form-label">Space Permission</label>
<div class="col-sm-10">
<div class="form-check">
{{input type="checkbox" class="form-check-input" id="keycloak-perm" checked=keycloakConfig.defaultPermissionAddSpace}}
<label class="form-check-label" for="keycloak-perm">
Can add spaces
</label>
</div>
<label for="ldap-defaultPermissionAddSpace" class="col-sm-3 col-form-label">Can Create Spaces</label>
<div class="col-sm-9">
{{x-toggle value=keycloakConfig.defaultPermissionAddSpace size="medium" theme="light" onToggle=(action (mut keycloakConfig.defaultPermissionAddSpace))}}
</div>
</div>
{{/if}}
<div class="btn btn-success mt-4" {{action 'onSave'}}>Save</div>
{{#if isLDAPProvider}}
<div class="form-group row">
<label for="ldap-host" class="col-sm-3 col-form-label">LDAP Server</label>
<div class="col-sm-9">
{{focus-input id="ldap-host" type="text" value=ldapConfig.serverHost class=(if ldapErrorServerHost 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">IP or host address, e.g. ldap.example.org, 127.0.0.1</small>
</div>
</div>
<div class="form-group row">
<label for="ldap-port" class="col-sm-3 col-form-label">LDAP Server Port</label>
<div class="col-sm-9">
{{input id="ldap-port" type="number" value=ldapConfig.serverPort class=(if ldapErrorServerPort 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">Port number, e.g. 389</small>
</div>
</div>
<div class="form-group row">
<label for="ldap-encryption" class="col-sm-3 col-form-label">Encryption</label>
<div class="col-sm-9">
<select onchange={{action 'onLDAPEncryption' value="target.value"}} class="form-control">
<option value="{{constants.AuthProvider.EncryptionTypeNone}}" selected={{is-equal ldapConfig.encryptionType constants.AuthProvider.EncryptionTypeNone}}>
{{constants.AuthProvider.EncryptionTypeNone}}
</option>
<option value="{{constants.AuthProvider.EncryptionTypeStartTLS}}" selected={{is-equal ldapConfig.encryptionType constants.AuthProvider.EncryptionTypeStartTLS}}>
{{constants.AuthProvider.EncryptionTypeStartTLS}}
</option>
</select>
</div>
</div>
<div class="form-group row">
<label for="ldap-baseDN" class="col-sm-3 col-form-label">Base DN</label>
<div class="col-sm-9">
{{input id="ldap-baseDN" type="text" value=ldapConfig.baseDN class='form-control'}}
<small class="form-text text-muted">Starting point for search filters, e.g. ou=users,dc=example,dc=com</small>
</div>
</div>
<div class="form-group row">
<label for="ldap-bindDN" class="col-sm-3 col-form-label">Bind DN</label>
<div class="col-sm-9">
{{input id="ldap-bindDN" type="text" value=ldapConfig.bindDN class=(if ldapErrorBindDN 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">login credentials for LDAP server</small>
</div>
</div>
<div class="form-group row">
<label for="ldap-bindPassword" class="col-sm-3 col-form-label">Bind Password</label>
<div class="col-sm-9">
{{input id="ldap-bindPassword" type="password" value=ldapConfig.bindPassword class=(if ldapErrorBindPassword 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">login credentials for LDAP server</small>
</div>
</div>
<div class="form-group row">
<label for="ldap-userFilter" class="col-sm-3 col-form-label">User Filter</label>
<div class="col-sm-9">
{{input id="ldap-userFilter" type="text" value=ldapConfig.userFilter class=(if ldapErrorNoFilter 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">Search filter for finding users, e.g. (|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))</small>
<small class="form-text text-muted highlight">Specify User Filter and/or Group Filter</small>
</div>
</div>
<div class="form-group row">
<label for="ldap-groupFilter" class="col-sm-3 col-form-label">Group Filter</label>
<div class="col-sm-9">
{{input id="ldap-groupFilter" type="text" value=ldapConfig.groupFilter class=(if ldapErrorNoFilter 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">Search filter for finding users via groups, e.g. (&(objectClass=group)(|(cn=ship_crew)(cn=admin_staff))</small>
</div>
</div>
<div class="form-group row">
<label for="ldap-attributeUserRDN" class="col-sm-3 col-form-label">User Attribute RDN</label>
<div class="col-sm-9">
{{input id="ldap-attributeUserRDN" type="text" value=ldapConfig.attributeUserRDN class=(if ldapErrorAttributeUserRDN 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">Username/login attribute, e.g. uid in LDAP, sAMAccountName in Active Directory</small>
<small class="form-text text-muted highlight">User Attributes used to retreive data when using User Filter</small>
</div>
</div>
<div class="form-group row">
<label for="ldap-attributeUserFirstname" class="col-sm-3 col-form-label">User Attribute Firstname</label>
<div class="col-sm-9">
{{input id="ldap-attributeUserFirstname" type="text" value=ldapConfig.attributeUserFirstname class=(if ldapErrorAttributeUserFirstname 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">Firstname attribute, e.g. givenName</small>
</div>
</div>
<div class="form-group row">
<label for="ldap-attributeUserLastname" class="col-sm-3 col-form-label">User Attribute Lastname</label>
<div class="col-sm-9">
{{input id="ldap-attributeUserLastname" type="text" value=ldapConfig.attributeUserLastname class=(if ldapErrorAttributeUserLastname 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">Lastname attribute, e.g. sn</small>
</div>
</div>
<div class="form-group row">
<label for="ldap-attributeUserEmail" class="col-sm-3 col-form-label">User Attribute Email</label>
<div class="col-sm-9">
{{input id="ldap-attributeUserEmail" type="text" value=ldapConfig.attributeUserEmail class=(if ldapErrorAttributeUserEmail 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">Email attribute, e.g. mail</small>
</div>
</div>
<div class="form-group row">
<label for="ldap-attributeGroupMember" class="col-sm-3 col-form-label">Group Attribute Member</label>
<div class="col-sm-9">
{{input id="ldap-attributeGroupMember" type="text" value=ldapConfig.attributeGroupMember class=(if ldapErrorAttributeGroupMember 'form-control is-invalid' 'form-control')}}
<small class="form-text text-muted">Attribute that identifies individual group member, e.g. member or uniqueMember</small>
<small class="form-text text-muted highlight">Group Attributes used to retreive data when using Group Filter</small>
</div>
</div>
<div class="form-group row">
<label for="ldap-disableLogout" class="col-sm-3 col-form-label">Disable Logout</label>
<div class="col-sm-9">
{{x-toggle value=ldapConfig.disableLogout size="medium" theme="light" onToggle=(action (mut ldapConfig.disableLogout))}}
</div>
</div>
<div class="form-group row">
<label for="ldap-defaultPermissionAddSpace" class="col-sm-3 col-form-label">Can Create Spaces</label>
<div class="col-sm-9">
{{x-toggle value=ldapConfig.defaultPermissionAddSpace size="medium" theme="light" onToggle=(action (mut ldapConfig.defaultPermissionAddSpace))}}
</div>
</div>
<div class="form-group row">
<div class="col-sm-3"></div>
<div class="col-sm-9">
<div class="btn btn-secondary mt-4" {{action 'onLDAPPreview'}}>Test Connection & Preview pauth&rarr;</div>
</div>
</div>
{{/if}}
<div class="btn btn-success mt-4" {{action 'onSave'}}>ACTIVATE</div>
</form>
{{#if (gt keycloakFailure.length 0)}}
<p class="admin-setting-failure my-3">Keycloak connection failed: {{keycloakFailure}}</p>
{{/if}}
</div>
<div id="ldap-preview-modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">LDAP Preview</div>
<div class="modal-body">
{{#if ldapPreview.isError}}
<p class="text-danger">{{ldapPreview.message}}</p>
{{else}}
<p class="text-success">Connection successful, found {{ldapPreview.count}} users.</p>
{{#each ldapPreview.users as |user|}}
<p>{{user.firstname}} {{user.firstname}} ({{user.email}})</p>
{{/each}}
{{/if}}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>

View file

@ -3,7 +3,14 @@
{{#if syncInProgress}}
<div class="btn btn-secondary mt-3 mb-3">Keycloak user sync running...</div>
{{else}}
<div class="btn btn-success mt-3 mb-3" {{action 'onSync'}}>Sync with Keycloak</div>
<div class="btn btn-success mt-3 mb-3" {{action 'onSyncKeycloak'}}>Sync with Keycloak</div>
{{/if}}
{{/if}}
{{#if isAuthProviderLDAP}}
{{#if syncInProgress}}
<div class="btn btn-secondary mt-3 mb-3">LDAP user sync running...</div>
{{else}}
<div class="btn btn-success mt-3 mb-3" {{action 'onSyncLDAP'}}>Sync with LDAP</div>
{{/if}}
{{/if}}

View file

@ -1,6 +1,6 @@
{
"name": "documize",
"version": "1.69.2",
"version": "1.70.0",
"description": "The Document IDE",
"private": true,
"repository": "",

View file

@ -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"
)

175
model/auth/ldap.go Normal file
View file

@ -0,0 +1,175 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. 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 <sales@documize.com>.
//
// https://documize.com
package auth
import (
"strings"
)
// Example for Active Directory -- filter users that belong to SomeGroupName:
// (&(objectCategory=Person)(sAMAccountName=*)(memberOf=cn=SomeGroupName,ou=users,dc=example,dc=com))
//
// Example for Active Directory -- filter all users that belong to SomeGroupName:
// (&(objectCategory=Person)(sAMAccountName=*)(memberOf:1.2.840.113556.1.4.1941:=cn=SomeGroupName,ou=users,dc=example,dc=com))
//
// Example for Active Directory -- filter all users that belong to MyGroup1, MyGroup2 or MyGroup3:
// (&(objectCategory=Person)(sAMAccountName=*)(|(memberOf=cn=MyGroup1,ou=users,dc=example,dc=com)(memberOf=cn=MyGroup2,ou=users,dc=example,dc=com)(memberOf=cn=MyGroup3,ou=users,dc=example,dc=com)))
//
// Example of group filter that returns users belonging to either Developers or Administrators group:
// (&(objectCategory=Group)(|(cn=developers)(cn=administrators)))
//
// Sources of filter names:
// https://docs.oracle.com/cd/E26217_01/E26214/html/ldap-filters-attrs-users.html
// https://social.technet.microsoft.com/wiki/contents/articles/5392.active-directory-ldap-syntax-filters.aspx
// LDAPConfig that specifies LDAP server connection details and query filters.
type LDAPConfig struct {
ServerHost string `json:"serverHost"`
ServerPort int `json:"serverPort"`
ServerType ServerType `json:"serverType"`
EncryptionType EncryptionType `json:"encryptionType"`
BaseDN string `json:"baseDN"`
BindDN string `json:"bindDN"`
BindPassword string `json:"bindPassword"`
UserFilter string `json:"userFilter"`
GroupFilter string `json:"groupFilter"`
DisableLogout bool `json:"disableLogout"`
DefaultPermissionAddSpace bool `json:"defaultPermissionAddSpace"`
AttributeUserRDN string `json:"attributeUserRDN"` // usually uid (LDAP) or sAMAccountName (AD)
AttributeUserFirstname string `json:"attributeUserFirstname"` // usually givenName
AttributeUserLastname string `json:"attributeUserLastname"` // usually sn
AttributeUserEmail string `json:"attributeUserEmail"` // usually mail
AttributeUserDisplayName string `json:"attributeUserDisplayName"` // usually displayName
AttributeUserGroupName string `json:"attributeUserGroupName"` // usually memberOf
AttributeGroupMember string `json:"attributeGroupMember"` // usually member
}
// ServerType identifies the LDAP server type
type ServerType string
const (
// ServerTypeLDAP represents a generic LDAP server OpenLDAP.
ServerTypeLDAP = "ldap"
// ServerTypeAD represents Microsoft Active Directory server.
ServerTypeAD = "ad"
)
// EncryptionType determines encryption method for LDAP connection.EncryptionType
type EncryptionType string
const (
// EncryptionTypeNone is none.
EncryptionTypeNone = "none"
// EncryptionTypeStartTLS is using start TLS.
EncryptionTypeStartTLS = "starttls"
)
const (
// MaxPageSize controls how many query results are
// fetched at once from the LDAP server.
// See https://answers.splunk.com/answers/1538/what-is-ldap-error-size-limit-exceeded.html
MaxPageSize = 250
)
// Clean ensures configuration data is formatted correctly.
func (c *LDAPConfig) Clean() {
c.BaseDN = strings.TrimSpace(c.BaseDN)
c.BindDN = strings.TrimSpace(c.BindDN)
c.BindPassword = strings.TrimSpace(c.BindPassword)
c.ServerHost = strings.TrimSpace(c.ServerHost)
c.UserFilter = strings.TrimSpace(c.UserFilter)
c.GroupFilter = strings.TrimSpace(c.GroupFilter)
if c.ServerPort == 0 {
c.ServerPort = 389
}
if c.ServerType == "" {
c.ServerType = ServerTypeLDAP
}
if c.EncryptionType == "" {
c.EncryptionType = "none"
}
if c.EncryptionType != EncryptionTypeNone || c.EncryptionType != EncryptionTypeStartTLS {
c.EncryptionType = EncryptionTypeNone
}
c.AttributeUserRDN = strings.TrimSpace(c.AttributeUserRDN)
c.AttributeUserFirstname = strings.TrimSpace(c.AttributeUserFirstname)
c.AttributeUserLastname = strings.TrimSpace(c.AttributeUserLastname)
c.AttributeUserEmail = strings.TrimSpace(c.AttributeUserEmail)
c.AttributeUserDisplayName = strings.TrimSpace(c.AttributeUserDisplayName)
c.AttributeUserGroupName = strings.TrimSpace(c.AttributeUserGroupName)
c.AttributeGroupMember = strings.TrimSpace(c.AttributeGroupMember)
}
// GetUserFilterAttributes gathers the fields that can be requested
// when executing a user-based object filter.
func (c *LDAPConfig) GetUserFilterAttributes() []string {
a := []string{}
// defaults
a = append(a, "dn")
a = append(a, "cn")
if len(c.AttributeUserRDN) > 0 {
a = append(a, c.AttributeUserRDN)
}
if len(c.AttributeUserFirstname) > 0 {
a = append(a, c.AttributeUserFirstname)
}
if len(c.AttributeUserLastname) > 0 {
a = append(a, c.AttributeUserLastname)
}
if len(c.AttributeUserEmail) > 0 {
a = append(a, c.AttributeUserEmail)
}
if len(c.AttributeUserDisplayName) > 0 {
a = append(a, c.AttributeUserDisplayName)
}
if len(c.AttributeUserGroupName) > 0 {
a = append(a, c.AttributeUserGroupName)
}
return a
}
// GetGroupFilterAttributes gathers the fields that can be requested
// when executing a group-based object filter.
func (c *LDAPConfig) GetGroupFilterAttributes() []string {
a := []string{}
// defaults
a = append(a, "dn")
a = append(a, "cn")
if len(c.AttributeGroupMember) > 0 {
a = append(a, c.AttributeGroupMember)
}
return a
}
// LDAPUser details user record returned by LDAP
type LDAPUser struct {
RemoteID string `json:"remoteId"`
CN string `json:"cn"`
Email string `json:"email"`
Firstname string `json:"firstName"`
Lastname string `json:"lastName"`
}

View file

@ -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}
@ -80,6 +82,7 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) {
//**************************************************
AddPublic(rt, "authenticate/keycloak", []string{"POST", "OPTIONS"}, nil, keycloak.Authenticate)
AddPublic(rt, "authenticate/ldap", []string{"POST", "OPTIONS"}, nil, ldap.Authenticate)
AddPublic(rt, "authenticate", []string{"POST", "OPTIONS"}, nil, auth.Login)
AddPublic(rt, "validate", []string{"GET", "OPTIONS"}, nil, auth.ValidateToken)
AddPublic(rt, "forgot", []string{"POST", "OPTIONS"}, nil, user.ForgotPassword)
@ -150,7 +153,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 +214,9 @@ 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/ldap/preview", []string{"POST", "OPTIONS"}, nil, ldap.Preview)
AddPrivate(rt, "global/ldap/sync", []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)

View file

@ -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

View file

@ -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 ./...

View file

@ -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

View file

@ -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

View file

@ -1,2 +0,0 @@
test:
go test -v ./...

View file

@ -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).

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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
}

View file

@ -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
}

File diff suppressed because it is too large Load diff

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

18
vendor/gopkg.in/asn1-ber.v1/.travis.yml generated vendored Normal file
View file

@ -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 ./...

22
vendor/gopkg.in/asn1-ber.v1/LICENSE generated vendored Normal file
View file

@ -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.

24
vendor/gopkg.in/asn1-ber.v1/README.md generated vendored Normal file
View file

@ -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

504
vendor/gopkg.in/asn1-ber.v1/ber.go generated vendored Normal file
View file

@ -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
}

25
vendor/gopkg.in/asn1-ber.v1/content_int.go generated vendored Normal file
View file

@ -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
}

29
vendor/gopkg.in/asn1-ber.v1/header.go generated vendored Normal file
View file

@ -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
}

103
vendor/gopkg.in/asn1-ber.v1/identifier.go generated vendored Normal file
View file

@ -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<<highBit) != 0 {
break
}
highBit--
}
tagBytes := int(math.Ceil(float64(highBit) / 7.0))
for i := tagBytes - 1; i >= 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
}

81
vendor/gopkg.in/asn1-ber.v1/length.go generated vendored Normal file
View file

@ -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
}

24
vendor/gopkg.in/asn1-ber.v1/util.go generated vendored Normal file
View file

@ -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
}

0
vendor/gopkg.in/ldap.v2/.gitignore generated vendored Normal file
View file

31
vendor/gopkg.in/ldap.v2/.travis.yml generated vendored Normal file
View file

@ -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

View file

@ -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.

52
vendor/gopkg.in/ldap.v2/Makefile generated vendored Normal file
View file

@ -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

53
vendor/gopkg.in/ldap.v2/README.md generated vendored Normal file
View file

@ -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

113
vendor/gopkg.in/ldap.v2/add.go generated vendored Normal file
View file

@ -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
}

13
vendor/gopkg.in/ldap.v2/atomic_value.go generated vendored Normal file
View file

@ -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
}

28
vendor/gopkg.in/ldap.v2/atomic_value_go13.go generated vendored Normal file
View file

@ -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
}

143
vendor/gopkg.in/ldap.v2/bind.go generated vendored Normal file
View file

@ -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
}

27
vendor/gopkg.in/ldap.v2/client.go generated vendored Normal file
View file

@ -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)
}

85
vendor/gopkg.in/ldap.v2/compare.go generated vendored Normal file
View file

@ -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 <attributedescription>
// -- [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)
}

470
vendor/gopkg.in/ldap.v2/conn.go generated vendored Normal file
View file

@ -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
}
}
}

420
vendor/gopkg.in/ldap.v2/control.go generated vendored Normal file
View file

@ -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
}

24
vendor/gopkg.in/ldap.v2/debug.go generated vendored Normal file
View file

@ -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)
}
}

Some files were not shown because too many files have changed in this diff Show more