diff --git a/Gopkg.lock b/Gopkg.lock
index 96bba98..a745dad 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -43,6 +43,12 @@
revision = "ca9ada44574153444b00d3fd9c8559e4cc95f896"
version = "v1.1"
+[[projects]]
+ branch = "master"
+ name = "github.com/jda/go-crowd"
+ packages = ["."]
+ revision = "415c27e65cd496563601465cfe630d4b3245d70f"
+
[[projects]]
branch = "master"
name = "github.com/juju/errors"
@@ -95,6 +101,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
- inputs-digest = "8d56acebac43d560504fd8420c68a9021e29a60ee6b09d477ddc0d2e815a5606"
+ inputs-digest = "c60c92a35a0972af226bbe9e4e3638d032a0ad26646fa6ac68919f2b3b805e82"
solver-name = "gps-cdcl"
solver-version = 1
diff --git a/README.md b/README.md
index 964803e..521faf2 100644
--- a/README.md
+++ b/README.md
@@ -145,6 +145,20 @@ Each `rules` entry has two mandantory and three optional fields of which at leas
The `allow` and `deny` directives are arrays of users and groups. Groups are prefixed using an `@` sign. There is a simple logic: Users before groups, denies before allows. So if you allow the group `@test` containing the user `mike` but deny the user `mike`, mike will not be able to access the matching sites.
+### Provider configuration: Atlassian Crowd (`crowd`)
+
+The crowd auth provider connects nginx-sso with an Atlassian Crowd directory server. The SSO authentication cookie used by Jira and Confluence is also used by nginx-sso which means a login in Jira will also perform a login on nginx-sso and vice versa.
+
+```yaml
+providers:
+ crowd:
+ url: "https://crowd.example.com/crowd/"
+ app_name: ""
+ app_pass: ""
+```
+
+The configuration is quite simple: Create an application in Crowd, enter the Crowd URL and the application credentials into the config and you're done.
+
### Provider configuration: Simple Auth (`simple`)
The simple auth provider consists of a static mapping between users and passwords and groups and users. This can be seen as the replacement of htpasswd files.
diff --git a/auth_crowd.go b/auth_crowd.go
new file mode 100644
index 0000000..213354a
--- /dev/null
+++ b/auth_crowd.go
@@ -0,0 +1,178 @@
+package main
+
+import (
+ "net/http"
+ "strings"
+
+ crowd "github.com/jda/go-crowd"
+ log "github.com/sirupsen/logrus"
+ yaml "gopkg.in/yaml.v2"
+)
+
+func init() {
+ registerAuthenticator(&authCrowd{})
+}
+
+type authCrowd struct {
+ URL string `yaml:"url"`
+ AppName string `yaml:"app_name"`
+ AppPassword string `yaml:"app_pass"`
+
+ crowd crowd.Crowd
+}
+
+// AuthenticatorID needs to return an unique string to identify
+// this special authenticator
+func (a authCrowd) AuthenticatorID() string { return "crowd" }
+
+// Configure loads the configuration for the Authenticator from the
+// global config.yaml file which is passed as a byte-slice.
+// If no configuration for the Authenticator is supplied the function
+// needs to return the errAuthenticatorUnconfigured
+func (a *authCrowd) Configure(yamlSource []byte) error {
+ envelope := struct {
+ Providers struct {
+ Crowd *authCrowd `yaml:"crowd"`
+ } `yaml:"providers"`
+ }{}
+
+ if err := yaml.Unmarshal(yamlSource, &envelope); err != nil {
+ return err
+ }
+
+ if envelope.Providers.Crowd == nil {
+ return errAuthenticatorUnconfigured
+ }
+
+ a.URL = envelope.Providers.Crowd.URL
+ a.AppName = envelope.Providers.Crowd.AppName
+ a.AppPassword = envelope.Providers.Crowd.AppPassword
+
+ if a.AppName == "" || a.AppPassword == "" {
+ return errAuthenticatorUnconfigured
+ }
+
+ var err error
+ a.crowd, err = crowd.New(a.AppName, a.AppPassword, a.URL)
+
+ return err
+}
+
+// DetectUser is used to detect a user without a login form from
+// a cookie, header or other methods
+// If no user was detected the errNoValidUserFound needs to be
+// returned
+func (a authCrowd) DetectUser(res http.ResponseWriter, r *http.Request) (string, []string, error) {
+ cc, err := a.crowd.GetCookieConfig()
+ if err != nil {
+ return "", nil, err
+ }
+
+ cookie, err := r.Cookie(cc.Name)
+ switch err {
+ case nil:
+ // Fine, we do have a cookie
+ case http.ErrNoCookie:
+ // Also fine, there is no cookie
+ return "", nil, errNoValidUserFound
+ default:
+ return "", nil, err
+ }
+
+ ssoToken := cookie.Value
+ sess, err := a.crowd.GetSession(ssoToken)
+ if err != nil {
+ log.WithError(err).Debug("Getting crowd session failed")
+ return "", nil, errNoValidUserFound
+ }
+
+ user := sess.User.UserName
+ cGroups, err := a.crowd.GetDirectGroups(user)
+ if err != nil {
+ return "", nil, err
+ }
+
+ groups := []string{}
+ for _, g := range cGroups {
+ groups = append(groups, g.Name)
+ }
+
+ return user, groups, nil
+}
+
+// Login is called when the user submits the login form and needs
+// to authenticate the user or throw an error. If the user has
+// successfully logged in the persistent cookie should be written
+// in order to use DetectUser for the next login.
+// If the user did not login correctly the errNoValidUserFound
+// needs to be returned
+func (a authCrowd) Login(res http.ResponseWriter, r *http.Request) error {
+ username := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "username"}, "-"))
+ password := r.FormValue(strings.Join([]string{a.AuthenticatorID(), "password"}, "-"))
+
+ cc, err := a.crowd.GetCookieConfig()
+ if err != nil {
+ return err
+ }
+
+ sess, err := a.crowd.NewSession(username, password, r.RemoteAddr)
+ if err != nil {
+ log.WithFields(log.Fields{
+ "username": username,
+ }).WithError(err).Debug("Crowd authentication failed")
+ return errNoValidUserFound
+ }
+
+ http.SetCookie(res, &http.Cookie{
+ Name: cc.Name,
+ Value: sess.Token,
+ Path: "/",
+ Domain: cc.Domain,
+ Expires: sess.Expires,
+ Secure: cc.Secure,
+ HttpOnly: true,
+ })
+
+ return nil
+}
+
+// LoginFields needs to return the fields required for this login
+// method. If no login using this method is possible the function
+// needs to return nil.
+func (a authCrowd) LoginFields() (fields []loginField) {
+ return []loginField{
+ {
+ Label: "Username",
+ Name: "username",
+ Placeholder: "Username",
+ Type: "text",
+ },
+ {
+ Label: "Password",
+ Name: "password",
+ Placeholder: "****",
+ Type: "password",
+ },
+ }
+}
+
+// Logout is called when the user visits the logout endpoint and
+// needs to destroy any persistent stored cookies
+func (a authCrowd) Logout(res http.ResponseWriter, r *http.Request) (err error) {
+ cc, err := a.crowd.GetCookieConfig()
+ if err != nil {
+ return err
+ }
+
+ http.SetCookie(res, &http.Cookie{
+ Name: cc.Name,
+ Value: "",
+ Path: "/",
+ Domain: cc.Domain,
+ MaxAge: -1,
+ Secure: cc.Secure,
+ HttpOnly: true,
+ })
+
+ return nil
+}
diff --git a/config.yaml b/config.yaml
index 74fc9ea..e7b2b90 100644
--- a/config.yaml
+++ b/config.yaml
@@ -29,6 +29,13 @@ acl:
allow: ["luzifer", "@admins"]
providers:
+ # Authentication against an Atlassian Crowd directory server
+ # Supports: Users, Groups
+ crowd:
+ url: "https://crowd.example.com/crowd/"
+ app_name: ""
+ app_pass: ""
+
# Authentication against embedded user database
# Supports: Users, Groups
simple:
diff --git a/vendor/github.com/jda/go-crowd/LICENSE b/vendor/github.com/jda/go-crowd/LICENSE
new file mode 100644
index 0000000..6a4d0e0
--- /dev/null
+++ b/vendor/github.com/jda/go-crowd/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Jonathan Auer
+
+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.
\ No newline at end of file
diff --git a/vendor/github.com/jda/go-crowd/README.md b/vendor/github.com/jda/go-crowd/README.md
new file mode 100644
index 0000000..6ea1f0a
--- /dev/null
+++ b/vendor/github.com/jda/go-crowd/README.md
@@ -0,0 +1,23 @@
+go-crowd
+========
+Go library for interacting with [Atlassian Crowd](https://www.atlassian.com/software/crowd/)
+
+* [![GoDoc](https://godoc.org/github.com/jda/go-crowd?status.png)](http://godoc.org/github.com/jda/go-crowd)
+* Crowd [API Documentation](https://developer.atlassian.com/display/CROWDDEV/Remote+API+Reference)
+
+## Client example
+```go
+client, err := crowd.New("crowd_app_user",
+ "crowd_app_password",
+ "crowd service URL")
+
+user, err := client.Authenticate("testuser", "testpass")
+if err != nil {
+ /*
+ failure or reject from crowd. check if err = reason from
+ https://developer.atlassian.com/display/CROWDDEV/Using+the+Crowd+REST+APIs#UsingtheCrowdRESTAPIs-HTTPResponseCodesandErrorResponses
+ */
+}
+
+// if auth successful, user contains user information
+```
\ No newline at end of file
diff --git a/vendor/github.com/jda/go-crowd/auth.go b/vendor/github.com/jda/go-crowd/auth.go
new file mode 100644
index 0000000..2336a4b
--- /dev/null
+++ b/vendor/github.com/jda/go-crowd/auth.go
@@ -0,0 +1,70 @@
+package crowd
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+)
+
+type authReq struct {
+ XMLName struct{} `xml:"password"`
+ Password string `xml:"value"`
+}
+
+// Authenticate a user & password against Crowd. Returns error on failure
+// or account lockout. Success is a populated User with nil error.
+func (c *Crowd) Authenticate(user string, pass string) (User, error) {
+ u := User{}
+
+ ar := authReq{Password: pass}
+ arEncoded, err := xml.Marshal(ar)
+ if err != nil {
+ return u, err
+ }
+ arBuf := bytes.NewBuffer(arEncoded)
+
+ v := url.Values{}
+ v.Set("username", user)
+ url := c.url + "rest/usermanagement/1/authentication?" + v.Encode()
+
+ client := http.Client{Jar: c.cookies}
+ req, err := http.NewRequest("POST", url, arBuf)
+ if err != nil {
+ return u, err
+ }
+ req.SetBasicAuth(c.user, c.passwd)
+ req.Header.Set("Accept", "application/xml")
+ req.Header.Set("Content-Type", "application/xml")
+ resp, err := client.Do(req)
+ if err != nil {
+ return u, err
+ }
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return u, err
+ }
+
+ switch resp.StatusCode {
+ case 400:
+ er := Error{}
+ err = xml.Unmarshal(body, &er)
+ if err != nil {
+ return u, err
+ }
+
+ return u, fmt.Errorf("%s", er.Reason)
+ case 200:
+ err = xml.Unmarshal(body, &u)
+ if err != nil {
+ return u, err
+ }
+ default:
+ return u, fmt.Errorf("request failed: %s\n", resp.Status)
+ }
+
+ return u, nil
+}
diff --git a/vendor/github.com/jda/go-crowd/auth_test.go b/vendor/github.com/jda/go-crowd/auth_test.go
new file mode 100644
index 0000000..e5a75b3
--- /dev/null
+++ b/vendor/github.com/jda/go-crowd/auth_test.go
@@ -0,0 +1,34 @@
+package crowd
+
+import (
+ "os"
+ "testing"
+)
+
+func TestAuthentication(t *testing.T) {
+ tv := PrepVars(t)
+ c, err := New(tv.AppUsername, tv.AppPassword, tv.AppURL)
+ if err != nil {
+ t.Error(err)
+ }
+
+ user := os.Getenv("APP_USER_USERNAME")
+ if user == "" {
+ t.Skip("Can't run test because APP_USER_USERNAME undefined")
+ }
+
+ passwd := os.Getenv("APP_USER_PASSWORD")
+ if passwd == "" {
+ t.Skip("Can't run test because APP_USER_PASSWORD undefined")
+ }
+
+ a, err := c.Authenticate(user, passwd)
+ if err != nil {
+ t.Error(err)
+ }
+ t.Logf("Got: %+v\n", a)
+
+ if a.UserName == "" {
+ t.Errorf("UserName was empty so we didn't get/decode a response")
+ }
+}
diff --git a/vendor/github.com/jda/go-crowd/base.go b/vendor/github.com/jda/go-crowd/base.go
new file mode 100644
index 0000000..128b9cb
--- /dev/null
+++ b/vendor/github.com/jda/go-crowd/base.go
@@ -0,0 +1,41 @@
+// Package crowd provides methods for interacting with the
+// Atlassian Crowd authentication, directory integration, and
+// Single Sign-On system.
+package crowd
+
+import (
+ "net/http"
+ "net/http/cookiejar"
+)
+
+// Crowd represents your Crowd (client) Application settings
+type Crowd struct {
+ user string
+ passwd string
+ url string
+ cookies http.CookieJar
+}
+
+// New initializes & returns a Crowd object.
+func New(appuser string, apppass string, baseurl string) (Crowd, error) {
+ cr := Crowd{
+ user: appuser,
+ passwd: apppass,
+ url: baseurl,
+ }
+
+ // TODO make sure URL ends with '/'
+
+ cj, err := cookiejar.New(nil)
+ if err != nil {
+ return cr, err
+ }
+
+ cr.cookies = cj
+
+ return cr, nil
+}
+
+func (c *Crowd) get() {
+
+}
diff --git a/vendor/github.com/jda/go-crowd/base_test.go b/vendor/github.com/jda/go-crowd/base_test.go
new file mode 100644
index 0000000..c6f9d18
--- /dev/null
+++ b/vendor/github.com/jda/go-crowd/base_test.go
@@ -0,0 +1,40 @@
+package crowd
+
+import (
+ "os"
+ "testing"
+)
+
+type TestVars struct {
+ AppUsername string
+ AppPassword string
+ AppURL string
+}
+
+// Make sure we have the env vars to run, handle bailing if we don't
+func PrepVars(t *testing.T) TestVars {
+ var tv TestVars
+
+ appU := os.Getenv("APP_USERNAME")
+ if appU == "" {
+ t.Skip("Can't run test because APP_USERNAME undefined")
+ } else {
+ tv.AppUsername = appU
+ }
+
+ appP := os.Getenv("APP_PASSWORD")
+ if appP == "" {
+ t.Skip("Can't run test because APP_PASSWORD undefined")
+ } else {
+ tv.AppPassword = appP
+ }
+
+ appURL := os.Getenv("APP_URL")
+ if appURL == "" {
+ t.Skip("Can't run test because APP_URL undefined")
+ } else {
+ tv.AppURL = appURL
+ }
+
+ return tv
+}
diff --git a/vendor/github.com/jda/go-crowd/cookie.go b/vendor/github.com/jda/go-crowd/cookie.go
new file mode 100644
index 0000000..67343a7
--- /dev/null
+++ b/vendor/github.com/jda/go-crowd/cookie.go
@@ -0,0 +1,50 @@
+package crowd
+
+import (
+ "encoding/xml"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+)
+
+// CookieConfig holds configuration values needed to set a Crowd SSO cookie.
+type CookieConfig struct {
+ XMLName struct{} `xml:"cookie-config"`
+ Domain string `xml:"domain"`
+ Secure bool `xml:"secure"`
+ Name string `xml:"name"`
+}
+
+// GetCookieConfig returns settings needed to set a Crowd SSO cookie.
+func (c *Crowd) GetCookieConfig() (CookieConfig, error) {
+ cc := CookieConfig{}
+
+ client := http.Client{Jar: c.cookies}
+ req, err := http.NewRequest("GET", c.url+"rest/usermanagement/1/config/cookie", nil)
+ if err != nil {
+ return cc, err
+ }
+ req.SetBasicAuth(c.user, c.passwd)
+ req.Header.Set("Accept", "application/xml")
+ req.Header.Set("Content-Type", "application/xml")
+ resp, err := client.Do(req)
+ if err != nil {
+ return cc, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != 200 {
+ return cc, fmt.Errorf("request failed: %s\n", resp.Status)
+ }
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return cc, err
+ }
+
+ err = xml.Unmarshal(body, &cc)
+ if err != nil {
+ return cc, err
+ }
+
+ return cc, nil
+}
diff --git a/vendor/github.com/jda/go-crowd/cookie_test.go b/vendor/github.com/jda/go-crowd/cookie_test.go
new file mode 100644
index 0000000..150eed0
--- /dev/null
+++ b/vendor/github.com/jda/go-crowd/cookie_test.go
@@ -0,0 +1,19 @@
+package crowd
+
+import (
+ "testing"
+)
+
+func TestGetCookieConfig(t *testing.T) {
+ tv := PrepVars(t)
+ c, err := New(tv.AppUsername, tv.AppPassword, tv.AppURL)
+ if err != nil {
+ t.Error(err)
+ }
+
+ ck, err := c.GetCookieConfig()
+ if err != nil {
+ t.Error(err)
+ }
+ t.Logf("Got: %+v\n", ck)
+}
diff --git a/vendor/github.com/jda/go-crowd/crowdauth/loginTemplate.go b/vendor/github.com/jda/go-crowd/crowdauth/loginTemplate.go
new file mode 100644
index 0000000..6e2c1de
--- /dev/null
+++ b/vendor/github.com/jda/go-crowd/crowdauth/loginTemplate.go
@@ -0,0 +1,15 @@
+package crowdauth
+
+var defLoginPage string = `
+
Login required
+
+Login required
+
+
+`
diff --git a/vendor/github.com/jda/go-crowd/crowdauth/middleware.go b/vendor/github.com/jda/go-crowd/crowdauth/middleware.go
new file mode 100644
index 0000000..bfe7eb4
--- /dev/null
+++ b/vendor/github.com/jda/go-crowd/crowdauth/middleware.go
@@ -0,0 +1,205 @@
+// Package crowdauth provides middleware for Crowd SSO logins
+//
+// Goals:
+// 1) drop-in authentication against Crowd SSO
+// 2) make it easy to use Crowd SSO as part of your own auth flow
+package crowdauth // import "go.jona.me/crowd/crowdauth"
+
+import (
+ "errors"
+ "go.jona.me/crowd"
+ "html/template"
+ "log"
+ "net/http"
+ "time"
+)
+
+type SSO struct {
+ CrowdApp *crowd.Crowd
+ LoginPage AuthLoginPage
+ LoginTemplate *template.Template
+ ClientAddressFinder ClientAddressFinder
+ CookieConfig crowd.CookieConfig
+}
+
+// The AuthLoginPage type extends the normal http.HandlerFunc type
+// with a boolean return to indicate login success or failure.
+type AuthLoginPage func(http.ResponseWriter, *http.Request, *SSO) bool
+
+// ClientAddressFinder type represents a function that returns the
+// end-user's IP address, allowing you to handle cases where the address
+// is masked by proxy servers etc by checking headers or whatever to find
+// the user's address
+type ClientAddressFinder func(*http.Request) (string, error)
+
+var authErr string = "unauthorized, login required"
+
+func DefaultClientAddressFinder(r *http.Request) (addr string, err error) {
+ return r.RemoteAddr, nil
+}
+
+// New creates a new instance of SSO
+func New(user string, password string, crowdURL string) (s *SSO, err error) {
+ s = &SSO{}
+ s.LoginPage = loginPage
+ s.ClientAddressFinder = DefaultClientAddressFinder
+ s.LoginTemplate = template.Must(template.New("authPage").Parse(defLoginPage))
+
+ cwd, err := crowd.New(user, password, crowdURL)
+ if err != nil {
+ return s, err
+ }
+ s.CrowdApp = &cwd
+ s.CookieConfig, err = s.CrowdApp.GetCookieConfig()
+ if err != nil {
+ return s, err
+ }
+
+ return s, nil
+}
+
+// Handler provides HTTP middleware using http.Handler chaining
+// that requires user authentication via Atlassian Crowd SSO.
+func (s *SSO) Handler(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if s.loginHandler(w, r) == false {
+ return
+ }
+ h.ServeHTTP(w, r)
+ })
+
+}
+
+func (s *SSO) loginHandler(w http.ResponseWriter, r *http.Request) bool {
+ ck, err := r.Cookie(s.CookieConfig.Name)
+
+ if err == http.ErrNoCookie {
+ // no cookie so show login page if GET
+ // if POST check if login and handle
+ // if fail, show login page with message
+ if r.Method == "GET" {
+ s.LoginPage(w, r, s)
+ } else if r.Method == "POST" {
+ authOK := s.LoginPage(w, r, s)
+ if authOK == true {
+ // Redirect for fresh pass through auth etc on success
+ http.Redirect(w, r, r.RequestURI, http.StatusTemporaryRedirect)
+ return false
+ } else {
+ log.Printf("crowdauth: authentication failed\n")
+ }
+ } else {
+ http.Error(w, authErr, http.StatusUnauthorized)
+ }
+ return false
+ } else {
+ // validate cookie or show login page
+ host, err := s.ClientAddressFinder(r)
+ if err != nil {
+ log.Printf("crowdauth: could not get remote addr: %s\n", err)
+ return false
+ }
+
+ _, err = s.CrowdApp.ValidateSession(ck.Value, host)
+ if err != nil {
+ log.Printf("crowdauth: could not validate cookie, deleting because: %s\n", err)
+ s.EndSession(w, r)
+ s.LoginPage(w, r, s)
+ return false
+ }
+
+ // valid cookie so fallthrough
+ }
+ return true
+}
+
+func (s *SSO) Login(user string, pass string, addr string) (cs crowd.Session, err error) {
+ cs, err = s.CrowdApp.NewSession(user, pass, addr)
+ return cs, err
+}
+
+func (s *SSO) Logout(w http.ResponseWriter, r *http.Request, newURL string) {
+ s.EndSession(w, r)
+ http.Redirect(w, r, newURL, http.StatusTemporaryRedirect)
+}
+
+// StartSession sets a Crowd session cookie.
+func (s *SSO) StartSession(w http.ResponseWriter, cs crowd.Session) {
+ ck := http.Cookie{
+ Name: s.CookieConfig.Name,
+ Domain: s.CookieConfig.Domain,
+ Secure: s.CookieConfig.Secure,
+ Value: cs.Token,
+ Expires: cs.Expires,
+ }
+ http.SetCookie(w, &ck)
+}
+
+// EndSession invalidates the current Crowd session and cookie
+func (s *SSO) EndSession(w http.ResponseWriter, r *http.Request) {
+ currentCookie, _ := r.Cookie(s.CookieConfig.Name)
+ newCookie := &http.Cookie{
+ Name: s.CookieConfig.Name,
+ Domain: s.CookieConfig.Domain,
+ Secure: s.CookieConfig.Secure,
+ MaxAge: -1,
+ Expires: time.Unix(1, 0),
+ Value: "LOGGED-OUT",
+ }
+ s.CrowdApp.InvalidateSession(currentCookie.Value)
+
+ log.Printf("Got cookie to remove: %+v\n", currentCookie)
+ log.Printf("Removal cookie: %+v\n", newCookie)
+ http.SetCookie(w, newCookie)
+}
+
+// Get User information for the current session (by cookie)
+func (s *SSO) GetUser(r *http.Request) (u crowd.User, err error) {
+ currentCookie, err := r.Cookie(s.CookieConfig.Name)
+ if err == http.ErrNoCookie {
+ return u, errors.New("no session cookie")
+ }
+
+ userSession, err := s.CrowdApp.GetSession(currentCookie.Value)
+ if err != nil {
+ return u, errors.New("session not valid")
+ }
+
+ return userSession.User, nil
+}
+
+func loginPage(w http.ResponseWriter, r *http.Request, s *SSO) bool {
+ if r.Method == "GET" { // show login page and bail
+ showLoginPage(w, s)
+ return false
+ } else if r.Method == "POST" {
+ user := r.FormValue("username")
+ pass := r.FormValue("password")
+ host, err := s.ClientAddressFinder(r)
+ if err != nil {
+ log.Printf("crowdauth: could not get remote addr: %s\n", err)
+ showLoginPage(w, s)
+ return false
+ }
+
+ sess, err := s.Login(user, pass, host)
+ if err != nil {
+ log.Printf("crowdauth: login/new session failed: %s\n", err)
+ showLoginPage(w, s)
+ return false
+ }
+
+ s.StartSession(w, sess)
+ } else {
+ return false
+ }
+
+ return true
+}
+
+func showLoginPage(w http.ResponseWriter, s *SSO) {
+ err := s.LoginTemplate.ExecuteTemplate(w, "authPage", nil)
+ if err != nil {
+ log.Printf("crowdauth: could not exec template: %s\n", err)
+ }
+}
diff --git a/vendor/github.com/jda/go-crowd/error.go b/vendor/github.com/jda/go-crowd/error.go
new file mode 100644
index 0000000..e49f9c9
--- /dev/null
+++ b/vendor/github.com/jda/go-crowd/error.go
@@ -0,0 +1,9 @@
+package crowd
+
+// Error represents a error response from Crowd.
+// Error reasons are documented at https://developer.atlassian.com/display/CROWDDEV/Using+the+Crowd+REST+APIs#UsingtheCrowdRESTAPIs-HTTPResponseCodesandErrorResponses
+type Error struct {
+ XMLName struct{} `xml:"error"`
+ Reason string `xml:"reason"`
+ Message string `xml:"message"`
+}
diff --git a/vendor/github.com/jda/go-crowd/groups.go b/vendor/github.com/jda/go-crowd/groups.go
new file mode 100644
index 0000000..71f6240
--- /dev/null
+++ b/vendor/github.com/jda/go-crowd/groups.go
@@ -0,0 +1,87 @@
+package crowd
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+)
+
+// Link is a child of Group
+type Link struct {
+ Href string `json:"href"`
+ Rel string `json:"rel"`
+}
+
+// Group represents a group in Crowd
+type Group struct {
+ Name string `json:"name"`
+ Link Link `json:"link"`
+}
+
+// Groups come in lists
+type listGroups struct {
+ Groups []*Group `json:"groups"`
+ Expand string `json:"expand"`
+}
+
+// GetGroups retrieves a list of groups of which a user is a direct (and nested if donested is true) member.
+func (c *Crowd) GetGroups(user string, donested bool) ([]*Group, error) {
+ groupList := listGroups{}
+
+ v := url.Values{}
+ v.Set("username", user)
+ var endpoint string
+
+ if donested {
+ endpoint = "nested"
+ } else {
+ endpoint = "direct"
+ }
+
+ url := c.url + "rest/usermanagement/1/user/group/" + endpoint + "?" + v.Encode()
+ client := http.Client{Jar: c.cookies}
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return groupList.Groups, err
+ }
+ req.SetBasicAuth(c.user, c.passwd)
+ req.Header.Set("Accept", "application/json")
+ resp, err := client.Do(req)
+ if err != nil {
+ return groupList.Groups, err
+ }
+ defer resp.Body.Close()
+
+ switch resp.StatusCode {
+ case 404:
+ return groupList.Groups, fmt.Errorf("user not found")
+ case 200:
+ // fall through switch without returning
+ default:
+ return groupList.Groups, fmt.Errorf("request failed: %s", resp.Status)
+ }
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return groupList.Groups, err
+ }
+
+ err = json.Unmarshal(body, &groupList)
+ if err != nil {
+ return groupList.Groups, err
+ }
+
+ return groupList.Groups, nil
+}
+
+// GetNestedGroups retrieves a list of groups of which a user is a direct or nested member
+func (c *Crowd) GetNestedGroups(user string) ([]*Group, error) {
+ return c.GetGroups(user, true)
+}
+
+// GetDirectGroups retrieves a list of groups of which a user is a direct member
+func (c *Crowd) GetDirectGroups(user string) ([]*Group, error) {
+ return c.GetGroups(user, false)
+}
diff --git a/vendor/github.com/jda/go-crowd/groups_test.go b/vendor/github.com/jda/go-crowd/groups_test.go
new file mode 100644
index 0000000..be8018c
--- /dev/null
+++ b/vendor/github.com/jda/go-crowd/groups_test.go
@@ -0,0 +1,62 @@
+package crowd
+
+import (
+ "os"
+ "testing"
+)
+
+func TestGetDirectGroups(t *testing.T) {
+ tv := PrepVars(t)
+ c, err := New(tv.AppUsername, tv.AppPassword, tv.AppURL)
+ if err != nil {
+ t.Error(err)
+ }
+
+ user := os.Getenv("APP_USER_USERNAME")
+ if user == "" {
+ t.Skip("Can't run test because APP_USER_USERNAME undefined")
+ }
+
+ // test new session
+ groups, err := c.GetDirectGroups(user)
+ if err != nil {
+ t.Errorf("Error getting user's direct group membership list: %s\n", err)
+ } else {
+ t.Logf("Got user's direct group membership list:")
+ for _, element := range groups {
+ t.Logf(" %s", element.Name)
+ }
+ }
+
+ if len(groups) == 0 {
+ t.Error("groups list was empty so we didn't get/decode a response from GetIndirectGroups")
+ }
+}
+
+func TestGetNestedGroups(t *testing.T) {
+ tv := PrepVars(t)
+ c, err := New(tv.AppUsername, tv.AppPassword, tv.AppURL)
+ if err != nil {
+ t.Error(err)
+ }
+
+ user := os.Getenv("APP_USER_USERNAME")
+ if user == "" {
+ t.Skip("Can't run test because APP_USER_USERNAME undefined")
+ }
+
+ // test new session
+ groups, err := c.GetNestedGroups(user)
+ if err != nil {
+ t.Errorf("Error getting user's nested group membership list: %s\n", err)
+ } else {
+ t.Logf("Got user's nested group membership list:")
+ for _, element := range groups {
+ t.Logf(" %s", element.Name)
+ }
+ }
+
+ if len(groups) == 0 {
+ t.Error("groups list was empty so we didn't get/decode a response from GetIndirectGroups")
+ }
+}
diff --git a/vendor/github.com/jda/go-crowd/sso.go b/vendor/github.com/jda/go-crowd/sso.go
new file mode 100644
index 0000000..59cc3a4
--- /dev/null
+++ b/vendor/github.com/jda/go-crowd/sso.go
@@ -0,0 +1,221 @@
+package crowd
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "time"
+)
+
+// Session represents a single sign-on (SSO) session in Crowd
+type Session struct {
+ XMLName struct{} `xml:"session"`
+ Expand string `xml:"expand,attr"`
+ Token string `xml:"token"`
+ User User `xml:"user,omitempty"`
+ Created time.Time `xml:"created-date"`
+ Expires time.Time `xml:"expiry-date"`
+}
+
+// session authentication request
+type sessionAuthReq struct {
+ XMLName struct{} `xml:"authentication-context"`
+ Username string `xml:"username"`
+ Password string `xml:"password"`
+ ValidationFactors []sessionValidationFactor `xml:"validation-factors>validation-factor"`
+}
+
+// validation factors for session
+type sessionValidationFactor struct {
+ XMLName struct{} `xml:"validation-factor"`
+ Name string `xml:"name"`
+ Value string `xml:"value"`
+}
+
+// session validation request -> just validation factors
+type sessionValidationValidationFactor struct {
+ XMLName struct{} `xml:"validation-factors"`
+ ValidationFactors []sessionValidationFactor `xml:"validation-factor"`
+}
+
+// Authenticate a user and start a SSO session if valid.
+func (c *Crowd) NewSession(user string, pass string, address string) (Session, error) {
+ s := Session{}
+
+ svf := sessionValidationFactor{Name: "remote_address", Value: address}
+ sar := sessionAuthReq{Username: user, Password: pass}
+ sar.ValidationFactors = append(sar.ValidationFactors, svf)
+
+ sarEncoded, err := xml.Marshal(sar)
+ if err != nil {
+ return s, err
+ }
+ sarBuf := bytes.NewBuffer(sarEncoded)
+
+ url := c.url + "rest/usermanagement/1/session"
+
+ client := http.Client{Jar: c.cookies}
+ req, err := http.NewRequest("POST", url, sarBuf)
+ if err != nil {
+ return s, err
+ }
+ req.SetBasicAuth(c.user, c.passwd)
+ req.Header.Set("Accept", "application/xml")
+ req.Header.Set("Content-Type", "application/xml")
+ resp, err := client.Do(req)
+ if err != nil {
+ return s, err
+ }
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return s, err
+ }
+
+ switch resp.StatusCode {
+ case 400:
+ er := Error{}
+ err = xml.Unmarshal(body, &er)
+ if err != nil {
+ return s, err
+ }
+
+ return s, fmt.Errorf("%s", er.Reason)
+ case 201:
+ err = xml.Unmarshal(body, &s)
+ if err != nil {
+ return s, err
+ }
+ default:
+ return s, fmt.Errorf("request failed: %s\n", resp.Status)
+ }
+
+ return s, nil
+}
+
+// Validate a SSO token against Crowd. Returns error on failure
+// or account lockout. Success is a populated Session with nil error.
+func (c *Crowd) ValidateSession(token string, clientaddr string) (Session, error) {
+ s := Session{}
+
+ svf := sessionValidationFactor{Name: "remote_address", Value: clientaddr}
+ svvf := sessionValidationValidationFactor{}
+ svvf.ValidationFactors = append(svvf.ValidationFactors, svf)
+
+ svvfEncoded, err := xml.Marshal(svvf)
+ if err != nil {
+ return s, err
+ }
+ svvfBuf := bytes.NewBuffer(svvfEncoded)
+
+ url := c.url + "rest/usermanagement/1/session/" + token
+
+ client := http.Client{Jar: c.cookies}
+ req, err := http.NewRequest("POST", url, svvfBuf)
+ if err != nil {
+ return s, err
+ }
+ req.SetBasicAuth(c.user, c.passwd)
+ req.Header.Set("Accept", "application/xml")
+ req.Header.Set("Content-Type", "application/xml")
+ resp, err := client.Do(req)
+ if err != nil {
+ return s, err
+ }
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return s, err
+ }
+
+ switch resp.StatusCode {
+ case 400:
+ er := Error{}
+ err = xml.Unmarshal(body, &er)
+ if err != nil {
+ return s, err
+ }
+
+ return s, fmt.Errorf("%s", er.Reason)
+ case 404:
+ er := Error{}
+ err = xml.Unmarshal(body, &er)
+ if err != nil {
+ return s, err
+ }
+
+ return s, fmt.Errorf("%s", er.Reason)
+ case 200:
+ err = xml.Unmarshal(body, &s)
+ if err != nil {
+ return s, err
+ }
+ default:
+ return s, fmt.Errorf("request failed: %s\n", resp.Status)
+ }
+
+ return s, nil
+}
+
+// Invalidate SSO session token. Returns error on failure.
+func (c *Crowd) InvalidateSession(token string) error {
+ client := http.Client{Jar: c.cookies}
+ req, err := http.NewRequest("DELETE", c.url+"rest/usermanagement/1/session/"+token, nil)
+ if err != nil {
+ return err
+ }
+ req.SetBasicAuth(c.user, c.passwd)
+ req.Header.Set("Accept", "application/xml")
+ req.Header.Set("Content-Type", "application/xml")
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != 204 {
+ return fmt.Errorf("request failed: %s\n", resp.Status)
+ }
+
+ return nil
+}
+
+// Get SSO session information by token
+func (c *Crowd) GetSession(token string) (s Session, err error) {
+ client := http.Client{Jar: c.cookies}
+ url := c.url + "rest/usermanagement/1/session/" + token
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return s, err
+ }
+ req.SetBasicAuth(c.user, c.passwd)
+ req.Header.Set("Accept", "application/xml")
+ req.Header.Set("Content-Type", "application/xml")
+ resp, err := client.Do(req)
+ if err != nil {
+ return s, err
+ }
+ defer resp.Body.Close()
+
+ switch resp.StatusCode {
+ case 404:
+ return s, fmt.Errorf("session not found")
+ case 200:
+ // fall through switch without returning
+ default:
+ return s, fmt.Errorf("request failed: %s\n", resp.Status)
+ }
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return s, err
+ }
+
+ err = xml.Unmarshal(body, &s)
+ if err != nil {
+ return s, err
+ }
+
+ return s, nil
+}
diff --git a/vendor/github.com/jda/go-crowd/sso_test.go b/vendor/github.com/jda/go-crowd/sso_test.go
new file mode 100644
index 0000000..5838471
--- /dev/null
+++ b/vendor/github.com/jda/go-crowd/sso_test.go
@@ -0,0 +1,75 @@
+package crowd
+
+import (
+ "os"
+ "testing"
+)
+
+func TestSSOLifeCycle(t *testing.T) {
+ tv := PrepVars(t)
+ c, err := New(tv.AppUsername, tv.AppPassword, tv.AppURL)
+ if err != nil {
+ t.Error(err)
+ }
+
+ user := os.Getenv("APP_USER_USERNAME")
+ if user == "" {
+ t.Skip("Can't run test because APP_USER_USERNAME undefined")
+ }
+
+ passwd := os.Getenv("APP_USER_PASSWORD")
+ if passwd == "" {
+ t.Skip("Can't run test because APP_USER_PASSWORD undefined")
+ }
+
+ addr := "10.10.10.10"
+
+ // test new session
+ a, err := c.NewSession(user, passwd, addr)
+ if err != nil {
+ t.Errorf("Error creating new session: %s\n", err)
+ } else {
+ t.Logf("Got new session: %+v\n", a)
+ }
+
+ if a.Token == "" {
+ t.Errorf("Token was empty so we didn't get/decode a response from NewSession")
+ }
+
+ // test validate for session we just created
+ si, err := c.ValidateSession(a.Token, addr)
+ if err != nil {
+ t.Errorf("Error validating session: %s\n", err)
+ } else {
+ t.Logf("Validated session: %+v\n", si)
+ }
+
+ if si.Token == "" {
+ t.Errorf("Token was empty so we didn't get/decode a response from ValidateSession")
+ }
+
+ // test get info for session
+ sdat, err := c.GetSession(a.Token)
+ if err != nil {
+ t.Errorf("Error getting session: %s\n", err)
+ } else {
+ t.Logf("Got session: %+v\n", sdat)
+ }
+
+ // test invalidating session
+ err = c.InvalidateSession(a.Token)
+ if err != nil {
+ t.Errorf("Error invalidating session: %s\n", err)
+ } else {
+ t.Log("Invalidated session")
+ }
+
+ // make sure sesssion is gone
+ ivsess, err := c.ValidateSession(a.Token, addr)
+ if err == nil {
+ t.Errorf("Validating non-existant session should fail, got: %+v\n", ivsess)
+ } else {
+ t.Log("Could not validate session that doesn't exist (this is good)")
+ }
+
+}
diff --git a/vendor/github.com/jda/go-crowd/user.go b/vendor/github.com/jda/go-crowd/user.go
new file mode 100644
index 0000000..8ac7089
--- /dev/null
+++ b/vendor/github.com/jda/go-crowd/user.go
@@ -0,0 +1,64 @@
+package crowd
+
+import (
+ "encoding/xml"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+)
+
+// User represents a user in Crowd
+type User struct {
+ XMLName struct{} `xml:"user"`
+ UserName string `xml:"name,attr"`
+ FirstName string `xml:"first-name"`
+ LastName string `xml:"last-name"`
+ DisplayName string `xml:"display-name"`
+ Email string `xml:"email"`
+ Active bool `xml:"active"`
+ Key string `xml:"key"`
+}
+
+// GetUser retrieves user information
+func (c *Crowd) GetUser(user string) (User, error) {
+ u := User{}
+
+ v := url.Values{}
+ v.Set("username", user)
+ url := c.url + "rest/usermanagement/1/user?" + v.Encode()
+ client := http.Client{Jar: c.cookies}
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return u, err
+ }
+ req.SetBasicAuth(c.user, c.passwd)
+ req.Header.Set("Accept", "application/xml")
+ req.Header.Set("Content-Type", "application/xml")
+ resp, err := client.Do(req)
+ if err != nil {
+ return u, err
+ }
+ defer resp.Body.Close()
+
+ switch resp.StatusCode {
+ case 404:
+ return u, fmt.Errorf("user not found")
+ case 200:
+ // fall through switch without returning
+ default:
+ return u, fmt.Errorf("request failed: %s\n", resp.Status)
+ }
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return u, err
+ }
+
+ err = xml.Unmarshal(body, &u)
+ if err != nil {
+ return u, err
+ }
+
+ return u, nil
+}
diff --git a/vendor/github.com/jda/go-crowd/user_test.go b/vendor/github.com/jda/go-crowd/user_test.go
new file mode 100644
index 0000000..46275b3
--- /dev/null
+++ b/vendor/github.com/jda/go-crowd/user_test.go
@@ -0,0 +1,32 @@
+package crowd
+
+import (
+ "os"
+ "testing"
+)
+
+func TestGetUser(t *testing.T) {
+ tv := PrepVars(t)
+ c, err := New(tv.AppUsername, tv.AppPassword, tv.AppURL)
+ if err != nil {
+ t.Error(err)
+ }
+
+ user := os.Getenv("APP_USER_USERNAME")
+ if user == "" {
+ t.Skip("Can't run test because APP_USER_USERNAME undefined")
+ }
+
+ // test new session
+ u, err := c.GetUser(user)
+ if err != nil {
+ t.Errorf("Error getting user info: %s\n", err)
+ } else {
+ t.Logf("Got user info: %+v\n", u)
+ }
+
+ if u.UserName == "" {
+ t.Errorf("username was empty so we didn't get/decode a response from GetUser")
+ }
+
+}