mirror of
https://github.com/Luzifer/nginx-sso.git
synced 2024-12-20 12:51:17 +00:00
Implement Crowd authentication (#2)
* Re-add example configuration for Crowd * Implement Crowd authentication * Fix: Some errors just mean there is no user * Document crowd provider * Vendor new dependencies * Reduce error messages: Check for config details
This commit is contained in:
parent
8731310b3c
commit
6fa934880e
21 changed files with 1274 additions and 1 deletions
8
Gopkg.lock
generated
8
Gopkg.lock
generated
|
@ -43,6 +43,12 @@
|
||||||
revision = "ca9ada44574153444b00d3fd9c8559e4cc95f896"
|
revision = "ca9ada44574153444b00d3fd9c8559e4cc95f896"
|
||||||
version = "v1.1"
|
version = "v1.1"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/jda/go-crowd"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "415c27e65cd496563601465cfe630d4b3245d70f"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/juju/errors"
|
name = "github.com/juju/errors"
|
||||||
|
@ -95,6 +101,6 @@
|
||||||
[solve-meta]
|
[solve-meta]
|
||||||
analyzer-name = "dep"
|
analyzer-name = "dep"
|
||||||
analyzer-version = 1
|
analyzer-version = 1
|
||||||
inputs-digest = "8d56acebac43d560504fd8420c68a9021e29a60ee6b09d477ddc0d2e815a5606"
|
inputs-digest = "c60c92a35a0972af226bbe9e4e3638d032a0ad26646fa6ac68919f2b3b805e82"
|
||||||
solver-name = "gps-cdcl"
|
solver-name = "gps-cdcl"
|
||||||
solver-version = 1
|
solver-version = 1
|
||||||
|
|
14
README.md
14
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.
|
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`)
|
### 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.
|
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.
|
||||||
|
|
178
auth_crowd.go
Normal file
178
auth_crowd.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -29,6 +29,13 @@ acl:
|
||||||
allow: ["luzifer", "@admins"]
|
allow: ["luzifer", "@admins"]
|
||||||
|
|
||||||
providers:
|
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
|
# Authentication against embedded user database
|
||||||
# Supports: Users, Groups
|
# Supports: Users, Groups
|
||||||
simple:
|
simple:
|
||||||
|
|
21
vendor/github.com/jda/go-crowd/LICENSE
generated
vendored
Normal file
21
vendor/github.com/jda/go-crowd/LICENSE
generated
vendored
Normal file
|
@ -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.
|
23
vendor/github.com/jda/go-crowd/README.md
generated
vendored
Normal file
23
vendor/github.com/jda/go-crowd/README.md
generated
vendored
Normal file
|
@ -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
|
||||||
|
```
|
70
vendor/github.com/jda/go-crowd/auth.go
generated
vendored
Normal file
70
vendor/github.com/jda/go-crowd/auth.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
34
vendor/github.com/jda/go-crowd/auth_test.go
generated
vendored
Normal file
34
vendor/github.com/jda/go-crowd/auth_test.go
generated
vendored
Normal file
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
41
vendor/github.com/jda/go-crowd/base.go
generated
vendored
Normal file
41
vendor/github.com/jda/go-crowd/base.go
generated
vendored
Normal file
|
@ -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() {
|
||||||
|
|
||||||
|
}
|
40
vendor/github.com/jda/go-crowd/base_test.go
generated
vendored
Normal file
40
vendor/github.com/jda/go-crowd/base_test.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
50
vendor/github.com/jda/go-crowd/cookie.go
generated
vendored
Normal file
50
vendor/github.com/jda/go-crowd/cookie.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
19
vendor/github.com/jda/go-crowd/cookie_test.go
generated
vendored
Normal file
19
vendor/github.com/jda/go-crowd/cookie_test.go
generated
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
15
vendor/github.com/jda/go-crowd/crowdauth/loginTemplate.go
generated
vendored
Normal file
15
vendor/github.com/jda/go-crowd/crowdauth/loginTemplate.go
generated
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package crowdauth
|
||||||
|
|
||||||
|
var defLoginPage string = `<html>
|
||||||
|
<head><title>Login required</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Login required</h1>
|
||||||
|
<form method="POST" action="">
|
||||||
|
<label for="inputUser">Username</label>
|
||||||
|
<input id="inputUser" name="username" required autofocus><br>
|
||||||
|
<label for="inputPassword">Password</label>
|
||||||
|
<input type="password" name="password" id="inputPassword"required>
|
||||||
|
<button type="submit">Sign in</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>`
|
205
vendor/github.com/jda/go-crowd/crowdauth/middleware.go
generated
vendored
Normal file
205
vendor/github.com/jda/go-crowd/crowdauth/middleware.go
generated
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
9
vendor/github.com/jda/go-crowd/error.go
generated
vendored
Normal file
9
vendor/github.com/jda/go-crowd/error.go
generated
vendored
Normal file
|
@ -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"`
|
||||||
|
}
|
87
vendor/github.com/jda/go-crowd/groups.go
generated
vendored
Normal file
87
vendor/github.com/jda/go-crowd/groups.go
generated
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
62
vendor/github.com/jda/go-crowd/groups_test.go
generated
vendored
Normal file
62
vendor/github.com/jda/go-crowd/groups_test.go
generated
vendored
Normal file
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
221
vendor/github.com/jda/go-crowd/sso.go
generated
vendored
Normal file
221
vendor/github.com/jda/go-crowd/sso.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
75
vendor/github.com/jda/go-crowd/sso_test.go
generated
vendored
Normal file
75
vendor/github.com/jda/go-crowd/sso_test.go
generated
vendored
Normal file
|
@ -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)")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
64
vendor/github.com/jda/go-crowd/user.go
generated
vendored
Normal file
64
vendor/github.com/jda/go-crowd/user.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
32
vendor/github.com/jda/go-crowd/user_test.go
generated
vendored
Normal file
32
vendor/github.com/jda/go-crowd/user_test.go
generated
vendored
Normal file
|
@ -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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue