3
0

Added integration and unit tests

This commit is contained in:
Peter Grlica
2021-05-18 07:56:42 +02:00
parent bb1043181c
commit a4b61c044e
11 changed files with 392 additions and 12 deletions
+46
View File
@@ -0,0 +1,46 @@
package saml
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_TemplateProvider(t *testing.T) {
var (
tcc = []struct {
name string
url string
expect templateProvider
}{
{
name: "Default SAML provider",
url: "http://example.tld",
expect: templateProvider{
Label: "Default SAML provider",
Handle: "saml/init",
Icon: "key",
},
},
{
name: "",
url: "http://example.tld",
expect: templateProvider{
Label: "http://example.tld",
Handle: "saml/init",
Icon: "key",
},
},
}
)
for _, tc := range tcc {
t.Run(tc.name, func(t *testing.T) {
var (
req = require.New(t)
)
req.Equal(tc.expect, TemplateProvider(tc.url, tc.name))
})
}
}
+71
View File
@@ -0,0 +1,71 @@
package saml
import (
"testing"
"github.com/stretchr/testify/require"
)
const (
defaultSAMLEmailPayload = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
defaultSamlTestEmailPayload = "urn:oasis:names:tc:SAML:attribute:subject-id"
)
func Test_guessIdentifier(t *testing.T) {
var (
sp = SamlSPService{
IDPUserMeta: &IdpIdentityPayload{
Name: "name_field",
Handle: "handle_field",
Identifier: "identifier_field",
},
}
tcc = []struct {
name string
payload map[string][]string
expect string
}{
{
name: "default email",
payload: map[string][]string{"email": {"test@example.com"}},
expect: "test@example.com",
},
{
name: "samltest.id email payload",
payload: map[string][]string{defaultSamlTestEmailPayload: {"test@example.com"}},
expect: "test@example.com",
},
{
name: "default SAML emailAddress payload",
payload: map[string][]string{defaultSAMLEmailPayload: {"test@example.com"}},
expect: "test@example.com",
},
{
name: "emailAddress payload",
payload: map[string][]string{"emailAddress": {"test@example.com"}},
expect: "test@example.com",
},
{
name: "missing email payload",
payload: map[string][]string{"non-existing-identifier": {"test@example.com"}},
expect: "",
},
{
name: "default payload as set in settings",
payload: map[string][]string{"identifier_field": {"test@example.com"}},
expect: "test@example.com",
},
}
)
for _, tc := range tcc {
t.Run(tc.name, func(t *testing.T) {
var (
req = require.New(t)
)
req.Equal(tc.expect, sp.GuessIdentifier(tc.payload))
})
}
}
+7 -6
View File
@@ -3,6 +3,9 @@ package service
import (
"context"
"fmt"
"testing"
"time"
"github.com/cortezaproject/corteza-server/pkg/eventbus"
"github.com/cortezaproject/corteza-server/pkg/id"
"github.com/cortezaproject/corteza-server/store"
@@ -12,8 +15,6 @@ import (
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
"testing"
"time"
)
// Mock auth service with nil for current time, dummy provider validator and mock db
@@ -80,23 +81,23 @@ func TestAuth_External(t *testing.T) {
cases = []struct {
name string
profile goth.User
profile types.ExternalAuthUser
user *types.User
err error
}{
{
"matching by user email",
goth.User{UserID: freshProfileID(), Provider: "-", Email: validUser.Email},
types.ExternalAuthUser{goth.User{UserID: freshProfileID(), Provider: "-", Email: validUser.Email}},
validUser,
nil},
{
"unknown profile",
goth.User{UserID: freshProfileID(), Provider: "-", Email: "fresh-from-foo@test.cortezaproject.org"},
types.ExternalAuthUser{goth.User{UserID: freshProfileID(), Provider: "-", Email: "fresh-from-foo@test.cortezaproject.org"}},
&types.User{Email: "fresh-from-foo@test.cortezaproject.org"},
nil},
{
"profile match by provider ID",
goth.User{UserID: fooCredentials.Credentials, Provider: fooCredentials.Kind, Email: "valid+2nd+email@test.cortezaproject.org"},
types.ExternalAuthUser{goth.User{UserID: fooCredentials.Credentials, Provider: fooCredentials.Kind, Email: "valid+2nd+email@test.cortezaproject.org"}},
validUser,
nil},
}
@@ -0,0 +1,175 @@
package system
import (
"context"
"net/http"
"net/url"
"testing"
"time"
"github.com/cortezaproject/corteza-server/auth/handlers"
"github.com/cortezaproject/corteza-server/auth/saml"
"github.com/cortezaproject/corteza-server/system/service"
"github.com/cortezaproject/corteza-server/system/types"
s "github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp"
jwt "github.com/dgrijalva/jwt-go"
"github.com/steinfletcher/apitest"
)
func loadSAMLService(ctx context.Context) (srvc *saml.SamlSPService, err error) {
links := handlers.GetLinks()
certManager := saml.NewCertManager(&saml.CertStoreLoader{})
cert, err := certManager.Parse(
readStaticFile("static/spCert.cert"),
readStaticFile("static/spCert.key"))
if err != nil {
return
}
idpUrl, err := url.Parse("")
if err != nil {
return
}
// idp metadata needs to be loaded before
// the internal samlsp package
md, err := samlsp.ParseMetadata(readStaticFile("static/idp_metadata.xml"))
ru, err := url.Parse("http://localhost:8084")
rootURL := &url.URL{
Scheme: ru.Scheme,
User: ru.User,
Host: ru.Host,
}
if err != nil {
return
}
srvc, err = saml.NewSamlSPService(saml.SamlSPArgs{
AcsURL: links.SamlCallback,
MetaURL: links.SamlMetadata,
SloURL: links.SamlLogout,
IdpURL: *idpUrl,
Host: *rootURL,
Cert: cert,
IdpMeta: md,
})
srvc.Handler().ServiceProvider.AllowIDPInitiated = true
return
}
func TestAuthExternalSAMLSuccess(t *testing.T) {
var (
h = newHelper(t)
cookieSessionIDPtoSP = apitest.
NewCookie("saml_tCu5PV6EgxcvUAa9e57uJ2g-bTkqnNkyyHHaOu15yEfZjgWKt02AtXGe").
Value(string(readStaticFile("static/idp_to_sp.cookie")))
cookieTokenIDPtoSPAfterLogin = apitest.
NewCookie("token").
Value(string(readStaticFile("static/idp_to_sp_token.cookie")))
)
s.MaxClockSkew = time.Hour
s.MaxIssueDelay = time.Hour
jwt.TimeFunc = func() time.Time {
tm, _ := time.Parse("2006-01-2 15:04:05", "2021-05-17 09:17:10")
return tm
}
s.TimeNow = func() time.Time {
tm, _ := time.Parse("2006-01-2 15:04:05", "2021-05-17 09:17:10")
return tm
}
// first step, there is no session cookie, redirect to idp
// in this case, host from parsed metadata
t.Log("start login process")
h.apiInit().
Get(handlers.GetLinks().SamlInit).
Expect(t).
Assert(func(res *http.Response, req *http.Request) error {
loc, _ := res.Location()
h.assertBody("SSO Error: saml: session not present", res.Body)
h.a.NotEmpty(loc.Query().Get("RelayState"))
h.a.NotEmpty(loc.Query().Get("SAMLRequest"))
h.a.Equal(http.StatusFound, res.StatusCode)
return nil
}).
End()
cookies := []*apitest.Cookie{}
// coming back from idp, posting to sp-related endpoint
// mocking session cookie and saml response
// if everything is ok, redirect back to SAML init
t.Log("post from idp to sp")
h.apiInit().
Post(handlers.GetLinks().SamlCallback).
Header("Content-Type", "application/x-www-form-urlencoded").
FormData("RelayState", "tCu5PV6EgxcvUAa9e57uJ2g-bTkqnNkyyHHaOu15yEfZjgWKt02AtXGe").
Cookies(cookieSessionIDPtoSP).
FormData("SAMLResponse", string(readStaticFile("static/idp_to_sp.post"))).
Expect(t).
Assert(func(res *http.Response, req *http.Request) error {
loc, _ := res.Location()
h.a.NotNil(getSessionCookie("token", res.Cookies()...))
h.a.Equal(http.StatusFound, res.StatusCode)
h.a.Equal(handlers.GetLinks().SamlInit, loc.String())
cookies = append(cookies, cookieSessionIDPtoSP)
return nil
}).
End()
// idp sends a token session cookie also
cookies = append(cookies, cookieTokenIDPtoSPAfterLogin)
// once everything is set and the external authentication via
// internal Corteza services is done, redirect to default path (profile)
t.Log("redirect to profile after session is created")
h.apiInit().
Get(handlers.GetLinks().SamlInit).
Cookies(cookies...).
Expect(t).
Assert(func(res *http.Response, req *http.Request) error {
loc, _ := res.Location()
ss, _, err := service.DefaultStore.SearchAuthSessions(context.Background(), types.AuthSessionFilter{})
h.a.NoError(err)
h.a.Len(ss, 1)
h.a.NotNil(getSessionCookie("session", res.Cookies()...))
h.a.Equal(http.StatusSeeOther, res.StatusCode)
h.a.Equal(handlers.GetLinks().Profile, loc.String())
return nil
}).
End()
}
func getSessionCookie(name string, cc ...*http.Cookie) (found *http.Cookie) {
for _, c := range cc {
if c.Name == name {
found = c
return
}
}
return
}
+42 -6
View File
@@ -2,15 +2,19 @@ package system
import (
"context"
"embed"
"errors"
"io"
"os"
"testing"
"github.com/cortezaproject/corteza-server/app"
handlers "github.com/cortezaproject/corteza-server/auth/handlers"
"github.com/cortezaproject/corteza-server/auth/request"
"github.com/cortezaproject/corteza-server/auth/saml"
"github.com/cortezaproject/corteza-server/pkg/api/server"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/cli"
"github.com/cortezaproject/corteza-server/pkg/eventbus"
"github.com/cortezaproject/corteza-server/pkg/id"
label "github.com/cortezaproject/corteza-server/pkg/label"
ltype "github.com/cortezaproject/corteza-server/pkg/label/types"
@@ -32,21 +36,25 @@ import (
"go.uber.org/zap"
)
//go:embed static
var mockData embed.FS
type (
helper struct {
t *testing.T
a *require.Assertions
t *testing.T
a *require.Assertions
sp *saml.SamlSPService
cUser *types.User
roleID uint64
data embed.FS
}
)
var (
testApp *app.CortezaApp
r chi.Router
eventBus = eventbus.New()
hh *handlers.AuthHandlers
)
func init() {
@@ -64,10 +72,14 @@ func rs(a ...int) string {
}
func InitTestApp() {
var sm *request.SessionManager
if testApp == nil {
ctx := cli.Context()
testApp = helpers.NewIntegrationTestApp(ctx, func(app *app.CortezaApp) (err error) {
service.CurrentSettings.Auth.External.Enabled = true
rbac.SetGlobal(rbac.NewTestService(zap.NewNop(), app.Store))
service.DefaultObjectStore, err = plain.NewWithAfero(afero.NewMemMapFs(), "test")
if err != nil {
@@ -79,16 +91,28 @@ func InitTestApp() {
return err
}
eventbus.Set(eventBus)
sm = request.NewSessionManager(service.DefaultStore, app.Opt.Auth, service.DefaultLogger)
return nil
})
}
sp, _ := loadSAMLService(context.Background())
hh = &handlers.AuthHandlers{
SamlSPService: *sp,
Log: zap.NewNop(),
AuthService: service.DefaultAuth,
SessionManager: sm,
}
if r == nil {
r = chi.NewRouter()
r.Use(server.BaseMiddleware(false, logger.Default())...)
helpers.BindAuthMiddleware(r)
rest.MountRoutes(r)
hh.MountHttpRoutes(r)
}
}
@@ -105,6 +129,7 @@ func newHelper(t *testing.T) helper {
cUser: &types.User{
ID: id.Next(),
},
data: mockData,
}
h.cUser.SetRoles([]uint64{h.roleID})
@@ -174,6 +199,17 @@ func (h helper) setLabel(res label.LabeledResource, name, value string) {
}))
}
func (h helper) assertBody(e string, s io.ReadCloser) {
b, err := io.ReadAll(s)
h.a.NoError(err)
h.a.Equal(e, string(b))
}
func (h helper) clearTemplates() {
h.noError(store.TruncateTemplates(context.Background(), service.DefaultStore))
}
func readStaticFile(f string) []byte {
c, _ := mockData.ReadFile(f)
return c
}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODQiLCJleHAiOjE2MjEyNDMwODIsImlhdCI6MTYyMTI0Mjk5MiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDg0IiwibmJmIjoxNjIxMjQyOTkyLCJzdWIiOiJ0Q3U1UFY2RWd4Y3ZVQWE5ZTU3dUoyZy1iVGtxbk5reXlISGFPdTE1eUVmWmpnV0t0MDJBdFhHZSIsImlkIjoiaWQtN2JmZmIyNmQ2YmVlNGI1YmRmODZmOGRjNGRhMmNmYzExN2Q4OWQwZiIsInVyaSI6Ii9hdXRoL2V4dGVybmFsL3NhbWwvaW5pdCIsInNhbWwtYXV0aG4tcmVxdWVzdCI6dHJ1ZX0.CU_jrc5gx6JhafzbekO-7VXLJU6jzDd-R4QyrtQIZN3jyqIZtYB466KiTFZnyYeEWEjK7GW18eHuzZFHmDpcQ9weOtvu9u0Z7UUDm3YQoG-6XgUeQKTV2i1uPzq1ZlT8iiMBUsn0kdKL2F18U4jl4Fss0_Ysdc3OqoEJ73xcu0P721ZSsg-vEwyooe1WMSosunN_HEmWOU2aC61uQwNFSRk5_JotdUEytko1Jzn9JeOnLllf8izr7z7JWEnBqN8845IV_zqiScrAptpAZRocAeMAbFPFPmj5_lL2SzKC4GF4lkoOIZ5vWRBVdfzIxvD21rVZK5rRlSdYUmT0txFoQw
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODQiLCJleHAiOjE2MjEyNDY2MzAsImlhdCI6MTYyMTI0MzAzMCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDg0IiwibmJmIjoxNjIxMjQzMDMwLCJzdWIiOiJBQWR6WldOeVpYUXhFMmR5SFBqRStoTVhQQW5uSUNpbUt2c21XeC9YMW9MaEFOV1dGbU0wWElMS01xa1dBWjFrVldiR0J0ZjhFS0xsb2wvakM2ZlJMa25Cc3dZRDlWNUxHQUErcDFxZHBON2w2MGI3ZHYvMFcwa25VRm1SSm8vWFlTYmRkTVFweUtBMWlCNVFrejZERDVsS0Ixa2VTMzBiYWpuT29OTT0iLCJhdHRyIjp7IlNlc3Npb25JbmRleCI6WyJfYzA5YWFiNDRhNTUyOGVhNzM2NTdkNTNjYTRmODA4NDMiXSwiZGlzcGxheU5hbWUiOlsiUmljayBTYW5jaGV6Il0sImVkdVBlcnNvbkVudGl0bGVtZW50IjpbInVybjptYWNlOmRpcjplbnRpdGxlbWVudDpjb21tb24tbGliLXRlcm1zIl0sImdpdmVuTmFtZSI6WyJSaWNrIl0sIm1haWwiOlsicnNhbmNoZXpAc2FtbHRlc3QuaWQiXSwicm9sZSI6WyJtYW5hZ2VyQHNhbWx0ZXN0LmlkIl0sInNuIjpbIlNhbmNoZXoiXSwidGVsZXBob25lTnVtYmVyIjpbIisxLTU1NS01NTUtNTUxNSJdLCJ1aWQiOlsicmljayJdLCJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDphdHRyaWJ1dGU6c3ViamVjdC1pZCI6WyJyc2FuY2hlekBzYW1sdGVzdC5pZCJdfSwic2FtbC1zZXNzaW9uIjp0cnVlfQ.bNYxkq2dnISDr4x0Wg9pBXcOsSpKEvf5Bb_1vfZDLax6yH2Jjn0Dwjn7LMI6rRcz31YdMkhxrc9sISDD4Peotc1q_QNmJ92xCCzDq9jZM7ptRpKHZJIWkgwUDmmsLjCWj_YiuOwsPP6Zt70AJXNUtOcBg04Dw5ncpcR1cRqf6rTP7rAdNjQ_Euitln1gHK4Zz-fH0GgtS_pYHIzKTRarxiEktMvQGiaNce5e-1DmMOS3L9_6-lSlTa4sYG9BVG4icFf3QnUXqc3DCgGQr7waMuZNA35jqSJ1MSwhLVSHV5gb_l3HxoHKwZhue5d874apTk8cX00wlDO0Q9Z1z2R9Pg
+19
View File
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDITCCAgmgAwIBAgIUB34XNoIgt3k4tLbC40GDnLuvYAkwDQYJKoZIhvcNAQEL
BQAwIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1wbGUuY29tMB4XDTIxMDQyMjE3
MzIzOVoXDTIyMDQyMjE3MzIzOVowIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1w
bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp9jElUsjM3We
UHA9CjGQLoQJlhNOqkv2oKZBvTpr8bAQF40vP9qGiwrZqRe0se1xCTmlVM/HGrct
C0I6aL7+WTamNEvwPimoWsk1V7zYY7cFW+JEmL/E379R+9giQLBxIvvg7hSd7icl
ucwG5V6WPenQiyneEegTsEEgKqagIAgSV9zYjNQrQes73WHBj8VCZhTsVEnVO+/V
mCJaEwwovZWP0loLebR29/I4gr/LmfQfrIA+j116o5zp44vl1yW98xq5snkgb4YB
fm6pTHDl2rn4pM3avL+sK1usz7HQwmKrW6cpLtKVc+Sbjf3MyLUuHxRmkPoY2J56
uJ1OWChoYwIDAQABo1MwUTAdBgNVHQ4EFgQUTjg/FEKaUjpqzm3KzCg+WMUsm2ow
HwYDVR0jBBgwFoAUTjg/FEKaUjpqzm3KzCg+WMUsm2owDwYDVR0TAQH/BAUwAwEB
/zANBgkqhkiG9w0BAQsFAAOCAQEALXtIWymIgZDMKHlVzMWq/Id/jP2aI0hpvZgS
2Lv7UDdyF1bj1MDTOtDkERoTVh/Xbhao5ls+GVhxvudjnZ8irdtlQRJNWTgj58Zi
zagO3StQ2duwZPyvt9jHgVJ8Jm9w8JVgsNP4Notl5Gj7dMGhxnm4aA2oHScJoC5g
u2ukGAAxgUpjNA7j9C/7iVLzijFzhztq7XTmbnJmuFPR50rqHJcTTYMVDc8Pb73c
mjDZDz9U91v+7j5oDLV/8khvLsHxi85/GA1+j7z77wWzrIwwNjY4YfbhF0HJZXQz
Xf3EHGxtSC01hJyfBjyXh5H8+NbyAYF8+d8y+uFu+GHhLS+Yag==
-----END CERTIFICATE-----
+28
View File
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCn2MSVSyMzdZ5Q
cD0KMZAuhAmWE06qS/agpkG9OmvxsBAXjS8/2oaLCtmpF7Sx7XEJOaVUz8caty0L
Qjpovv5ZNqY0S/A+KahayTVXvNhjtwVb4kSYv8Tfv1H72CJAsHEi++DuFJ3uJyW5
zAblXpY96dCLKd4R6BOwQSAqpqAgCBJX3NiM1CtB6zvdYcGPxUJmFOxUSdU779WY
IloTDCi9lY/SWgt5tHb38jiCv8uZ9B+sgD6PXXqjnOnji+XXJb3zGrmyeSBvhgF+
bqlMcOXaufikzdq8v6wrW6zPsdDCYqtbpyku0pVz5JuN/czItS4fFGaQ+hjYnnq4
nU5YKGhjAgMBAAECggEAPlzd/ZJjS9VhswVgyI7NwVqxrR8TVVbQFbRwLHyuaqg9
8mI0sgbhgnvPj3INYyaTnxfaA/8HPTfd9pbu2MhN/Ju/eSLV6mLT+JdVyHmT9Mil
pxQU5KQr4+5T6bzOTTbBcnwfgJYMb9X/wF68GTDhpbNgFrTBm+mclxo7d11dlUiQ
bdA2KBhIM54kKcWEO90AP2FiC/cRSbv8afHVb0Fxy1SBAGVDSscUaEwgyHkRf6H7
Mmt4xhfBwLZZk5pVa97pZsas7L0sm4BtyYSs4elJesuo3DTpezZSzWHNreJcCL00
v7pm9ZWv9PYeUcQqSw6Ws2fhsydiN93o8s/fPpF0IQKBgQDQqvKPjGo3HLRnBmpc
nhUVQgjdh/QobJCGBjdxR/C3NkcatNLONddhefqovfb2jITdHmRKW+7XgRq/q457
mZ2JPvvryiWcVqf6D91tSoFYrVg5nU1zr6nTga1BpTH00/eNEXxjMAAguPj3m7Sw
3BP7L8yZZI1pOOjzVzuISEDlGwKBgQDN62ZXzRqorn5t+mDywTAJ2qAyZzoDAtUP
CjANOKBCECqE9cOW1qBDJcaEF7G+xyB22CMciOELjGXWw6ZwFWjc23tDCEkoHoia
5IWvwNs5ncdPCuOm3aay9w2dx9Idy+Eq5Vpe4XDlnZvEfzjwXX3gbQ5YS9kbNE2r
XtmgD/BmWQKBgBNKqrBA0BUWT0tzGWRErThQ6ZbpmdYe62Gos3mCqCuYFgzPCOpN
qgL2DwmIvotexG3ZAHardzJvWjS8PKkKs7jbnNjY0I9ap58D1nnjOIAlTpHNDDsU
04OdapI2Hp8+9ZUSN8jHyEs+Lq5ds9/iCOrhKW5JEJXY0BinSPa5j15fAoGBAM2I
dqCQollXwe34CaiD13UeeOOWUTsMKqlWW9v2d085X5dSzyTRmSksnVbfZ5SqoOa+
mV0z6pxiSIvywUACvqYjlIa10H9w6pzgF+fzMV3y9CsbDVtSxb7ABSFFf54qD9eH
EYq+rrchd4bMDYMtbiUB9V2AZ3VV4Wh5xfKTtjoRAoGAec/nBmX/oHiG/3T2LEmo
bTT+/eXc4LUzgHvFG6HW40PI8T2TfMTUGQH+90zkraryUF5PNoUF58tooDAcMWXm
DPEHFHeWn8T4obfuUjTw1mwj7Xjzr40HDitjIQa0bvBVwdYyEgiEw1CaeaUpq9Z8
ElGWoRHT87pfoZjbdPz7a4Y=
-----END PRIVATE KEY-----