1
0
Fork 0
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:
Knut Ahlers 2018-02-04 14:51:08 +01:00 committed by GitHub
parent 8731310b3c
commit 6fa934880e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1274 additions and 1 deletions

8
Gopkg.lock generated
View file

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

View file

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

View file

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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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")
}
}