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"
|
||||
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
|
||||
|
|
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.
|
||||
|
||||
### 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.
|
||||
|
|
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"]
|
||||
|
||||
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:
|
||||
|
|
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