mirror of
https://github.com/documize/community.git
synced 2025-07-20 05:39:42 +02:00
Merge pull request #167 from documize/ldap
Native LDAP and Active Directory sync and authentication
This commit is contained in:
commit
53e4861ded
109 changed files with 7003 additions and 4943 deletions
26
Gopkg.lock
generated
26
Gopkg.lock
generated
|
@ -1,14 +1,6 @@
|
||||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
# 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]]
|
[[projects]]
|
||||||
digest = "1:606d068450c82b9ddaa21de992f73563754077f0f411235cdfe71d0903a268c3"
|
digest = "1:606d068450c82b9ddaa21de992f73563754077f0f411235cdfe71d0903a268c3"
|
||||||
name = "github.com/codegangsta/negroni"
|
name = "github.com/codegangsta/negroni"
|
||||||
|
@ -250,11 +242,26 @@
|
||||||
revision = "0298784c4606cdf01e99644da115863c052a737c"
|
revision = "0298784c4606cdf01e99644da115863c052a737c"
|
||||||
version = "v1.5.0"
|
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]
|
[solve-meta]
|
||||||
analyzer-name = "dep"
|
analyzer-name = "dep"
|
||||||
analyzer-version = 1
|
analyzer-version = 1
|
||||||
input-imports = [
|
input-imports = [
|
||||||
"github.com/andygrunwald/go-jira",
|
|
||||||
"github.com/codegangsta/negroni",
|
"github.com/codegangsta/negroni",
|
||||||
"github.com/dgrijalva/jwt-go",
|
"github.com/dgrijalva/jwt-go",
|
||||||
"github.com/documize/blackfriday",
|
"github.com/documize/blackfriday",
|
||||||
|
@ -275,6 +282,7 @@
|
||||||
"golang.org/x/oauth2",
|
"golang.org/x/oauth2",
|
||||||
"gopkg.in/alexcesaro/quotedprintable.v3",
|
"gopkg.in/alexcesaro/quotedprintable.v3",
|
||||||
"gopkg.in/andygrunwald/go-jira.v1",
|
"gopkg.in/andygrunwald/go-jira.v1",
|
||||||
|
"gopkg.in/ldap.v2",
|
||||||
]
|
]
|
||||||
solver-name = "gps-cdcl"
|
solver-name = "gps-cdcl"
|
||||||
solver-version = 1
|
solver-version = 1
|
||||||
|
|
|
@ -89,14 +89,10 @@
|
||||||
go-tests = true
|
go-tests = true
|
||||||
unused-packages = true
|
unused-packages = true
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/documize/unidecode"
|
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
name = "github.com/documize/slug"
|
name = "github.com/documize/slug"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
name = "github.com/andygrunwald/go-jira"
|
name = "gopkg.in/andygrunwald/go-jira.v1"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
|
|
@ -58,9 +58,9 @@ Space view.
|
||||||
|
|
||||||
## Latest version
|
## 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
|
## 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.
|
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
|
### Keycloak Integration
|
||||||
|
|
||||||
Documize provides out-of-the-box integration with [Redhat Keycloak](http://www.keycloak.org) for open source identity and access management.
|
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
90
domain/auth/add.go
Normal 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
|
||||||
|
}
|
|
@ -64,7 +64,7 @@ func (h *Handler) Sync(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exit if not using Keycloak
|
// 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.Message = "Error: skipping user sync with Keycloak as it is not the configured option"
|
||||||
result.IsError = true
|
result.IsError = true
|
||||||
response.WriteJSON(w, result)
|
response.WriteJSON(w, result)
|
||||||
|
@ -73,7 +73,7 @@ func (h *Handler) Sync(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make Keycloak auth provider config
|
// Make Keycloak auth provider config
|
||||||
c := keycloakConfig{}
|
c := ath.KeycloakConfig{}
|
||||||
err = json.Unmarshal([]byte(org.AuthConfig), &c)
|
err = json.Unmarshal([]byte(org.AuthConfig), &c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Message = "Error: unable read Keycloak configuration data"
|
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)
|
insert = append(insert, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track the number of Keycloak users with missing data.
|
// Track the number of Keycloak users with missing data.
|
||||||
missing := 0
|
missing := 0
|
||||||
|
|
||||||
|
@ -129,7 +130,7 @@ func (h *Handler) Sync(w http.ResponseWriter, r *http.Request) {
|
||||||
if len(u.Email) == 0 {
|
if len(u.Email) == 0 {
|
||||||
missing++
|
missing++
|
||||||
} else {
|
} 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
a := keycloakAuthRequest{}
|
a := ath.KeycloakAuthRequest{}
|
||||||
err = json.Unmarshal(body, &a)
|
err = json.Unmarshal(body, &a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.WriteBadRequestError(w, method, err.Error())
|
response.WriteBadRequestError(w, method, err.Error())
|
||||||
|
@ -181,7 +182,7 @@ func (h *Handler) Authenticate(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx.OrgID = org.RefID
|
ctx.OrgID = org.RefID
|
||||||
|
|
||||||
// Fetch Keycloak auth provider config
|
// Fetch Keycloak auth provider config
|
||||||
ac := keycloakConfig{}
|
ac := ath.KeycloakConfig{}
|
||||||
err = json.Unmarshal([]byte(org.AuthConfig), &ac)
|
err = json.Unmarshal([]byte(org.AuthConfig), &ac)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.WriteBadRequestError(w, method, "Unable to unmarshall Keycloak Public Key")
|
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.Salt = secrets.GenerateSalt()
|
||||||
u.Password = secrets.GeneratePassword(secrets.GenerateRandomPassword(), u.Salt)
|
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 {
|
if err != nil {
|
||||||
response.WriteServerError(w, method, err)
|
response.WriteServerError(w, method, err)
|
||||||
h.Runtime.Log.Error(method, err)
|
h.Runtime.Log.Error(method, err)
|
||||||
|
|
|
@ -13,7 +13,6 @@ package keycloak
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -22,18 +21,14 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/documize/community/core/env"
|
|
||||||
"github.com/documize/community/core/stringutil"
|
"github.com/documize/community/core/stringutil"
|
||||||
"github.com/documize/community/core/uniqueid"
|
"github.com/documize/community/model/auth"
|
||||||
"github.com/documize/community/domain"
|
|
||||||
usr "github.com/documize/community/domain/user"
|
|
||||||
"github.com/documize/community/model/account"
|
|
||||||
"github.com/documize/community/model/user"
|
"github.com/documize/community/model/user"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Fetch gets list of Keycloak users for specified Realm, Client Id
|
// 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{}
|
users = []user.User{}
|
||||||
|
|
||||||
form := url.Values{}
|
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)
|
return users, errors.New("Keycloak authentication failed " + res.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
ka := keycloakAPIAuth{}
|
ka := auth.KeycloakAPIAuth{}
|
||||||
err = json.Unmarshal(body, &ka)
|
err = json.Unmarshal(body, &ka)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return users, err
|
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)
|
return users, errors.New("Keycloak users list call failed " + res.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
kcUsers := []keycloakUser{}
|
kcUsers := []auth.KeycloakUser{}
|
||||||
err = json.Unmarshal(body, &kcUsers)
|
err = json.Unmarshal(body, &kcUsers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.Wrap(err, "cannot unmarshal Keycloak user list response")
|
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
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
123
domain/auth/ldap/ad_test.go
Normal 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")
|
||||||
|
}
|
373
domain/auth/ldap/endpoint.go
Normal file
373
domain/auth/ldap/endpoint.go
Normal 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
297
domain/auth/ldap/ldap.go
Normal 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
|
||||||
|
}
|
139
domain/auth/ldap/local_test.go
Normal file
139
domain/auth/ldap/local_test.go
Normal 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")
|
||||||
|
}
|
123
domain/auth/ldap/public_test.go
Normal file
123
domain/auth/ldap/public_test.go
Normal 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")
|
||||||
|
}
|
|
@ -21,10 +21,9 @@ import (
|
||||||
// StripAuthSecrets removes sensitive data from auth provider configuration
|
// StripAuthSecrets removes sensitive data from auth provider configuration
|
||||||
func StripAuthSecrets(r *env.Runtime, provider, config string) string {
|
func StripAuthSecrets(r *env.Runtime, provider, config string) string {
|
||||||
switch provider {
|
switch provider {
|
||||||
case "documize":
|
case auth.AuthProviderDocumize:
|
||||||
return config
|
return config
|
||||||
break
|
case auth.AuthProviderKeycloak:
|
||||||
case "keycloak":
|
|
||||||
c := auth.KeycloakConfig{}
|
c := auth.KeycloakConfig{}
|
||||||
err := json.Unmarshal([]byte(config), &c)
|
err := json.Unmarshal([]byte(config), &c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -42,7 +41,23 @@ func StripAuthSecrets(r *env.Runtime, provider, config string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(j)
|
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
|
return config
|
|
@ -23,7 +23,7 @@ func startRuntime() (rt *env.Runtime, s *domain.Store) {
|
||||||
rt = new(env.Runtime)
|
rt = new(env.Runtime)
|
||||||
s = new(domain.Store)
|
s = new(domain.Store)
|
||||||
|
|
||||||
rt.Log = logging.NewLogger()
|
rt.Log = logging.NewLogger(false)
|
||||||
web.Embed = embed.NewEmbedder()
|
web.Embed = embed.NewEmbedder()
|
||||||
|
|
||||||
rt.Product = env.ProdInfo{}
|
rt.Product = env.ProdInfo{}
|
||||||
|
@ -61,3 +61,5 @@ func setupContext() domain.RequestContext {
|
||||||
ctx.OrgID = "test"
|
ctx.OrgID = "test"
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For dummy user data https://www.mockaroo.com
|
||||||
|
|
|
@ -41,8 +41,8 @@ func main() {
|
||||||
// product details
|
// product details
|
||||||
rt.Product = env.ProdInfo{}
|
rt.Product = env.ProdInfo{}
|
||||||
rt.Product.Major = "1"
|
rt.Product.Major = "1"
|
||||||
rt.Product.Minor = "69"
|
rt.Product.Minor = "70"
|
||||||
rt.Product.Patch = "2"
|
rt.Product.Patch = "0"
|
||||||
rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch)
|
rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch)
|
||||||
rt.Product.Edition = "Community"
|
rt.Product.Edition = "Community"
|
||||||
rt.Product.Title = fmt.Sprintf("%s Edition", rt.Product.Edition)
|
rt.Product.Title = fmt.Sprintf("%s Edition", rt.Product.Edition)
|
||||||
|
|
1342
embed/bindata.go
1342
embed/bindata.go
File diff suppressed because one or more lines are too long
|
@ -10,7 +10,6 @@
|
||||||
// https://documize.com
|
// https://documize.com
|
||||||
|
|
||||||
import { resolve } from 'rsvp';
|
import { resolve } from 'rsvp';
|
||||||
|
|
||||||
import Base from 'ember-simple-auth/authenticators/base';
|
import Base from 'ember-simple-auth/authenticators/base';
|
||||||
|
|
||||||
export default Base.extend({
|
export default Base.extend({
|
||||||
|
|
|
@ -10,12 +10,11 @@
|
||||||
// https://documize.com
|
// https://documize.com
|
||||||
|
|
||||||
import { isPresent } from '@ember/utils';
|
import { isPresent } from '@ember/utils';
|
||||||
|
|
||||||
import { reject, resolve } from 'rsvp';
|
import { reject, resolve } from 'rsvp';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import Base from 'ember-simple-auth/authenticators/base';
|
|
||||||
import encodingUtil from '../utils/encoding';
|
import encodingUtil from '../utils/encoding';
|
||||||
import netUtil from '../utils/net';
|
import netUtil from '../utils/net';
|
||||||
|
import Base from 'ember-simple-auth/authenticators/base';
|
||||||
|
|
||||||
export default Base.extend({
|
export default Base.extend({
|
||||||
ajax: service(),
|
ajax: service(),
|
||||||
|
|
|
@ -10,11 +10,10 @@
|
||||||
// https://documize.com
|
// https://documize.com
|
||||||
|
|
||||||
import { isPresent } from '@ember/utils';
|
import { isPresent } from '@ember/utils';
|
||||||
|
|
||||||
import { reject, resolve } from 'rsvp';
|
import { reject, resolve } from 'rsvp';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import Base from 'ember-simple-auth/authenticators/base';
|
|
||||||
import netUtil from '../utils/net';
|
import netUtil from '../utils/net';
|
||||||
|
import Base from 'ember-simple-auth/authenticators/base';
|
||||||
|
|
||||||
export default Base.extend({
|
export default Base.extend({
|
||||||
ajax: service(),
|
ajax: service(),
|
||||||
|
|
60
gui/app/authenticators/ldap.js
Normal file
60
gui/app/authenticators/ldap.js
Normal 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();
|
||||||
|
}
|
||||||
|
});
|
|
@ -15,17 +15,24 @@ import { set } from '@ember/object';
|
||||||
import { copy } from '@ember/object/internals';
|
import { copy } from '@ember/object/internals';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import Notifier from '../../mixins/notifier';
|
import Notifier from '../../mixins/notifier';
|
||||||
|
import ModalMixin from '../../mixins/modal';
|
||||||
import encoding from '../../utils/encoding';
|
import encoding from '../../utils/encoding';
|
||||||
import Component from '@ember/component';
|
import Component from '@ember/component';
|
||||||
|
|
||||||
export default Component.extend(Notifier, {
|
export default Component.extend(ModalMixin, Notifier, {
|
||||||
appMeta: service(),
|
appMeta: service(),
|
||||||
|
globalSvc: service('global'),
|
||||||
|
|
||||||
isDocumizeProvider: computed('authProvider', function() {
|
isDocumizeProvider: computed('authProvider', function() {
|
||||||
return this.get('authProvider') === this.get('constants').AuthProvider.Documize;
|
return this.get('authProvider') === this.get('constants').AuthProvider.Documize;
|
||||||
}),
|
}),
|
||||||
isKeycloakProvider: computed('authProvider', function() {
|
isKeycloakProvider: computed('authProvider', function() {
|
||||||
return this.get('authProvider') === this.get('constants').AuthProvider.Keycloak;
|
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'),
|
KeycloakUrlError: empty('keycloakConfig.url'),
|
||||||
KeycloakRealmError: empty('keycloakConfig.realm'),
|
KeycloakRealmError: empty('keycloakConfig.realm'),
|
||||||
KeycloakClientIdError: empty('keycloakConfig.clientId'),
|
KeycloakClientIdError: empty('keycloakConfig.clientId'),
|
||||||
|
@ -34,8 +41,28 @@ export default Component.extend(Notifier, {
|
||||||
KeycloakAdminPasswordError: empty('keycloakConfig.adminPassword'),
|
KeycloakAdminPasswordError: empty('keycloakConfig.adminPassword'),
|
||||||
keycloakFailure: '',
|
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() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
|
||||||
this.keycloakConfig = {
|
this.keycloakConfig = {
|
||||||
url: '',
|
url: '',
|
||||||
realm: '',
|
realm: '',
|
||||||
|
@ -55,10 +82,13 @@ export default Component.extend(Notifier, {
|
||||||
let provider = this.get('authProvider');
|
let provider = this.get('authProvider');
|
||||||
let constants = this.get('constants');
|
let constants = this.get('constants');
|
||||||
|
|
||||||
|
this.set('ldapPreview', {isError: true, message: 'Unable to connect'});
|
||||||
|
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
case constants.AuthProvider.Documize:
|
case constants.AuthProvider.Documize:
|
||||||
// nothing to do
|
// nothing to do
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case constants.AuthProvider.Keycloak: // eslint-disable-line no-case-declarations
|
case constants.AuthProvider.Keycloak: // eslint-disable-line no-case-declarations
|
||||||
let config = this.get('authConfig');
|
let config = this.get('authConfig');
|
||||||
|
|
||||||
|
@ -73,6 +103,20 @@ export default Component.extend(Notifier, {
|
||||||
|
|
||||||
this.set('keycloakConfig', config);
|
this.set('keycloakConfig', config);
|
||||||
break;
|
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);
|
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() {
|
onSave() {
|
||||||
let constants = this.get('constants');
|
let constants = this.get('constants');
|
||||||
let provider = this.get('authProvider');
|
let provider = this.get('authProvider');
|
||||||
|
@ -98,6 +164,7 @@ export default Component.extend(Notifier, {
|
||||||
case constants.AuthProvider.Documize:
|
case constants.AuthProvider.Documize:
|
||||||
config = {};
|
config = {};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case constants.AuthProvider.Keycloak:
|
case constants.AuthProvider.Keycloak:
|
||||||
if (this.get('KeycloakUrlError')) {
|
if (this.get('KeycloakUrlError')) {
|
||||||
this.$("#keycloak-url").focus();
|
this.$("#keycloak-url").focus();
|
||||||
|
@ -141,6 +208,27 @@ export default Component.extend(Notifier, {
|
||||||
|
|
||||||
set(config, 'publicKey', encoding.Base64.encode(this.get('keycloakConfig.publicKey')));
|
set(config, 'publicKey', encoding.Base64.encode(this.get('keycloakConfig.publicKey')));
|
||||||
break;
|
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();
|
this.showWait();
|
||||||
|
@ -148,8 +236,11 @@ export default Component.extend(Notifier, {
|
||||||
let data = { authProvider: provider, authConfig: JSON.stringify(config) };
|
let data = { authProvider: provider, authConfig: JSON.stringify(config) };
|
||||||
|
|
||||||
this.get('onSave')(data).then(() => {
|
this.get('onSave')(data).then(() => {
|
||||||
|
// Without sync we cannot log in
|
||||||
|
|
||||||
|
// Keycloak sync process
|
||||||
if (data.authProvider === constants.AuthProvider.Keycloak) {
|
if (data.authProvider === constants.AuthProvider.Keycloak) {
|
||||||
this.get('onSync')().then((response) => {
|
this.get('onSyncKeycloak')().then((response) => {
|
||||||
if (response.isError) {
|
if (response.isError) {
|
||||||
this.set('keycloakFailure', response.message);
|
this.set('keycloakFailure', response.message);
|
||||||
console.log(response.message); // eslint-disable-line no-console
|
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();
|
this.showDone();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -240,8 +240,12 @@ export default Component.extend(AuthProvider, ModalMixin, TooltipMixin, {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onSync() {
|
onSyncKeycloak() {
|
||||||
this.get('onSync')();
|
this.get('onSyncKeycloak')();
|
||||||
|
},
|
||||||
|
|
||||||
|
onSyncLDAP() {
|
||||||
|
this.get('onSyncLDAP')();
|
||||||
},
|
},
|
||||||
|
|
||||||
onLimit(limit) {
|
onLimit(limit) {
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { empty, and } from '@ember/object/computed';
|
import { empty, and } from '@ember/object/computed';
|
||||||
import { set } from '@ember/object';
|
import { set } from '@ember/object';
|
||||||
import Component from '@ember/component';
|
|
||||||
import { isEmpty } from '@ember/utils';
|
import { isEmpty } from '@ember/utils';
|
||||||
|
import Component from '@ember/component';
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
email: "",
|
email: "",
|
||||||
|
|
|
@ -36,7 +36,7 @@ export default Component.extend(ModalMixin, {
|
||||||
|
|
||||||
this.pins = [];
|
this.pins = [];
|
||||||
|
|
||||||
if (this.get('appMeta.authProvider') === constants.AuthProvider.Keycloak) {
|
if (this.get('appMeta.authProvider') !== constants.AuthProvider.Documize) {
|
||||||
let config = this.get('appMeta.authConfig');
|
let config = this.get('appMeta.authConfig');
|
||||||
config = JSON.parse(config);
|
config = JSON.parse(config);
|
||||||
this.set('enableLogout', !config.disableLogout);
|
this.set('enableLogout', !config.disableLogout);
|
||||||
|
|
|
@ -23,7 +23,12 @@ let constants = EmberObject.extend({
|
||||||
|
|
||||||
AuthProvider: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
|
AuthProvider: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
|
||||||
Documize: 'documize',
|
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
|
DocumentActionType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
|
||||||
|
|
|
@ -16,6 +16,7 @@ export default Mixin.create({
|
||||||
appMeta: service(),
|
appMeta: service(),
|
||||||
isAuthProviderDocumize: true,
|
isAuthProviderDocumize: true,
|
||||||
IsAuthProviderKeycloak: false,
|
IsAuthProviderKeycloak: false,
|
||||||
|
IsAuthProviderLDAP: false,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
@ -23,5 +24,6 @@ export default Mixin.create({
|
||||||
|
|
||||||
this.set('isAuthProviderDocumize', this.get('appMeta.authProvider') === constants.AuthProvider.Documize);
|
this.set('isAuthProviderDocumize', this.get('appMeta.authProvider') === constants.AuthProvider.Documize);
|
||||||
this.set('isAuthProviderKeycloak', this.get('appMeta.authProvider') === constants.AuthProvider.Keycloak);
|
this.set('isAuthProviderKeycloak', this.get('appMeta.authProvider') === constants.AuthProvider.Keycloak);
|
||||||
|
this.set('isAuthProviderLDAP', this.get('appMeta.authProvider') === constants.AuthProvider.LDAP);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,11 +10,11 @@
|
||||||
// https://documize.com
|
// https://documize.com
|
||||||
|
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
|
|
||||||
export default Controller.extend({
|
export default Controller.extend({
|
||||||
userService: service('user'),
|
userService: service('user'),
|
||||||
|
appMeta: service('app-meta'),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
forgot: function (email) {
|
forgot: function (email) {
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default Route.extend({
|
||||||
beforeModel() {
|
beforeModel() {
|
||||||
let constants = this.get('constants');
|
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');
|
this.transitionTo('auth.login');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<div class="auth-box">
|
<div class="auth-box">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<img src="/assets/img/logo-purple.png" title="Documize" alt="Documize" class="img-fluid" />
|
<img src="/assets/img/logo-purple.png" title="Documize" alt="Documize" class="img-fluid" />
|
||||||
|
<div class="url">Authenticate with {{appMeta.appHost}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-form">
|
<div class="login-form">
|
||||||
{{forgot-password forgot=(action 'forgot')}}
|
{{forgot-password forgot=(action 'forgot')}}
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
// https://documize.com
|
// https://documize.com
|
||||||
|
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import Controller from '@ember/controller';
|
|
||||||
import AuthProvider from '../../../mixins/auth';
|
import AuthProvider from '../../../mixins/auth';
|
||||||
|
import Controller from '@ember/controller';
|
||||||
|
|
||||||
export default Controller.extend(AuthProvider, {
|
export default Controller.extend(AuthProvider, {
|
||||||
appMeta: service('app-meta'),
|
appMeta: service('app-meta'),
|
||||||
|
@ -19,10 +19,19 @@ export default Controller.extend(AuthProvider, {
|
||||||
invalidCredentials: false,
|
invalidCredentials: false,
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
|
if (this.get('sAuthProviderDocumize')) {
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
email: '',
|
email: '',
|
||||||
password: ''
|
password: ''
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.get('sAuthProviderLDAP')) {
|
||||||
|
this.setProperties({
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let dbhash = document.head.querySelector("[property=dbhash]").content;
|
let dbhash = document.head.querySelector("[property=dbhash]").content;
|
||||||
if (dbhash.length > 0 && dbhash !== "{{.DBhash}}") {
|
if (dbhash.length > 0 && dbhash !== "{{.DBhash}}") {
|
||||||
|
@ -32,6 +41,7 @@ export default Controller.extend(AuthProvider, {
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
login() {
|
login() {
|
||||||
|
if (this.get('isAuthProviderDocumize')) {
|
||||||
let creds = this.getProperties('email', 'password');
|
let creds = this.getProperties('email', 'password');
|
||||||
|
|
||||||
this.get('session').authenticate('authenticator:documize', creds).then((response) => {
|
this.get('session').authenticate('authenticator:documize', creds).then((response) => {
|
||||||
|
@ -41,5 +51,17 @@ export default Controller.extend(AuthProvider, {
|
||||||
this.set('invalidCredentials', true);
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,22 +3,27 @@
|
||||||
<div class="auth-box">
|
<div class="auth-box">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<img src="/assets/img/logo-purple.png" title="Documize" alt="Documize" class="img-fluid" />
|
<img src="/assets/img/logo-purple.png" title="Documize" alt="Documize" class="img-fluid" />
|
||||||
|
<div class="url">Authenticate with {{appMeta.appHost}}</div>
|
||||||
</div>
|
</div>
|
||||||
<form {{action 'login' on="submit"}}>
|
<form {{action 'login' on="submit"}}>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
{{#if isAuthProviderDocumize}}
|
||||||
<label for="authEmail">Email</label>
|
<label for="authEmail">Email</label>
|
||||||
{{focus-input type="email" value=email id="authEmail" class="form-control mousetrap" placeholder="" autocomplete="username email"}}
|
{{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>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="authPassword">Password</label>
|
<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>
|
</div>
|
||||||
<button type="submit" class="btn btn-success font-weight-bold text-uppercase mt-4">Sign in</button>
|
<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>
|
<div class="{{unless invalidCredentials "invisible"}} color-red mt-3">Invalid credentials</div>
|
||||||
{{#if isAuthProviderDocumize}}
|
{{#if isAuthProviderDocumize}}
|
||||||
<div class="mt-5">
|
|
||||||
{{#link-to 'auth.forgot'}}Forgot your password?{{/link-to}}
|
{{#link-to 'auth.forgot'}}Forgot your password?{{/link-to}}
|
||||||
</div>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,9 +10,7 @@
|
||||||
// https://documize.com
|
// https://documize.com
|
||||||
|
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
import Route from '@ember/routing/route';
|
import Route from '@ember/routing/route';
|
||||||
// import config from 'documize/config/environment';
|
|
||||||
|
|
||||||
export default Route.extend({
|
export default Route.extend({
|
||||||
session: service(),
|
session: service(),
|
||||||
|
@ -20,15 +18,6 @@ export default Route.extend({
|
||||||
|
|
||||||
activate: function () {
|
activate: function () {
|
||||||
this.get('session').invalidate().then(() => {
|
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');
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,11 +10,11 @@
|
||||||
// https://documize.com
|
// https://documize.com
|
||||||
|
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
|
|
||||||
export default Controller.extend({
|
export default Controller.extend({
|
||||||
userService: service('user'),
|
userService: service('user'),
|
||||||
|
appMeta: service('app-meta'),
|
||||||
password: "",
|
password: "",
|
||||||
passwordConfirm: "",
|
passwordConfirm: "",
|
||||||
mustMatch: false,
|
mustMatch: false,
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<div class="auth-box">
|
<div class="auth-box">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<img src="/assets/img/logo-purple.png" title="Documize" alt="Documize" class="img-fluid" />
|
<img src="/assets/img/logo-purple.png" title="Documize" alt="Documize" class="img-fluid" />
|
||||||
|
<div class="url">Authenticate with {{appMeta.appHost}}</div>
|
||||||
</div>
|
</div>
|
||||||
{{password-reset reset=(action 'reset')}}
|
{{password-reset reset=(action 'reset')}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,8 +11,8 @@
|
||||||
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import Route from '@ember/routing/route';
|
|
||||||
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
|
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
|
||||||
export default Route.extend(AuthenticatedRouteMixin, {
|
export default Route.extend(AuthenticatedRouteMixin, {
|
||||||
session: service(),
|
session: service(),
|
||||||
|
|
|
@ -11,8 +11,8 @@
|
||||||
|
|
||||||
import { Promise as EmberPromise } from 'rsvp';
|
import { Promise as EmberPromise } from 'rsvp';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import Controller from '@ember/controller';
|
|
||||||
import NotifierMixin from "../../../mixins/notifier";
|
import NotifierMixin from "../../../mixins/notifier";
|
||||||
|
import Controller from '@ember/controller';
|
||||||
|
|
||||||
export default Controller.extend(NotifierMixin, {
|
export default Controller.extend(NotifierMixin, {
|
||||||
global: service(),
|
global: service(),
|
||||||
|
@ -32,9 +32,17 @@ export default Controller.extend(NotifierMixin, {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onSync() {
|
onSyncKeycloak() {
|
||||||
return new EmberPromise((resolve) => {
|
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);
|
resolve(response);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,8 +11,8 @@
|
||||||
|
|
||||||
import { Promise as EmberPromise } from 'rsvp';
|
import { Promise as EmberPromise } from 'rsvp';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import Route from '@ember/routing/route';
|
|
||||||
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
|
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
|
||||||
export default Route.extend(AuthenticatedRouteMixin, {
|
export default Route.extend(AuthenticatedRouteMixin, {
|
||||||
appMeta: service(),
|
appMeta: service(),
|
||||||
|
@ -26,19 +26,22 @@ export default Route.extend(AuthenticatedRouteMixin, {
|
||||||
},
|
},
|
||||||
|
|
||||||
model() {
|
model() {
|
||||||
|
let constants = this.get('constants');
|
||||||
|
|
||||||
let data = {
|
let data = {
|
||||||
authProvider: this.get('appMeta.authProvider'),
|
authProvider: this.get('appMeta.authProvider'),
|
||||||
authConfig: null,
|
authConfig: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
return new EmberPromise((resolve) => {
|
return new EmberPromise((resolve) => {
|
||||||
let constants = this.get('constants');
|
|
||||||
|
|
||||||
this.get('global').getAuthConfig().then((config) => {
|
this.get('global').getAuthConfig().then((config) => {
|
||||||
switch (data.authProvider) {
|
switch (data.authProvider) {
|
||||||
case constants.AuthProvider.Keycloak:
|
case constants.AuthProvider.Keycloak:
|
||||||
data.authConfig = config;
|
data.authConfig = config;
|
||||||
break;
|
break;
|
||||||
|
case constants.AuthProvider.LDAP:
|
||||||
|
data.authConfig = config;
|
||||||
|
break;
|
||||||
case constants.AuthProvider.Documize:
|
case constants.AuthProvider.Documize:
|
||||||
data.authConfig = '';
|
data.authConfig = '';
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -1,2 +1,7 @@
|
||||||
{{customize/auth-settings authProvider=model.authProvider authConfig=model.authConfig
|
{{customize/auth-settings
|
||||||
onSave=(action 'onSave') onSync=(action 'onSync') onChange=(action 'onChange')}}
|
authProvider=model.authProvider
|
||||||
|
authConfig=model.authConfig
|
||||||
|
onSave=(action 'onSave')
|
||||||
|
onSyncLDAP=(action 'onSyncLDAP')
|
||||||
|
onSyncKeycloak=(action 'onSyncKeycloak')
|
||||||
|
onChange=(action 'onChange')}}
|
||||||
|
|
|
@ -57,9 +57,17 @@ export default Controller.extend({
|
||||||
this.loadUsers(filter);
|
this.loadUsers(filter);
|
||||||
},
|
},
|
||||||
|
|
||||||
onSync() {
|
onSyncKeycloak() {
|
||||||
this.set('syncInProgress', true);
|
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.set('syncInProgress', false);
|
||||||
this.loadUsers('');
|
this.loadUsers('');
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
{{customize/user-list users=model
|
{{customize/user-list users=model
|
||||||
syncInProgress=syncInProgress
|
syncInProgress=syncInProgress
|
||||||
userLimit=userLimit
|
userLimit=userLimit
|
||||||
onSync=(action "onSync")
|
onSyncKeycloak=(action "onSyncKeycloak")
|
||||||
|
onSyncLDAP=(action "onSyncLDAP")
|
||||||
onFilter=(action "onFilter")
|
onFilter=(action "onFilter")
|
||||||
onDelete=(action "onDelete")
|
onDelete=(action "onDelete")
|
||||||
onSave=(action "onSave")
|
onSave=(action "onSave")
|
||||||
|
|
|
@ -9,11 +9,11 @@
|
||||||
//
|
//
|
||||||
// https://documize.com
|
// https://documize.com
|
||||||
|
|
||||||
import Route from '@ember/routing/route';
|
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';
|
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';
|
||||||
import netUtil from '../utils/net';
|
import netUtil from '../utils/net';
|
||||||
import TooltipMixin from '../mixins/tooltip';
|
import TooltipMixin from '../mixins/tooltip';
|
||||||
|
import Route from '@ember/routing/route';
|
||||||
|
|
||||||
export default Route.extend(ApplicationRouteMixin, TooltipMixin, {
|
export default Route.extend(ApplicationRouteMixin, TooltipMixin, {
|
||||||
appMeta: service(),
|
appMeta: service(),
|
||||||
|
@ -27,7 +27,7 @@ export default Route.extend(ApplicationRouteMixin, TooltipMixin, {
|
||||||
let sa = this.get('session.session.authenticator');
|
let sa = this.get('session.session.authenticator');
|
||||||
|
|
||||||
return this.get('appMeta').boot(transition.targetName, window.location.href).then(data => {
|
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')) {
|
if (!this.get('appMeta.setupMode') && !this.get('appMeta.secureMode')) {
|
||||||
return this.get('session').authenticate('authenticator:anonymous', data);
|
return this.get('session').authenticate('authenticator:anonymous', data);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ export default Service.extend({
|
||||||
ajax: service(),
|
ajax: service(),
|
||||||
localStorage: service(),
|
localStorage: service(),
|
||||||
kcAuth: service(),
|
kcAuth: service(),
|
||||||
|
appHost: '',
|
||||||
apiHost: `${config.apiHost}`,
|
apiHost: `${config.apiHost}`,
|
||||||
endpoint: `${config.apiHost}/${config.apiNamespace}`,
|
endpoint: `${config.apiHost}/${config.apiNamespace}`,
|
||||||
conversionEndpoint: '',
|
conversionEndpoint: '',
|
||||||
|
@ -72,6 +73,7 @@ export default Service.extend({
|
||||||
return this.get('ajax').request('public/meta').then((response) => {
|
return this.get('ajax').request('public/meta').then((response) => {
|
||||||
this.setProperties(response);
|
this.setProperties(response);
|
||||||
this.set('version', 'v' + this.get('version'));
|
this.set('version', 'v' + this.get('version'));
|
||||||
|
this.set('appHost', window.location.host);
|
||||||
|
|
||||||
if (requestedRoute === 'secure') {
|
if (requestedRoute === 'secure') {
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
|
|
|
@ -82,9 +82,9 @@ export default Service.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
syncExternalUsers() {
|
syncKeycloak() {
|
||||||
if(this.get('sessionService.isAdmin')) {
|
if(this.get('sessionService.isAdmin')) {
|
||||||
return this.get('ajax').request(`users/sync`, {
|
return this.get('ajax').request(`global/sync/keycloak`, {
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
return 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.
|
// Returns product license.
|
||||||
searchStatus() {
|
searchStatus() {
|
||||||
if (this.get('sessionService.isGlobalAdmin')) {
|
if (this.get('sessionService.isGlobalAdmin')) {
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
// https://documize.com
|
// https://documize.com
|
||||||
|
|
||||||
import { Promise as EmberPromise } from 'rsvp';
|
import { Promise as EmberPromise } from 'rsvp';
|
||||||
import Service, { inject as service } from '@ember/service';
|
|
||||||
import netUtil from '../utils/net';
|
import netUtil from '../utils/net';
|
||||||
|
import Service, { inject as service } from '@ember/service';
|
||||||
|
|
||||||
export default Service.extend({
|
export default Service.extend({
|
||||||
sessionService: service('session'),
|
sessionService: service('session'),
|
||||||
|
|
7
gui/app/styles/bootstrap.scss
vendored
7
gui/app/styles/bootstrap.scss
vendored
|
@ -80,9 +80,14 @@ $input-btn-focus-color: rgba($color-primary, .25);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
> small {
|
> small, > div[class*="col"] > small {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> small.highlight, > div[class*="col"] > small.highlight {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: $color-orange !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// links
|
// links
|
||||||
|
|
|
@ -39,6 +39,7 @@ $color-red: #9E0D1F;
|
||||||
$color-green: #348A37;
|
$color-green: #348A37;
|
||||||
$color-blue: #2667af;
|
$color-blue: #2667af;
|
||||||
$color-goldy: #FFD700;
|
$color-goldy: #FFD700;
|
||||||
|
$color-yellow: #fff8dc;
|
||||||
$color-orange: #FFAD15;
|
$color-orange: #FFAD15;
|
||||||
|
|
||||||
// widgets
|
// widgets
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -23,7 +23,13 @@
|
||||||
|
|
||||||
> img {
|
> img {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
margin: 40px 0;
|
margin: 30px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .url {
|
||||||
|
margin: 20px 0;
|
||||||
|
color: $color-gray;
|
||||||
|
font-weight: 0.9rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,37 +7,79 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
> .option {
|
> .option {
|
||||||
@include ease-in();
|
@include border-radius(3px);
|
||||||
margin: 0 0 5px 0;
|
margin: 0 0 15px 0;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
color: $color-gray;
|
color: $color-gray;
|
||||||
background-color: $color-off-white;
|
background-color: $color-off-white;
|
||||||
|
border: 1px solid $color-gray;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
line-height: 26px;
|
line-height: 26px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $color-black;
|
> .text-header, > .text {
|
||||||
// background-color: $color-primary-light;
|
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 {
|
> .text {
|
||||||
|
@include ease-in();
|
||||||
|
color: $color-gray;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> .material-icons {
|
> .material-icons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
color: $color-white;
|
color: $color-green;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.3rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .selected {
|
> .selected {
|
||||||
color: $color-white !important;
|
> .text-header, > .text {
|
||||||
background-color: $color-link !important;
|
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%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,95 +9,252 @@
|
||||||
|
|
||||||
<div class="view-customize">
|
<div class="view-customize">
|
||||||
<form class="mt-5">
|
<form class="mt-5">
|
||||||
<div class="form-group row">
|
<div class="widget-list-picker widget-list-picker-horiz">
|
||||||
<label class="col-sm-2 col-form-label">Provider</label>
|
<ul class="options">
|
||||||
<div class="col-sm-10">
|
<li class="option {{if isDocumizeProvider 'selected'}}" {{action 'onDocumize'}}>
|
||||||
{{#ui/ui-radio selected=isDocumizeProvider onClick=(action 'onDocumize')}} Documize — email/password{{/ui/ui-radio}}
|
<div class="text-header">Documize</div>
|
||||||
{{#ui/ui-radio selected=isKeycloakProvider onClick=(action 'onKeycloak')}} Keycloak — bring your own authentication server{{/ui/ui-radio}}
|
<div class="text">Built-in email/password</div>
|
||||||
<small class="form-text text-muted">
|
{{#if isDocumizeProvider}}
|
||||||
External authentication servers, services must be accessible from the server running this Documize instance
|
<i class="material-icons">check</i>
|
||||||
</small>
|
{{/if}}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
{{#if isDocumizeProvider}}
|
||||||
|
<p>There are no settings.</p>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if isKeycloakProvider}}
|
{{#if isKeycloakProvider}}
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="keycloak-url" class="col-sm-2 col-form-label">Keycloak Server URL</label>
|
<label for="keycloak-url" class="col-sm-3 col-form-label">Keycloak Server URL</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-9">
|
||||||
{{focus-input id="keycloak-url" type="text" value=keycloakConfig.url class=(if KeycloakUrlError 'form-control is-invalid' 'form-control')}}
|
{{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>
|
<small class="form-text text-muted">e.g. http://localhost:8888/auth</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="keycloak-realm" class="col-sm-2 col-form-label">Keycloak Realm</label>
|
<label for="keycloak-realm" class="col-sm-3 col-form-label">Keycloak Realm</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-9">
|
||||||
{{input id="keycloak-realm" type="text" value=keycloakConfig.realm class=(if KeycloakRealmError 'form-control is-invalid' 'form-control')}}
|
{{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>
|
<small class="form-text text-muted">e.g. main</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="keycloak-publicKey" class="col-sm-2 col-form-label">Keycloak Realm Public Key</label>
|
<label for="keycloak-publicKey" class="col-sm-3 col-form-label">Keycloak Realm Public Key</label>
|
||||||
<div class="col-sm-10">
|
<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')}}
|
{{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 → Keys</small>
|
<small class="form-text text-muted">Copy the RSA Public Key from Realm Settings → Keys</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="keycloak-clientId" class="col-sm-2 col-form-label">Keycloak OIDC Client ID</label>
|
<label for="keycloak-clientId" class="col-sm-3 col-form-label">Keycloak OIDC Client ID</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-9">
|
||||||
{{input id="keycloak-clientId" type="text" value=keycloakConfig.clientId class=(if KeycloakClientIdError 'form-control is-invalid' 'form-control')}}
|
{{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>
|
<small class="form-text text-muted">e.g. account</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="keycloak-group" class="col-sm-2 col-form-label">Keycloak Group ID (Optional)</label>
|
<label for="keycloak-group" class="col-sm-3 col-form-label">Keycloak Group ID (Optional)</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-9">
|
||||||
{{input id="keycloak-group" type="text" value=keycloakConfig.group class="form-control"}}
|
{{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>
|
<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>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="keycloak-admin-user" class="col-sm-2 col-form-label">Keycloak Username</label>
|
<label for="keycloak-admin-user" class="col-sm-3 col-form-label">Keycloak Username</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-9">
|
||||||
{{input id="keycloak-admin-user" type="text" value=keycloakConfig.adminUser class=(if KeycloakAdminUserError 'form-control is-invalid' 'form-control')}}
|
{{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
|
<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>
|
against Realm specified above)</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="keycloak-admin-password" class="col-sm-2 col-form-label">Keycloak Password</label>
|
<label for="keycloak-admin-password" class="col-sm-3 col-form-label">Keycloak Password</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-9">
|
||||||
{{input id="keycloak-admin-password" type="password" value=keycloakConfig.adminPassword class=(if KeycloakAdminPasswordError 'form-control is-invalid' 'form-control')}}
|
{{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>
|
<small class="form-text text-muted">Used to connect with Keycloak and sync users with Documize</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="keycloak-admin-password" class="col-sm-2 col-form-label">Logout</label>
|
<label class="col-sm-3 col-form-label">Disable Logout</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-9">
|
||||||
<div class="form-check">
|
{{x-toggle value=keycloakConfig.disableLogout size="medium" theme="light" onToggle=(action (mut keycloakConfig.disableLogout))}}
|
||||||
{{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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="keycloak-admin-password" class="col-sm-2 col-form-label">Space Permission</label>
|
<label for="ldap-defaultPermissionAddSpace" class="col-sm-3 col-form-label">Can Create Spaces</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-9">
|
||||||
<div class="form-check">
|
{{x-toggle value=keycloakConfig.defaultPermissionAddSpace size="medium" theme="light" onToggle=(action (mut keycloakConfig.defaultPermissionAddSpace))}}
|
||||||
{{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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/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→</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="btn btn-success mt-4" {{action 'onSave'}}>ACTIVATE</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{{#if (gt keycloakFailure.length 0)}}
|
{{#if (gt keycloakFailure.length 0)}}
|
||||||
<p class="admin-setting-failure my-3">Keycloak connection failed: {{keycloakFailure}}</p>
|
<p class="admin-setting-failure my-3">Keycloak connection failed: {{keycloakFailure}}</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</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>
|
|
@ -3,7 +3,14 @@
|
||||||
{{#if syncInProgress}}
|
{{#if syncInProgress}}
|
||||||
<div class="btn btn-secondary mt-3 mb-3">Keycloak user sync running...</div>
|
<div class="btn btn-secondary mt-3 mb-3">Keycloak user sync running...</div>
|
||||||
{{else}}
|
{{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}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "documize",
|
"name": "documize",
|
||||||
"version": "1.69.2",
|
"version": "1.70.0",
|
||||||
"description": "The Document IDE",
|
"description": "The Document IDE",
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": "",
|
"repository": "",
|
||||||
|
|
|
@ -18,3 +18,14 @@ type AuthenticationModel struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
User user.User `json:"user"`
|
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
175
model/auth/ldap.go
Normal 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"`
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"github.com/documize/community/domain/attachment"
|
"github.com/documize/community/domain/attachment"
|
||||||
"github.com/documize/community/domain/auth"
|
"github.com/documize/community/domain/auth"
|
||||||
"github.com/documize/community/domain/auth/keycloak"
|
"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/block"
|
||||||
"github.com/documize/community/domain/category"
|
"github.com/documize/community/domain/category"
|
||||||
"github.com/documize/community/domain/conversion"
|
"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}
|
user := user.Handler{Runtime: rt, Store: s}
|
||||||
link := link.Handler{Runtime: rt, Store: s}
|
link := link.Handler{Runtime: rt, Store: s}
|
||||||
page := page.Handler{Runtime: rt, Store: s, Indexer: indexer}
|
page := page.Handler{Runtime: rt, Store: s, Indexer: indexer}
|
||||||
|
ldap := ldap.Handler{Runtime: rt, Store: s}
|
||||||
space := space.Handler{Runtime: rt, Store: s}
|
space := space.Handler{Runtime: rt, Store: s}
|
||||||
block := block.Handler{Runtime: rt, Store: s}
|
block := block.Handler{Runtime: rt, Store: s}
|
||||||
group := group.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/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, "authenticate", []string{"POST", "OPTIONS"}, nil, auth.Login)
|
||||||
AddPublic(rt, "validate", []string{"GET", "OPTIONS"}, nil, auth.ValidateToken)
|
AddPublic(rt, "validate", []string{"GET", "OPTIONS"}, nil, auth.ValidateToken)
|
||||||
AddPublic(rt, "forgot", []string{"POST", "OPTIONS"}, nil, user.ForgotPassword)
|
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{"GET", "OPTIONS"}, nil, user.Get)
|
||||||
AddPrivate(rt, "users/{userID}", []string{"PUT", "OPTIONS"}, nil, user.Update)
|
AddPrivate(rt, "users/{userID}", []string{"PUT", "OPTIONS"}, nil, user.Update)
|
||||||
AddPrivate(rt, "users/{userID}", []string{"DELETE", "OPTIONS"}, nil, user.Delete)
|
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/match", []string{"POST", "OPTIONS"}, nil, user.MatchUsers)
|
||||||
AddPrivate(rt, "users/import", []string{"POST", "OPTIONS"}, nil, user.BulkImport)
|
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/auth", []string{"PUT", "OPTIONS"}, nil, setting.SetAuthConfig)
|
||||||
AddPrivate(rt, "global/search/status", []string{"GET", "OPTIONS"}, nil, meta.SearchStatus)
|
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/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, "robots.txt", []string{"GET", "OPTIONS"}, nil, meta.RobotsTxt)
|
||||||
Add(rt, RoutePrefixRoot, "sitemap.xml", []string{"GET", "OPTIONS"}, nil, meta.Sitemap)
|
Add(rt, RoutePrefixRoot, "sitemap.xml", []string{"GET", "OPTIONS"}, nil, meta.Sitemap)
|
||||||
|
|
29
vendor/github.com/andygrunwald/go-jira/.gitignore
generated
vendored
29
vendor/github.com/andygrunwald/go-jira/.gitignore
generated
vendored
|
@ -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
|
|
17
vendor/github.com/andygrunwald/go-jira/.travis.yml
generated
vendored
17
vendor/github.com/andygrunwald/go-jira/.travis.yml
generated
vendored
|
@ -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 ./...
|
|
36
vendor/github.com/andygrunwald/go-jira/Gopkg.lock
generated
vendored
36
vendor/github.com/andygrunwald/go-jira/Gopkg.lock
generated
vendored
|
@ -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
|
|
46
vendor/github.com/andygrunwald/go-jira/Gopkg.toml
generated
vendored
46
vendor/github.com/andygrunwald/go-jira/Gopkg.toml
generated
vendored
|
@ -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
|
|
2
vendor/github.com/andygrunwald/go-jira/Makefile
generated
vendored
2
vendor/github.com/andygrunwald/go-jira/Makefile
generated
vendored
|
@ -1,2 +0,0 @@
|
||||||
test:
|
|
||||||
go test -v ./...
|
|
271
vendor/github.com/andygrunwald/go-jira/README.md
generated
vendored
271
vendor/github.com/andygrunwald/go-jira/README.md
generated
vendored
|
@ -1,271 +0,0 @@
|
||||||
# go-jira
|
|
||||||
|
|
||||||
[](https://godoc.org/github.com/andygrunwald/go-jira)
|
|
||||||
[](https://travis-ci.org/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).
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 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).
|
|
187
vendor/github.com/andygrunwald/go-jira/authentication.go
generated
vendored
187
vendor/github.com/andygrunwald/go-jira/authentication.go
generated
vendored
|
@ -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
|
|
||||||
}
|
|
204
vendor/github.com/andygrunwald/go-jira/board.go
generated
vendored
204
vendor/github.com/andygrunwald/go-jira/board.go
generated
vendored
|
@ -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
|
|
||||||
}
|
|
38
vendor/github.com/andygrunwald/go-jira/component.go
generated
vendored
38
vendor/github.com/andygrunwald/go-jira/component.go
generated
vendored
|
@ -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
|
|
||||||
}
|
|
82
vendor/github.com/andygrunwald/go-jira/error.go
generated
vendored
82
vendor/github.com/andygrunwald/go-jira/error.go
generated
vendored
|
@ -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()
|
|
||||||
}
|
|
43
vendor/github.com/andygrunwald/go-jira/field.go
generated
vendored
43
vendor/github.com/andygrunwald/go-jira/field.go
generated
vendored
|
@ -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
|
|
||||||
}
|
|
154
vendor/github.com/andygrunwald/go-jira/group.go
generated
vendored
154
vendor/github.com/andygrunwald/go-jira/group.go
generated
vendored
|
@ -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
|
|
||||||
}
|
|
1090
vendor/github.com/andygrunwald/go-jira/issue.go
generated
vendored
1090
vendor/github.com/andygrunwald/go-jira/issue.go
generated
vendored
File diff suppressed because it is too large
Load diff
451
vendor/github.com/andygrunwald/go-jira/jira.go
generated
vendored
451
vendor/github.com/andygrunwald/go-jira/jira.go
generated
vendored
|
@ -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
|
|
||||||
}
|
|
194
vendor/github.com/andygrunwald/go-jira/metaissue.go
generated
vendored
194
vendor/github.com/andygrunwald/go-jira/metaissue.go
generated
vendored
|
@ -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
|
|
||||||
}
|
|
37
vendor/github.com/andygrunwald/go-jira/priority.go
generated
vendored
37
vendor/github.com/andygrunwald/go-jira/priority.go
generated
vendored
|
@ -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
|
|
||||||
}
|
|
162
vendor/github.com/andygrunwald/go-jira/project.go
generated
vendored
162
vendor/github.com/andygrunwald/go-jira/project.go
generated
vendored
|
@ -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
|
|
||||||
}
|
|
35
vendor/github.com/andygrunwald/go-jira/resolution.go
generated
vendored
35
vendor/github.com/andygrunwald/go-jira/resolution.go
generated
vendored
|
@ -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
|
|
||||||
}
|
|
107
vendor/github.com/andygrunwald/go-jira/sprint.go
generated
vendored
107
vendor/github.com/andygrunwald/go-jira/sprint.go
generated
vendored
|
@ -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
|
|
||||||
}
|
|
44
vendor/github.com/andygrunwald/go-jira/statuscategory.go
generated
vendored
44
vendor/github.com/andygrunwald/go-jira/statuscategory.go
generated
vendored
|
@ -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
|
|
||||||
}
|
|
136
vendor/github.com/andygrunwald/go-jira/user.go
generated
vendored
136
vendor/github.com/andygrunwald/go-jira/user.go
generated
vendored
|
@ -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
|
|
||||||
}
|
|
96
vendor/github.com/andygrunwald/go-jira/version.go
generated
vendored
96
vendor/github.com/andygrunwald/go-jira/version.go
generated
vendored
|
@ -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
18
vendor/gopkg.in/asn1-ber.v1/.travis.yml
generated
vendored
Normal 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
22
vendor/gopkg.in/asn1-ber.v1/LICENSE
generated
vendored
Normal 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
24
vendor/gopkg.in/asn1-ber.v1/README.md
generated
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
[](https://godoc.org/gopkg.in/asn1-ber.v1) [](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
504
vendor/gopkg.in/asn1-ber.v1/ber.go
generated
vendored
Normal 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
25
vendor/gopkg.in/asn1-ber.v1/content_int.go
generated
vendored
Normal 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
29
vendor/gopkg.in/asn1-ber.v1/header.go
generated
vendored
Normal 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
103
vendor/gopkg.in/asn1-ber.v1/identifier.go
generated
vendored
Normal 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
81
vendor/gopkg.in/asn1-ber.v1/length.go
generated
vendored
Normal 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
24
vendor/gopkg.in/asn1-ber.v1/util.go
generated
vendored
Normal 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
0
vendor/gopkg.in/ldap.v2/.gitignore
generated
vendored
Normal file
31
vendor/gopkg.in/ldap.v2/.travis.yml
generated
vendored
Normal file
31
vendor/gopkg.in/ldap.v2/.travis.yml
generated
vendored
Normal 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
|
4
vendor/github.com/andygrunwald/go-jira/LICENSE → vendor/gopkg.in/ldap.v2/LICENSE
generated
vendored
4
vendor/github.com/andygrunwald/go-jira/LICENSE → vendor/gopkg.in/ldap.v2/LICENSE
generated
vendored
|
@ -1,6 +1,7 @@
|
||||||
The MIT License (MIT)
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
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,
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
52
vendor/gopkg.in/ldap.v2/Makefile
generated
vendored
Normal file
52
vendor/gopkg.in/ldap.v2/Makefile
generated
vendored
Normal 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
53
vendor/gopkg.in/ldap.v2/README.md
generated
vendored
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
[](https://godoc.org/gopkg.in/ldap.v2)
|
||||||
|
[](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
113
vendor/gopkg.in/ldap.v2/add.go
generated
vendored
Normal 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
13
vendor/gopkg.in/ldap.v2/atomic_value.go
generated
vendored
Normal 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
28
vendor/gopkg.in/ldap.v2/atomic_value_go13.go
generated
vendored
Normal 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
143
vendor/gopkg.in/ldap.v2/bind.go
generated
vendored
Normal 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
27
vendor/gopkg.in/ldap.v2/client.go
generated
vendored
Normal 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
85
vendor/gopkg.in/ldap.v2/compare.go
generated
vendored
Normal 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
470
vendor/gopkg.in/ldap.v2/conn.go
generated
vendored
Normal 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
420
vendor/gopkg.in/ldap.v2/control.go
generated
vendored
Normal 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
24
vendor/gopkg.in/ldap.v2/debug.go
generated
vendored
Normal 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
Loading…
Add table
Add a link
Reference in a new issue