mirror of
https://github.com/jsiebens/ionscale.git
synced 2026-03-31 15:07:49 +01:00
chore: restructure test setup and add some initial web login flow tests
This commit is contained in:
@@ -2,12 +2,15 @@ module github.com/jsiebens/ionscale
|
||||
|
||||
go 1.21
|
||||
|
||||
replace github.com/oauth2-proxy/mockoidc => github.com/jsiebens/mockoidc v0.0.0-20240108082145-5b2747b94b1b
|
||||
|
||||
require (
|
||||
github.com/apparentlymart/go-cidr v1.1.0
|
||||
github.com/bufbuild/connect-go v1.10.0
|
||||
github.com/caarlos0/env/v6 v6.10.1
|
||||
github.com/caddyserver/certmagic v0.20.0
|
||||
github.com/coreos/go-oidc/v3 v3.9.0
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2
|
||||
github.com/glebarez/sqlite v1.10.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.0.2
|
||||
github.com/go-jose/go-jose/v3 v3.0.1
|
||||
@@ -29,6 +32,7 @@ require (
|
||||
github.com/mitchellh/pointerstructure v1.2.1
|
||||
github.com/mr-tron/base58 v1.2.0
|
||||
github.com/nleeper/goment v1.4.4
|
||||
github.com/oauth2-proxy/mockoidc v0.0.0-20220308204021-b9169deeb282
|
||||
github.com/ory/dockertest/v3 v3.9.1
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
github.com/rodaine/table v1.1.0
|
||||
@@ -168,6 +172,7 @@ require (
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect
|
||||
google.golang.org/grpc v1.60.1 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gotest.tools/v3 v3.4.0 // indirect
|
||||
modernc.org/libc v1.38.0 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
|
||||
@@ -329,6 +329,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2 h1:S6Dco8FtAhEI/qkg/00H6RdEGC+MCy5GPiQ+xweNRFE=
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
@@ -558,6 +560,8 @@ github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMt
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/jsiebens/go-edit v0.1.0 h1:62SSGW8Qc2zoBcJx7gV86ImPHmQzlU/DQhwCOR4uilE=
|
||||
github.com/jsiebens/go-edit v0.1.0/go.mod h1:m/wuWMv8sNhSl+M2qA35gP/K5jX2J7Aa+g16VwyfxrI=
|
||||
github.com/jsiebens/mockoidc v0.0.0-20240108082145-5b2747b94b1b h1:Y/oA8TYORWGvOq5Ynl9tY+eA6IEV/5tjb3ugSU2Khfo=
|
||||
github.com/jsiebens/mockoidc v0.0.0-20240108082145-5b2747b94b1b/go.mod h1:5AdBTMDkanoW4IMv51fajiAUEEmKyNza7s8R2D/5pbw=
|
||||
github.com/jsimonetti/rtnetlink v1.3.5 h1:hVlNQNRlLDGZz31gBPicsG7Q53rnlsz1l1Ix/9XlpVA=
|
||||
github.com/jsimonetti/rtnetlink v1.3.5/go.mod h1:0LFedyiTkebnd43tE4YAkWGIq9jQphow4CcwxaT2Y00=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
@@ -661,6 +665,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nleeper/goment v1.4.4 h1:GlMTpxvhueljArSunzYjN9Ri4SOmpn0Vh2hg2z/IIl8=
|
||||
github.com/nleeper/goment v1.4.4/go.mod h1:zDl5bAyDhqxwQKAvkSXMRLOdCowrdZz53ofRJc4VhTo=
|
||||
github.com/oauth2-proxy/mockoidc v0.0.0-20220308204021-b9169deeb282 h1:TQMyrpijtkFyXpNI3rY5hsZQZw+paiH+BfAlsb81HBY=
|
||||
github.com/oauth2-proxy/mockoidc v0.0.0-20220308204021-b9169deeb282/go.mod h1:rW25Kyd08Wdn3UVn0YBsDTSvReu0jqpmJKzxITPSjks=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
|
||||
@@ -843,6 +849,7 @@ golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
@@ -1406,6 +1413,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
@@ -45,6 +45,21 @@ type AuthFormData struct {
|
||||
Csrf string
|
||||
}
|
||||
|
||||
type AuthInput struct {
|
||||
Key string `param:"key"`
|
||||
Flow AuthFlow `param:"flow"`
|
||||
AuthKey string `query:"ak" form:"ak"`
|
||||
Oidc bool `query:"oidc" form:"oidc"`
|
||||
}
|
||||
|
||||
type EndAuthForm struct {
|
||||
AccountID uint64 `form:"aid"`
|
||||
TailnetID uint64 `form:"tid"`
|
||||
AsSystemAdmin bool `form:"sad"`
|
||||
AuthKey string `form:"ak"`
|
||||
State string `form:"state"`
|
||||
}
|
||||
|
||||
type TailnetSelectionData struct {
|
||||
AccountID uint64
|
||||
Tailnets []domain.Tailnet
|
||||
@@ -54,34 +69,54 @@ type TailnetSelectionData struct {
|
||||
|
||||
type oauthState struct {
|
||||
Key string
|
||||
Flow string
|
||||
Flow AuthFlow
|
||||
}
|
||||
|
||||
type AuthFlow string
|
||||
|
||||
const (
|
||||
AuthFlowMachineRegistration = "r"
|
||||
AuthFlowClient = "c"
|
||||
AuthFlowSSHCheckFlow = "s"
|
||||
)
|
||||
|
||||
func (h *AuthenticationHandlers) StartAuth(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
flow := c.Param("flow")
|
||||
key := c.Param("key")
|
||||
|
||||
var input AuthInput
|
||||
if err := c.Bind(&input); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
// machine registration auth flow
|
||||
if flow == "r" || flow == "" {
|
||||
if req, err := h.repository.GetRegistrationRequestByKey(ctx, key); err != nil || req == nil {
|
||||
if input.Flow == AuthFlowMachineRegistration {
|
||||
req, err := h.repository.GetRegistrationRequestByKey(ctx, input.Key)
|
||||
if err != nil || req == nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if input.Oidc && h.authProvider != nil {
|
||||
goto startOidc
|
||||
}
|
||||
|
||||
if input.AuthKey != "" {
|
||||
return h.endMachineRegistrationFlow(c, EndAuthForm{AuthKey: input.AuthKey}, req)
|
||||
}
|
||||
|
||||
csrf := c.Get(middleware.DefaultCSRFConfig.ContextKey).(string)
|
||||
return c.Render(http.StatusOK, "auth.html", &AuthFormData{ProviderAvailable: h.authProvider != nil, Csrf: csrf})
|
||||
}
|
||||
|
||||
// cli auth flow
|
||||
if flow == "c" {
|
||||
if s, err := h.repository.GetAuthenticationRequest(ctx, key); err != nil || s == nil {
|
||||
if input.Flow == AuthFlowClient {
|
||||
if s, err := h.repository.GetAuthenticationRequest(ctx, input.Key); err != nil || s == nil {
|
||||
return logError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// ssh check auth flow
|
||||
if flow == "s" {
|
||||
if s, err := h.repository.GetSSHActionRequest(ctx, key); err != nil || s == nil {
|
||||
if input.Flow == AuthFlowSSHCheckFlow {
|
||||
if s, err := h.repository.GetSSHActionRequest(ctx, input.Key); err != nil || s == nil {
|
||||
return logError(err)
|
||||
}
|
||||
}
|
||||
@@ -90,7 +125,9 @@ func (h *AuthenticationHandlers) StartAuth(c echo.Context) error {
|
||||
return logError(fmt.Errorf("unable to start auth flow as no auth provider is configured"))
|
||||
}
|
||||
|
||||
state, err := h.createState(flow, key)
|
||||
startOidc:
|
||||
|
||||
state, err := h.createState(input.Flow, input.Key)
|
||||
if err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
@@ -103,21 +140,22 @@ func (h *AuthenticationHandlers) StartAuth(c echo.Context) error {
|
||||
func (h *AuthenticationHandlers) ProcessAuth(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
key := c.Param("key")
|
||||
authKey := c.FormValue("ak")
|
||||
interactive := c.FormValue("s")
|
||||
var input AuthInput
|
||||
if err := c.Bind(&input); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
req, err := h.repository.GetRegistrationRequestByKey(ctx, key)
|
||||
req, err := h.repository.GetRegistrationRequestByKey(ctx, input.Key)
|
||||
if err != nil || req == nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if authKey != "" {
|
||||
return h.endMachineRegistrationFlow(c, req, &oauthState{Key: key})
|
||||
if input.AuthKey != "" {
|
||||
return h.endMachineRegistrationFlow(c, EndAuthForm{AuthKey: input.AuthKey}, req)
|
||||
}
|
||||
|
||||
if interactive != "" {
|
||||
state, err := h.createState("r", key)
|
||||
if input.Oidc {
|
||||
state, err := h.createState(input.Flow, input.Key)
|
||||
if err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
@@ -127,7 +165,7 @@ func (h *AuthenticationHandlers) ProcessAuth(c echo.Context) error {
|
||||
return c.Redirect(http.StatusFound, redirectUrl)
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusFound, "/a/"+key)
|
||||
return c.Redirect(http.StatusFound, fmt.Sprintf("/a/%s/%s", input.Flow, input.Key))
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) Callback(c echo.Context) error {
|
||||
@@ -153,7 +191,7 @@ func (h *AuthenticationHandlers) Callback(c echo.Context) error {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
if state.Flow == "s" {
|
||||
if state.Flow == AuthFlowSSHCheckFlow {
|
||||
sshActionReq, err := h.repository.GetSSHActionRequest(ctx, state.Key)
|
||||
if err != nil || sshActionReq == nil {
|
||||
return c.Redirect(http.StatusFound, "/a/error?e=ua")
|
||||
@@ -186,7 +224,7 @@ func (h *AuthenticationHandlers) Callback(c echo.Context) error {
|
||||
|
||||
csrf := c.Get(middleware.DefaultCSRFConfig.ContextKey).(string)
|
||||
|
||||
if state.Flow == "r" {
|
||||
if state.Flow == AuthFlowMachineRegistration {
|
||||
if len(tailnets) == 0 {
|
||||
registrationRequest, err := h.repository.GetRegistrationRequestByKey(ctx, state.Key)
|
||||
if err == nil && registrationRequest != nil {
|
||||
@@ -195,6 +233,18 @@ func (h *AuthenticationHandlers) Callback(c echo.Context) error {
|
||||
}
|
||||
return c.Redirect(http.StatusFound, "/a/error?e=ua")
|
||||
}
|
||||
|
||||
if len(tailnets) == 1 {
|
||||
req, err := h.repository.GetRegistrationRequestByKey(ctx, state.Key)
|
||||
if err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
if req == nil {
|
||||
return logError(fmt.Errorf("invalid registration key"))
|
||||
}
|
||||
return h.endMachineRegistrationFlow(c, EndAuthForm{AccountID: account.ID, TailnetID: tailnets[0].ID}, req)
|
||||
}
|
||||
|
||||
return c.Render(http.StatusOK, "tailnets.html", &TailnetSelectionData{
|
||||
Csrf: csrf,
|
||||
Tailnets: tailnets,
|
||||
@@ -203,8 +253,8 @@ func (h *AuthenticationHandlers) Callback(c echo.Context) error {
|
||||
})
|
||||
}
|
||||
|
||||
if state.Flow == "c" {
|
||||
isSystemAdmin, err := h.isSystemAdmin(ctx, user)
|
||||
if state.Flow == AuthFlowClient {
|
||||
isSystemAdmin, err := h.isSystemAdmin(user)
|
||||
if err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
@@ -228,51 +278,38 @@ func (h *AuthenticationHandlers) Callback(c echo.Context) error {
|
||||
return echo.NewHTTPError(http.StatusNotFound)
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) isSystemAdmin(ctx context.Context, u *auth.User) (bool, error) {
|
||||
return h.systemIAMPolicy.EvaluatePolicy(&domain.Identity{UserID: u.ID, Email: u.Name, Attr: u.Attr})
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) listAvailableTailnets(ctx context.Context, u *auth.User) ([]domain.Tailnet, error) {
|
||||
var result = []domain.Tailnet{}
|
||||
tailnets, err := h.repository.ListTailnets(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, t := range tailnets {
|
||||
approved, err := t.IAMPolicy.EvaluatePolicy(&domain.Identity{UserID: u.ID, Email: u.Name, Attr: u.Attr})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if approved {
|
||||
result = append(result, t)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) EndOAuth(c echo.Context) error {
|
||||
func (h *AuthenticationHandlers) EndAuth(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
state, err := h.readState(c.QueryParam("state"))
|
||||
var form EndAuthForm
|
||||
if err := c.Bind(&form); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
state, err := h.readState(form.State)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid state parameter")
|
||||
}
|
||||
|
||||
if state.Flow == "r" {
|
||||
if state.Flow == AuthFlowMachineRegistration {
|
||||
req, err := h.repository.GetRegistrationRequestByKey(ctx, state.Key)
|
||||
if err != nil || req == nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
return h.endMachineRegistrationFlow(c, req, state)
|
||||
return h.endMachineRegistrationFlow(c, form, req)
|
||||
}
|
||||
|
||||
if state.Flow == AuthFlowClient {
|
||||
req, err := h.repository.GetAuthenticationRequest(ctx, state.Key)
|
||||
if err != nil || req == nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
return h.endCliAuthenticationFlow(c, req, state)
|
||||
return h.endCliAuthenticationFlow(c, form, req)
|
||||
}
|
||||
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid state parameter")
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) Success(c echo.Context) error {
|
||||
@@ -299,21 +336,9 @@ func (h *AuthenticationHandlers) Error(c echo.Context) error {
|
||||
return c.Render(http.StatusOK, "error.html", nil)
|
||||
}
|
||||
|
||||
type TailnetSelectionForm struct {
|
||||
AccountID uint64 `form:"aid"`
|
||||
TailnetID uint64 `form:"tid"`
|
||||
AsSystemAdmin bool `form:"sad"`
|
||||
AuthKey string `form:"ak"`
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) endCliAuthenticationFlow(c echo.Context, req *domain.AuthenticationRequest, state *oauthState) error {
|
||||
func (h *AuthenticationHandlers) endCliAuthenticationFlow(c echo.Context, form EndAuthForm, req *domain.AuthenticationRequest) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
var form TailnetSelectionForm
|
||||
if err := c.Bind(&form); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
account, err := h.repository.GetAccount(ctx, form.AccountID)
|
||||
if err != nil {
|
||||
return logError(err)
|
||||
@@ -371,14 +396,9 @@ func (h *AuthenticationHandlers) endCliAuthenticationFlow(c echo.Context, req *d
|
||||
return c.Redirect(http.StatusFound, "/a/success")
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, registrationRequest *domain.RegistrationRequest, state *oauthState) error {
|
||||
func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, form EndAuthForm, registrationRequest *domain.RegistrationRequest) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
var form TailnetSelectionForm
|
||||
if err := c.Bind(&form); err != nil {
|
||||
return logError(err)
|
||||
}
|
||||
|
||||
req := tailcfg.RegisterRequest(registrationRequest.Data)
|
||||
machineKey := registrationRequest.MachineKey
|
||||
nodeKey := req.NodeKey.String()
|
||||
@@ -542,6 +562,28 @@ func (h *AuthenticationHandlers) endMachineRegistrationFlow(c echo.Context, regi
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) isSystemAdmin(u *auth.User) (bool, error) {
|
||||
return h.systemIAMPolicy.EvaluatePolicy(&domain.Identity{UserID: u.ID, Email: u.Name, Attr: u.Attr})
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) listAvailableTailnets(ctx context.Context, u *auth.User) ([]domain.Tailnet, error) {
|
||||
var result = []domain.Tailnet{}
|
||||
tailnets, err := h.repository.ListTailnets(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, t := range tailnets {
|
||||
approved, err := t.IAMPolicy.EvaluatePolicy(&domain.Identity{UserID: u.ID, Email: u.Name, Attr: u.Attr})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if approved {
|
||||
result = append(result, t)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) exchangeUser(code string) (*auth.User, error) {
|
||||
redirectUrl := h.config.CreateUrl("/a/callback")
|
||||
|
||||
@@ -553,7 +595,7 @@ func (h *AuthenticationHandlers) exchangeUser(code string) (*auth.User, error) {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (h *AuthenticationHandlers) createState(flow string, key string) (string, error) {
|
||||
func (h *AuthenticationHandlers) createState(flow AuthFlow, key string) (string, error) {
|
||||
stateMap := oauthState{Key: key, Flow: flow}
|
||||
marshal, err := json.Marshal(&stateMap)
|
||||
if err != nil {
|
||||
|
||||
@@ -174,7 +174,7 @@ func Start(c *config.Config) error {
|
||||
auth.GET("/:flow/:key", authenticationHandlers.StartAuth)
|
||||
auth.POST("/:flow/:key", authenticationHandlers.ProcessAuth)
|
||||
auth.GET("/callback", authenticationHandlers.Callback)
|
||||
auth.POST("/callback", authenticationHandlers.EndOAuth)
|
||||
auth.POST("/callback", authenticationHandlers.EndAuth)
|
||||
auth.GET("/success", authenticationHandlers.Success)
|
||||
auth.GET("/error", authenticationHandlers.Error)
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
<form method="post">
|
||||
<input type="hidden" name="_csrf" value="{{.Csrf}}">
|
||||
<ul class="selectionList">
|
||||
<li><button type="submit" name="s" value="true">OpenID</button></li>
|
||||
<li><button type="submit" name="oidc" value="true">OpenID</button></li>
|
||||
</ul>
|
||||
</form>
|
||||
<div style="text-align: left; padding-bottom: 10px; padding-top: 20px">
|
||||
|
||||
+13
-12
@@ -4,24 +4,25 @@ import (
|
||||
"github.com/jsiebens/ionscale/pkg/defaults"
|
||||
ionscalev1 "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/jsiebens/ionscale/tests/sc"
|
||||
"github.com/jsiebens/ionscale/tests/tsn"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestACL_PeersShouldBeRemovedWhenNoMatchingACLRuleIsAvailable(t *testing.T) {
|
||||
sc.Run(t, func(s sc.Scenario) {
|
||||
tailnet := s.CreateTailnet("acltest")
|
||||
sc.Run(t, func(s *sc.Scenario) {
|
||||
tailnet := s.CreateTailnet()
|
||||
clientKey := s.CreateAuthKey(tailnet.Id, true, "tag:client")
|
||||
serverKey := s.CreateAuthKey(tailnet.Id, true, "tag:server")
|
||||
|
||||
client1 := s.NewTailscaleNode("client1")
|
||||
client2 := s.NewTailscaleNode("client2")
|
||||
server := s.NewTailscaleNode("server")
|
||||
client1 := s.NewTailscaleNode()
|
||||
client2 := s.NewTailscaleNode()
|
||||
server := s.NewTailscaleNode()
|
||||
|
||||
client1.Up(clientKey)
|
||||
client2.Up(clientKey)
|
||||
server.Up(serverKey)
|
||||
|
||||
server.WaitFor(sc.PeerCount(2))
|
||||
require.NoError(t, client1.Up(clientKey))
|
||||
require.NoError(t, client2.Up(clientKey))
|
||||
require.NoError(t, server.Up(serverKey))
|
||||
require.NoError(t, server.WaitFor(tsn.PeerCount(2)))
|
||||
|
||||
policy := defaults.DefaultACLPolicy()
|
||||
policy.Acls = []*ionscalev1.ACL{
|
||||
@@ -32,8 +33,8 @@ func TestACL_PeersShouldBeRemovedWhenNoMatchingACLRuleIsAvailable(t *testing.T)
|
||||
},
|
||||
}
|
||||
|
||||
s.SetAclPolicy(tailnet.Id, policy)
|
||||
s.SetACLPolicy(tailnet.Id, policy)
|
||||
|
||||
server.WaitFor(sc.PeerCount(0))
|
||||
require.NoError(t, server.WaitFor(tsn.PeerCount(0)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
http_listen_addr: ":8080"
|
||||
server_url: "http://localhost:8080"
|
||||
http_listen_addr: ":80"
|
||||
server_url: "http://ionscale"
|
||||
|
||||
tls:
|
||||
disable: true
|
||||
@@ -12,5 +12,11 @@ database:
|
||||
type: sqlite
|
||||
url: /opt/ionscale.db?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)
|
||||
|
||||
auth:
|
||||
provider:
|
||||
issuer: http://mockoidc/oidc
|
||||
client_id: "foo"
|
||||
client_secret: "bar"
|
||||
|
||||
logging:
|
||||
level: debug
|
||||
+30
-28
@@ -2,36 +2,20 @@ package tests
|
||||
|
||||
import (
|
||||
"github.com/jsiebens/ionscale/tests/sc"
|
||||
"github.com/jsiebens/ionscale/tests/tsn"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
sc.Run(t, func(s sc.Scenario) {
|
||||
tailnet := s.CreateTailnet("pingtest")
|
||||
key := s.CreateAuthKey(tailnet.Id, true)
|
||||
|
||||
nodeA := s.NewTailscaleNode("pingtest-a")
|
||||
nodeB := s.NewTailscaleNode("pingtest-b")
|
||||
|
||||
nodeA.Up(key)
|
||||
nodeB.Up(key)
|
||||
|
||||
nodeA.WaitFor(sc.PeerCount(1))
|
||||
nodeA.Ping("pingtest-b")
|
||||
nodeA.Ping(nodeB.IPv4())
|
||||
nodeA.Ping(nodeB.IPv6())
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetIPs(t *testing.T) {
|
||||
sc.Run(t, func(s sc.Scenario) {
|
||||
tailnet := s.CreateTailnet("tailnet01")
|
||||
sc.Run(t, func(s *sc.Scenario) {
|
||||
tailnet := s.CreateTailnet()
|
||||
authKey := s.CreateAuthKey(tailnet.Id, false)
|
||||
|
||||
tsNode := s.NewTailscaleNode("testip")
|
||||
tsNode := s.NewTailscaleNode()
|
||||
|
||||
tsNode.Up(authKey)
|
||||
require.NoError(t, tsNode.Up(authKey))
|
||||
|
||||
ip4 := tsNode.IPv4()
|
||||
ip6 := tsNode.IPv6()
|
||||
@@ -49,18 +33,36 @@ func TestGetIPs(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
sc.Run(t, func(s *sc.Scenario) {
|
||||
tailnet := s.CreateTailnet()
|
||||
key := s.CreateAuthKey(tailnet.Id, true)
|
||||
|
||||
nodeA := s.NewTailscaleNode()
|
||||
nodeB := s.NewTailscaleNode()
|
||||
|
||||
require.NoError(t, nodeA.Up(key))
|
||||
require.NoError(t, nodeB.Up(key))
|
||||
|
||||
require.NoError(t, nodeA.WaitFor(tsn.PeerCount(1)))
|
||||
require.NoError(t, nodeA.Ping(nodeB.Hostname()))
|
||||
require.NoError(t, nodeA.Ping(nodeB.IPv4()))
|
||||
require.NoError(t, nodeA.Ping(nodeB.IPv6()))
|
||||
})
|
||||
}
|
||||
|
||||
func TestNodeWithSameHostname(t *testing.T) {
|
||||
sc.Run(t, func(s sc.Scenario) {
|
||||
tailnet := s.CreateTailnet("tailnet01")
|
||||
sc.Run(t, func(s *sc.Scenario) {
|
||||
tailnet := s.CreateTailnet()
|
||||
authKey := s.CreateAuthKey(tailnet.Id, false)
|
||||
|
||||
tsNode := s.NewTailscaleNode("test")
|
||||
tsNode := s.NewTailscaleNode(sc.WithName("test"))
|
||||
|
||||
tsNode.Up(authKey)
|
||||
require.NoError(t, tsNode.Up(authKey))
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
tc := s.NewTailscaleNode("test")
|
||||
tc.Up(authKey)
|
||||
tc := s.NewTailscaleNode(sc.WithName("test"))
|
||||
require.NoError(t, tc.Up(authKey))
|
||||
}
|
||||
|
||||
machines := make(map[string]bool)
|
||||
|
||||
@@ -4,17 +4,19 @@ import (
|
||||
"github.com/jsiebens/ionscale/pkg/defaults"
|
||||
ionscalev1 "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/jsiebens/ionscale/tests/sc"
|
||||
"github.com/jsiebens/ionscale/tests/tsn"
|
||||
"github.com/stretchr/testify/require"
|
||||
"tailscale.com/tailcfg"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNodeAttrs(t *testing.T) {
|
||||
sc.Run(t, func(s sc.Scenario) {
|
||||
tailnet := s.CreateTailnet("nodeattrs")
|
||||
sc.Run(t, func(s *sc.Scenario) {
|
||||
tailnet := s.CreateTailnet()
|
||||
key := s.CreateAuthKey(tailnet.Id, true)
|
||||
|
||||
nodeA := s.NewTailscaleNode("test-a")
|
||||
nodeA.Up(key)
|
||||
nodeA := s.NewTailscaleNode()
|
||||
require.NoError(t, nodeA.Up(key))
|
||||
|
||||
policy := defaults.DefaultACLPolicy()
|
||||
policy.Nodeattrs = []*ionscalev1.NodeAttr{
|
||||
@@ -24,19 +26,19 @@ func TestNodeAttrs(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
s.SetAclPolicy(tailnet.Id, policy)
|
||||
s.SetACLPolicy(tailnet.Id, policy)
|
||||
|
||||
nodeA.WaitFor(sc.HasCapability("ionscale:test"))
|
||||
require.NoError(t, nodeA.WaitFor(tsn.HasCapability("ionscale:test")))
|
||||
})
|
||||
}
|
||||
|
||||
func TestNodeAttrs_IgnoreFunnelAttr(t *testing.T) {
|
||||
sc.Run(t, func(s sc.Scenario) {
|
||||
tailnet := s.CreateTailnet("nodeattrs")
|
||||
sc.Run(t, func(s *sc.Scenario) {
|
||||
tailnet := s.CreateTailnet()
|
||||
key := s.CreateAuthKey(tailnet.Id, true)
|
||||
|
||||
nodeA := s.NewTailscaleNode("test-a")
|
||||
nodeA.Up(key)
|
||||
nodeA := s.NewTailscaleNode()
|
||||
require.NoError(t, nodeA.Up(key))
|
||||
|
||||
policy := defaults.DefaultACLPolicy()
|
||||
policy.Nodeattrs = []*ionscalev1.NodeAttr{
|
||||
@@ -46,9 +48,9 @@ func TestNodeAttrs_IgnoreFunnelAttr(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
s.SetAclPolicy(tailnet.Id, policy)
|
||||
s.SetACLPolicy(tailnet.Id, policy)
|
||||
|
||||
nodeA.WaitFor(sc.HasCapability("ionscale:test"))
|
||||
nodeA.WaitFor(sc.IsMissingCapability(tailcfg.NodeAttrFunnel))
|
||||
require.NoError(t, nodeA.WaitFor(tsn.HasCapability("ionscale:test")))
|
||||
require.NoError(t, nodeA.WaitFor(tsn.IsMissingCapability(tailcfg.NodeAttrFunnel)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
package sc
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func PeerCount(expected int) func(*ipnstate.Status) bool {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
return len(status.Peers()) == expected
|
||||
}
|
||||
}
|
||||
|
||||
func HasCapability(capability tailcfg.NodeCapability) func(*ipnstate.Status) bool {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
self := status.Self
|
||||
|
||||
if self == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if slices.Contains(self.Capabilities, capability) {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, ok := self.CapMap[capability]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func IsMissingCapability(capability tailcfg.NodeCapability) func(*ipnstate.Status) bool {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
self := status.Self
|
||||
|
||||
if slices.Contains(self.Capabilities, capability) {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, ok := self.CapMap[capability]; ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package sc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/stretchr/testify/require"
|
||||
"strings"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type TailscaleNode interface {
|
||||
Hostname() string
|
||||
Up(authkey string)
|
||||
IPv4() string
|
||||
IPv6() string
|
||||
Ping(target string)
|
||||
WaitFor(func(*ipnstate.Status) bool)
|
||||
}
|
||||
|
||||
type tailscaleNode struct {
|
||||
t *testing.T
|
||||
loginServer string
|
||||
hostname string
|
||||
resource *dockertest.Resource
|
||||
}
|
||||
|
||||
func (t *tailscaleNode) Hostname() string {
|
||||
return t.hostname
|
||||
}
|
||||
|
||||
func (t *tailscaleNode) Up(authkey string) {
|
||||
t.mustExecTailscaleCmd("up", "--login-server", t.loginServer, "--authkey", authkey)
|
||||
t.waitForReady()
|
||||
}
|
||||
|
||||
func (t *tailscaleNode) IPv4() string {
|
||||
return t.mustExecTailscaleCmd("ip", "-4")
|
||||
}
|
||||
|
||||
func (t *tailscaleNode) IPv6() string {
|
||||
return t.mustExecTailscaleCmd("ip", "-6")
|
||||
}
|
||||
|
||||
func (t *tailscaleNode) waitForReady() ipnstate.Status {
|
||||
var status ipnstate.Status
|
||||
err := pool.Retry(func() error {
|
||||
out, err := t.execTailscaleCmd("status", "--json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(out), &status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if status.CurrentTailnet != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("not connected")
|
||||
})
|
||||
require.NoError(t.t, err)
|
||||
return status
|
||||
}
|
||||
|
||||
func (t *tailscaleNode) WaitForPeers(expected int) {
|
||||
err := pool.Retry(func() error {
|
||||
out, err := t.execTailscaleCmd("status", "--json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var status ipnstate.Status
|
||||
if err := json.Unmarshal([]byte(out), &status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(status.Peers()) != expected {
|
||||
return fmt.Errorf("incorrect peer count")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t.t, err)
|
||||
}
|
||||
|
||||
func (t *tailscaleNode) WaitFor(check func(status *ipnstate.Status) bool) {
|
||||
err := pool.Retry(func() error {
|
||||
out, err := t.execTailscaleCmd("status", "--json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var status ipnstate.Status
|
||||
if err := json.Unmarshal([]byte(out), &status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !check(&status) {
|
||||
return fmt.Errorf("condition not met")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t.t, err)
|
||||
}
|
||||
|
||||
func (t *tailscaleNode) Ping(target string) {
|
||||
result, err := t.execTailscaleCmd("ping", "--timeout=1s", "--c=10", "--until-direct=true", target)
|
||||
require.NoError(t.t, err)
|
||||
require.True(t.t, strings.Contains(result, "pong") || strings.Contains(result, "is local"), "ping failed")
|
||||
}
|
||||
|
||||
func (t *tailscaleNode) execTailscaleCmd(cmd ...string) (string, error) {
|
||||
i := append([]string{"/app/tailscale", "--socket=/tmp/tailscaled.sock"}, cmd...)
|
||||
return execCmd(t.resource, i...)
|
||||
}
|
||||
|
||||
func (t *tailscaleNode) mustExecTailscaleCmd(cmd ...string) string {
|
||||
i := append([]string{"/app/tailscale", "--socket=/tmp/tailscaled.sock"}, cmd...)
|
||||
s, err := execCmd(t.resource, i...)
|
||||
require.NoError(t.t, err)
|
||||
return s
|
||||
}
|
||||
|
||||
func execCmd(resource *dockertest.Resource, cmd ...string) (string, error) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
exitCode, err := resource.Exec(cmd, dockertest.ExecOptions{StdOut: &stdout, StdErr: &stderr})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return strings.TrimSpace(stdout.String()), err
|
||||
}
|
||||
|
||||
if exitCode != 0 {
|
||||
return strings.TrimSpace(stdout.String()), fmt.Errorf("command failed with: %s", stderr.String())
|
||||
}
|
||||
|
||||
return strings.TrimSpace(stdout.String()), nil
|
||||
}
|
||||
+92
-34
@@ -4,9 +4,14 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/bufbuild/connect-go"
|
||||
petname "github.com/dustinkirkland/golang-petname"
|
||||
ionscaleclt "github.com/jsiebens/ionscale/pkg/client/ionscale"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
ionscaleconnect "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1/ionscalev1connect"
|
||||
"github.com/jsiebens/ionscale/tests/tsn"
|
||||
"github.com/oauth2-proxy/mockoidc"
|
||||
mockoidcv1 "github.com/oauth2-proxy/mockoidc/pkg/gen/mockoidc/v1"
|
||||
"github.com/oauth2-proxy/mockoidc/pkg/gen/mockoidc/v1/mockoidcv1connect"
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -30,54 +35,88 @@ var (
|
||||
pool *dockertest.Pool
|
||||
)
|
||||
|
||||
type Scenario interface {
|
||||
NewTailscaleNode(hostname string) TailscaleNode
|
||||
|
||||
ListMachines(tailnetID uint64) []*api.Machine
|
||||
CreateAuthKey(tailnetID uint64, ephemeral bool, tags ...string) string
|
||||
CreateTailnet(name string) *api.Tailnet
|
||||
SetAclPolicy(tailnetID uint64, policy *api.ACLPolicy)
|
||||
}
|
||||
|
||||
type scenario struct {
|
||||
type Scenario struct {
|
||||
t *testing.T
|
||||
pool *dockertest.Pool
|
||||
network *dockertest.Network
|
||||
mockoidc *dockertest.Resource
|
||||
ionscale *dockertest.Resource
|
||||
resources []*dockertest.Resource
|
||||
client ionscaleconnect.IonscaleServiceClient
|
||||
ionscaleClient ionscaleconnect.IonscaleServiceClient
|
||||
mockoidcClient mockoidcv1connect.MockOIDCServiceClient
|
||||
}
|
||||
|
||||
func (s *scenario) CreateTailnet(name string) *api.Tailnet {
|
||||
createTailnetResponse, err := s.client.CreateTailnet(context.Background(), connect.NewRequest(&api.CreateTailnetRequest{Name: name}))
|
||||
func (s *Scenario) CreateTailnet() *api.Tailnet {
|
||||
name := petname.Generate(3, "-")
|
||||
createTailnetResponse, err := s.ionscaleClient.CreateTailnet(context.Background(), connect.NewRequest(&api.CreateTailnetRequest{Name: name}))
|
||||
require.NoError(s.t, err)
|
||||
return createTailnetResponse.Msg.GetTailnet()
|
||||
}
|
||||
|
||||
func (s *scenario) CreateAuthKey(tailnetID uint64, ephemeral bool, tags ...string) string {
|
||||
func (s *Scenario) CreateAuthKey(tailnetID uint64, ephemeral bool, tags ...string) string {
|
||||
if len(tags) == 0 {
|
||||
tags = []string{"tag:test"}
|
||||
}
|
||||
key, err := s.client.CreateAuthKey(context.Background(), connect.NewRequest(&api.CreateAuthKeyRequest{TailnetId: tailnetID, Ephemeral: ephemeral, Tags: tags, Expiry: durationpb.New(60 * time.Minute)}))
|
||||
key, err := s.ionscaleClient.CreateAuthKey(context.Background(), connect.NewRequest(&api.CreateAuthKeyRequest{TailnetId: tailnetID, Ephemeral: ephemeral, Tags: tags, Expiry: durationpb.New(60 * time.Minute)}))
|
||||
require.NoError(s.t, err)
|
||||
return key.Msg.Value
|
||||
}
|
||||
|
||||
func (s *scenario) ListMachines(tailnetID uint64) []*api.Machine {
|
||||
machines, err := s.client.ListMachines(context.Background(), connect.NewRequest(&api.ListMachinesRequest{TailnetId: tailnetID}))
|
||||
func (s *Scenario) ListMachines(tailnetID uint64) []*api.Machine {
|
||||
machines, err := s.ionscaleClient.ListMachines(context.Background(), connect.NewRequest(&api.ListMachinesRequest{TailnetId: tailnetID}))
|
||||
require.NoError(s.t, err)
|
||||
return machines.Msg.Machines
|
||||
}
|
||||
|
||||
func (s *scenario) SetAclPolicy(tailnetID uint64, policy *api.ACLPolicy) {
|
||||
_, err := s.client.SetACLPolicy(context.Background(), connect.NewRequest(&api.SetACLPolicyRequest{TailnetId: tailnetID, Policy: policy}))
|
||||
func (s *Scenario) AuthorizeMachines(tailnetID uint64) {
|
||||
machines := s.ListMachines(tailnetID)
|
||||
for _, m := range machines {
|
||||
_, err := s.ionscaleClient.AuthorizeMachine(context.Background(), connect.NewRequest(&api.AuthorizeMachineRequest{MachineId: m.Id}))
|
||||
require.NoError(s.t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scenario) SetACLPolicy(tailnetID uint64, policy *api.ACLPolicy) {
|
||||
_, err := s.ionscaleClient.SetACLPolicy(context.Background(), connect.NewRequest(&api.SetACLPolicyRequest{TailnetId: tailnetID, Policy: policy}))
|
||||
require.NoError(s.t, err)
|
||||
}
|
||||
|
||||
func (s *scenario) NewTailscaleNode(hostname string) TailscaleNode {
|
||||
tailscaleOptions := &dockertest.RunOptions{
|
||||
func (s *Scenario) SetIAMPolicy(tailnetID uint64, policy *api.IAMPolicy) {
|
||||
_, err := s.ionscaleClient.SetIAMPolicy(context.Background(), connect.NewRequest(&api.SetIAMPolicyRequest{TailnetId: tailnetID, Policy: policy}))
|
||||
require.NoError(s.t, err)
|
||||
}
|
||||
|
||||
func (s *Scenario) EnableMachineAutorization(tailnetID uint64) {
|
||||
_, err := s.ionscaleClient.EnableMachineAuthorization(context.Background(), connect.NewRequest(&api.EnableMachineAuthorizationRequest{TailnetId: tailnetID}))
|
||||
require.NoError(s.t, err)
|
||||
}
|
||||
|
||||
func (s *Scenario) PushOIDCUser(sub, email, preferredUsername string) {
|
||||
_, err := s.mockoidcClient.PushUser(context.Background(), connect.NewRequest(&mockoidcv1.PushUserRequest{Subject: sub, Email: email, PreferredUsername: preferredUsername}))
|
||||
require.NoError(s.t, err)
|
||||
}
|
||||
|
||||
type TailscaleNodeConfig struct {
|
||||
Hostname string
|
||||
}
|
||||
|
||||
type TailscaleNodeOpt = func(*TailscaleNodeConfig)
|
||||
|
||||
func WithName(name string) TailscaleNodeOpt {
|
||||
return func(config *TailscaleNodeConfig) {
|
||||
config.Hostname = name
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scenario) NewTailscaleNode(opts ...TailscaleNodeOpt) *tsn.TailscaleNode {
|
||||
config := &TailscaleNodeConfig{Hostname: petname.Generate(3, "-")}
|
||||
for _, o := range opts {
|
||||
o(config)
|
||||
}
|
||||
|
||||
runOpts := &dockertest.RunOptions{
|
||||
Repository: fmt.Sprintf("ts-%s", strings.Replace(targetVersion, ".", "-", -1)),
|
||||
Hostname: hostname,
|
||||
Hostname: config.Hostname,
|
||||
Networks: []*dockertest.Network{s.network},
|
||||
ExposedPorts: []string{"1055"},
|
||||
Cmd: []string{
|
||||
@@ -86,7 +125,7 @@ func (s *scenario) NewTailscaleNode(hostname string) TailscaleNode {
|
||||
}
|
||||
|
||||
resource, err := s.pool.RunWithOptions(
|
||||
tailscaleOptions,
|
||||
runOpts,
|
||||
restartPolicy,
|
||||
)
|
||||
require.NoError(s.t, err)
|
||||
@@ -96,15 +135,10 @@ func (s *scenario) NewTailscaleNode(hostname string) TailscaleNode {
|
||||
|
||||
s.resources = append(s.resources, resource)
|
||||
|
||||
return &tailscaleNode{
|
||||
t: s.t,
|
||||
loginServer: "http://ionscale:8080",
|
||||
hostname: hostname,
|
||||
resource: resource,
|
||||
}
|
||||
return tsn.New(s.t, config.Hostname, "http://ionscale", resource, s.pool.Retry)
|
||||
}
|
||||
|
||||
func Run(t *testing.T, f func(s Scenario)) {
|
||||
func Run(t *testing.T, f func(s *Scenario)) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipped due to -short flag")
|
||||
}
|
||||
@@ -116,7 +150,7 @@ func Run(t *testing.T, f func(s Scenario)) {
|
||||
}
|
||||
|
||||
var err error
|
||||
s := &scenario{t: t}
|
||||
s := &Scenario{t: t}
|
||||
|
||||
defer func() {
|
||||
for _, r := range s.resources {
|
||||
@@ -127,6 +161,10 @@ func Run(t *testing.T, f func(s Scenario)) {
|
||||
_ = pool.Purge(s.ionscale)
|
||||
}
|
||||
|
||||
if s.mockoidc != nil {
|
||||
_ = pool.Purge(s.mockoidc)
|
||||
}
|
||||
|
||||
if s.network != nil {
|
||||
_ = s.network.Close()
|
||||
}
|
||||
@@ -144,6 +182,26 @@ func Run(t *testing.T, f func(s Scenario)) {
|
||||
currentPath, err := os.Getwd()
|
||||
require.NoError(s.t, err)
|
||||
|
||||
// run mockoidc container
|
||||
{
|
||||
mockoidcOpts := &dockertest.RunOptions{
|
||||
Hostname: "mockoidc",
|
||||
Repository: "ghcr.io/jsiebens/mockoidc",
|
||||
Networks: []*dockertest.Network{s.network},
|
||||
ExposedPorts: []string{"80"},
|
||||
Cmd: []string{"--listen-addr", ":80", "--server-url", "http://mockoidc"},
|
||||
}
|
||||
|
||||
s.mockoidc, err = pool.RunWithOptions(mockoidcOpts, restartPolicy)
|
||||
require.NoError(s.t, err)
|
||||
|
||||
port := s.mockoidc.GetPort("80/tcp")
|
||||
err = pool.Retry(httpCheck(port, "/oidc/.well-known/openid-configuration"))
|
||||
require.NoError(s.t, err)
|
||||
|
||||
s.mockoidcClient = mockoidc.NewClient(fmt.Sprintf("http://localhost:%s", port), true)
|
||||
}
|
||||
|
||||
ionscale := &dockertest.RunOptions{
|
||||
Hostname: "ionscale",
|
||||
Repository: "ionscale-test",
|
||||
@@ -151,14 +209,14 @@ func Run(t *testing.T, f func(s Scenario)) {
|
||||
fmt.Sprintf("%s/config:/etc/ionscale", currentPath),
|
||||
},
|
||||
Networks: []*dockertest.Network{s.network},
|
||||
ExposedPorts: []string{"8080"},
|
||||
ExposedPorts: []string{"80"},
|
||||
Cmd: []string{"server", "--config", "/etc/ionscale/config.yaml"},
|
||||
}
|
||||
|
||||
s.ionscale, err = pool.RunWithOptions(ionscale, restartPolicy)
|
||||
require.NoError(s.t, err)
|
||||
|
||||
port := s.ionscale.GetPort("8080/tcp")
|
||||
port := s.ionscale.GetPort("80/tcp")
|
||||
|
||||
err = pool.Retry(httpCheck(port, "/key"))
|
||||
require.NoError(s.t, err)
|
||||
@@ -166,7 +224,7 @@ func Run(t *testing.T, f func(s Scenario)) {
|
||||
auth, err := ionscaleclt.LoadClientAuth("804ecd57365342254ce6647da5c249e85c10a0e51e74856bfdf292a2136b4249")
|
||||
require.NoError(s.t, err)
|
||||
|
||||
s.client, err = ionscaleclt.NewClient(auth, fmt.Sprintf("http://localhost:%s", port), true)
|
||||
s.ionscaleClient, err = ionscaleclt.NewClient(auth, fmt.Sprintf("http://localhost:%s", port), true)
|
||||
require.NoError(s.t, err)
|
||||
|
||||
f(s)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package tsn
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
type Condition = func(*ipnstate.Status) bool
|
||||
|
||||
func Connected() Condition {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
return status.CurrentTailnet != nil
|
||||
}
|
||||
}
|
||||
|
||||
func HasTailnet(tailnet string) Condition {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
return status.CurrentTailnet != nil && status.CurrentTailnet.Name == tailnet
|
||||
}
|
||||
}
|
||||
|
||||
func HasTag(tag string) Condition {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
return status.Self != nil && status.Self.Tags != nil && views.SliceContains[string](*status.Self.Tags, tag)
|
||||
}
|
||||
}
|
||||
|
||||
func NeedsMachineAuth() Condition {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
return status.BackendState == "NeedsMachineAuth"
|
||||
}
|
||||
}
|
||||
|
||||
func IsRunning() Condition {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
return status.BackendState == "Running"
|
||||
}
|
||||
}
|
||||
|
||||
func HasUser(email string) Condition {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
if status.Self == nil {
|
||||
return false
|
||||
}
|
||||
userID := status.Self.UserID
|
||||
if u, ok := status.User[userID]; ok {
|
||||
return u.LoginName == email
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func PeerCount(expected int) Condition {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
return len(status.Peers()) == expected
|
||||
}
|
||||
}
|
||||
|
||||
func HasCapability(capability tailcfg.NodeCapability) Condition {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
self := status.Self
|
||||
|
||||
if self == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if slices.Contains(self.Capabilities, capability) {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, ok := self.CapMap[capability]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func IsMissingCapability(capability tailcfg.NodeCapability) Condition {
|
||||
return func(status *ipnstate.Status) bool {
|
||||
self := status.Self
|
||||
|
||||
if slices.Contains(self.Capabilities, capability) {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, ok := self.CapMap[capability]; ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package tsn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func New(t *testing.T, name, loginServer string, resource *dockertest.Resource, retry func(func() error) error) *TailscaleNode {
|
||||
return &TailscaleNode{
|
||||
t: t,
|
||||
loginServer: loginServer,
|
||||
hostname: name,
|
||||
resource: resource,
|
||||
retry: retry,
|
||||
}
|
||||
}
|
||||
|
||||
type TailscaleNode struct {
|
||||
t *testing.T
|
||||
loginServer string
|
||||
hostname string
|
||||
resource *dockertest.Resource
|
||||
retry func(func() error) error
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) Hostname() string {
|
||||
return t.hostname
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) Up(authkey string) error {
|
||||
t.mustExecTailscaleCmd("up", "--login-server", t.loginServer, "--authkey", authkey)
|
||||
return t.WaitFor(Connected())
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) LoginWithOidc(flags ...UpFlag) (int, error) {
|
||||
check := func(stdout, stderr string) bool {
|
||||
return strings.Contains(stderr, "To authenticate, visit:")
|
||||
}
|
||||
|
||||
cmd := []string{"up", "--login-server", t.loginServer}
|
||||
for _, f := range flags {
|
||||
cmd = append(cmd, f...)
|
||||
}
|
||||
|
||||
_, stderr, err := t.execTailscaleCmdWithCheck(check, cmd...)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
urlStr := strings.ReplaceAll(stderr, "To authenticate, visit:\n\n\t", "")
|
||||
urlStr = strings.TrimSpace(urlStr)
|
||||
|
||||
u, err := url.Parse(urlStr)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
q := u.Query()
|
||||
q.Set("oidc", "true")
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
responseCode, err := t.curl(u)
|
||||
require.NoError(t.t, err)
|
||||
|
||||
if responseCode == http.StatusOK {
|
||||
return responseCode, t.WaitFor(Connected())
|
||||
}
|
||||
|
||||
return responseCode, nil
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) IPv4() string {
|
||||
return t.mustExecTailscaleCmd("ip", "-4")
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) IPv6() string {
|
||||
return t.mustExecTailscaleCmd("ip", "-6")
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) WaitFor(c Condition, additional ...Condition) error {
|
||||
err := t.retry(func() error {
|
||||
out, _, err := t.execTailscaleCmd("status", "--json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var status ipnstate.Status
|
||||
if err := json.Unmarshal([]byte(out), &status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !c(&status) {
|
||||
return fmt.Errorf("condition not met")
|
||||
}
|
||||
|
||||
for _, a := range additional {
|
||||
if !a(&status) {
|
||||
return fmt.Errorf("condition not met")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) Check(c Condition, additional ...Condition) error {
|
||||
out, _, err := t.execTailscaleCmd("status", "--json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var status ipnstate.Status
|
||||
if err := json.Unmarshal([]byte(out), &status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !c(&status) {
|
||||
return fmt.Errorf("condition not met")
|
||||
}
|
||||
|
||||
for _, a := range additional {
|
||||
if !a(&status) {
|
||||
return fmt.Errorf("condition not met")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) Ping(target string) error {
|
||||
result, _, err := t.execTailscaleCmd("ping", "--timeout=1s", "--c=10", "--until-direct=true", target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.Contains(result, "pong") && !strings.Contains(result, "is local") {
|
||||
return fmt.Errorf("ping failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) curl(url *url.URL) (int, error) {
|
||||
cmd := []string{"curl", "-L", "-s", "-o", "/dev/null", "-w", "%{http_code}", url.String()}
|
||||
stdout, _, err := execCmd(t.resource, cmd...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return strconv.Atoi(stdout)
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) execTailscaleCmd(cmd ...string) (string, string, error) {
|
||||
i := append([]string{"/app/tailscale", "--socket=/tmp/tailscaled.sock"}, cmd...)
|
||||
return execCmd(t.resource, i...)
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) execTailscaleCmdWithCheck(check func(string, string) bool, cmd ...string) (string, string, error) {
|
||||
i := append([]string{"/app/tailscale", "--socket=/tmp/tailscaled.sock"}, cmd...)
|
||||
return execCmdWithCheck(t.resource, check, i...)
|
||||
}
|
||||
|
||||
func (t *TailscaleNode) mustExecTailscaleCmd(cmd ...string) string {
|
||||
i := append([]string{"/app/tailscale", "--socket=/tmp/tailscaled.sock"}, cmd...)
|
||||
s, _, err := execCmd(t.resource, i...)
|
||||
require.NoError(t.t, err)
|
||||
return s
|
||||
}
|
||||
|
||||
func execCmd(resource *dockertest.Resource, cmd ...string) (string, string, error) {
|
||||
return execCmdWithCheck(resource, nil, cmd...)
|
||||
}
|
||||
|
||||
func execCmdWithCheck(resource *dockertest.Resource, check func(string, string) bool, cmd ...string) (string, string, error) {
|
||||
tr := strings.TrimSpace
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
type result struct {
|
||||
exitCode int
|
||||
err error
|
||||
}
|
||||
|
||||
resultChan := make(chan result, 1)
|
||||
checkChan := make(chan bool, 1)
|
||||
|
||||
go func() {
|
||||
exitCode, err := resource.Exec(cmd, dockertest.ExecOptions{StdOut: &stdout, StdErr: &stderr})
|
||||
resultChan <- result{exitCode, err}
|
||||
}()
|
||||
|
||||
if check != nil {
|
||||
done := make(chan bool)
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
done <- true
|
||||
close(done)
|
||||
}()
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if check(tr(stdout.String()), tr(stderr.String())) {
|
||||
checkChan <- true
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
select {
|
||||
case <-checkChan:
|
||||
return tr(stdout.String()), tr(stderr.String()), nil
|
||||
case res := <-resultChan:
|
||||
if res.err != nil {
|
||||
return stdout.String(), stderr.String(), res.err
|
||||
}
|
||||
|
||||
if res.exitCode != 0 {
|
||||
return tr(stdout.String()), tr(stderr.String()), fmt.Errorf("invalid exit code %d", res.exitCode)
|
||||
}
|
||||
|
||||
return tr(stdout.String()), tr(stderr.String()), nil
|
||||
case <-time.After(60 * time.Second):
|
||||
return tr(stdout.String()), tr(stderr.String()), fmt.Errorf("command timed out")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package tsn
|
||||
|
||||
type UpFlag = []string
|
||||
|
||||
func WithAdvertiseTags(tags string) UpFlag {
|
||||
return []string{"--advertise-tags", tags}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"github.com/jsiebens/ionscale/pkg/defaults"
|
||||
api "github.com/jsiebens/ionscale/pkg/gen/ionscale/v1"
|
||||
"github.com/jsiebens/ionscale/tests/sc"
|
||||
"github.com/jsiebens/ionscale/tests/tsn"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"net/http"
|
||||
"tailscale.com/tailcfg"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func newTailscaleNodeAndLoginWithOIDC(t *testing.T, s *sc.Scenario, expectedLoginName string, flags ...tsn.UpFlag) *tsn.TailscaleNode {
|
||||
node := s.NewTailscaleNode()
|
||||
code, err := node.LoginWithOidc(flags...)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, code)
|
||||
|
||||
require.NoError(t, node.WaitFor(tsn.Connected()))
|
||||
require.NoError(t, node.WaitFor(tsn.HasUser(expectedLoginName)))
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func TestWebLoginWithDomainFilterInIAMPolicy(t *testing.T) {
|
||||
sc.Run(t, func(s *sc.Scenario) {
|
||||
s.PushOIDCUser("123", "john@localtest.me", "john")
|
||||
s.PushOIDCUser("124", "jane@localtest.me", "jane")
|
||||
|
||||
tailnet := s.CreateTailnet()
|
||||
s.SetIAMPolicy(tailnet.Id, &api.IAMPolicy{Filters: []string{"domain == localtest.me"}})
|
||||
|
||||
john := newTailscaleNodeAndLoginWithOIDC(t, s, "john@localtest.me")
|
||||
jane := newTailscaleNodeAndLoginWithOIDC(t, s, "jane@localtest.me")
|
||||
|
||||
require.NoError(t, john.Check(tsn.HasTailnet(tailnet.Name)))
|
||||
require.NoError(t, jane.Check(tsn.HasTailnet(tailnet.Name)))
|
||||
|
||||
require.NoError(t, john.WaitFor(tsn.PeerCount(1)))
|
||||
require.NoError(t, jane.WaitFor(tsn.PeerCount(1)))
|
||||
})
|
||||
}
|
||||
|
||||
func TestWebLoginWithSubsAndEmailsInIAMPolicy(t *testing.T) {
|
||||
sc.Run(t, func(s *sc.Scenario) {
|
||||
s.PushOIDCUser("123", "john@localtest.me", "john")
|
||||
s.PushOIDCUser("124", "jane@localtest.me", "jane")
|
||||
|
||||
tailnet := s.CreateTailnet()
|
||||
s.SetIAMPolicy(tailnet.Id, &api.IAMPolicy{Subs: []string{"123"}, Emails: []string{"jane@localtest.me"}})
|
||||
|
||||
john := newTailscaleNodeAndLoginWithOIDC(t, s, "john@localtest.me")
|
||||
jane := newTailscaleNodeAndLoginWithOIDC(t, s, "jane@localtest.me")
|
||||
|
||||
require.NoError(t, john.WaitFor(tsn.PeerCount(1)))
|
||||
require.NoError(t, jane.WaitFor(tsn.PeerCount(1)))
|
||||
})
|
||||
}
|
||||
|
||||
func TestWebLoginWithUserAsTailnetAdmin(t *testing.T) {
|
||||
sc.Run(t, func(s *sc.Scenario) {
|
||||
s.PushOIDCUser("123", "john@localtest.me", "john")
|
||||
s.PushOIDCUser("124", "jane@localtest.me", "jane")
|
||||
|
||||
tailnet := s.CreateTailnet()
|
||||
s.SetIAMPolicy(tailnet.Id, &api.IAMPolicy{
|
||||
Filters: []string{"domain == localtest.me"},
|
||||
Roles: map[string]string{"john@localtest.me": "admin"},
|
||||
})
|
||||
|
||||
john := newTailscaleNodeAndLoginWithOIDC(t, s, "john@localtest.me")
|
||||
jane := newTailscaleNodeAndLoginWithOIDC(t, s, "jane@localtest.me")
|
||||
|
||||
require.NoError(t, john.Check(tsn.HasCapability(tailcfg.CapabilityAdmin)))
|
||||
require.NoError(t, jane.Check(tsn.IsMissingCapability(tailcfg.CapabilityAdmin)))
|
||||
})
|
||||
}
|
||||
|
||||
func TestWebLoginWhenNotAuthorizedForAnyTailnet(t *testing.T) {
|
||||
sc.Run(t, func(s *sc.Scenario) {
|
||||
s.PushOIDCUser("124", "jane@localtest.me", "jane")
|
||||
|
||||
tailnet := s.CreateTailnet()
|
||||
s.SetIAMPolicy(tailnet.Id, &api.IAMPolicy{
|
||||
Subs: []string{"123"},
|
||||
})
|
||||
|
||||
jane := s.NewTailscaleNode()
|
||||
code, err := jane.LoginWithOidc()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusForbidden, code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWebLoginWhenInvalidTagOwner(t *testing.T) {
|
||||
sc.Run(t, func(s *sc.Scenario) {
|
||||
s.PushOIDCUser("124", "jane@localtest.me", "jane")
|
||||
|
||||
tailnet := s.CreateTailnet()
|
||||
s.SetIAMPolicy(tailnet.Id, &api.IAMPolicy{
|
||||
Subs: []string{"124"},
|
||||
})
|
||||
|
||||
jane := s.NewTailscaleNode()
|
||||
code, err := jane.LoginWithOidc(tsn.WithAdvertiseTags("tag:test"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusForbidden, code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWebLoginAsTagOwner(t *testing.T) {
|
||||
sc.Run(t, func(s *sc.Scenario) {
|
||||
s.PushOIDCUser("124", "jane@localtest.me", "jane")
|
||||
|
||||
owners, err := structpb.NewList([]interface{}{"jane@localtest.me"})
|
||||
require.NoError(t, err)
|
||||
|
||||
aclPolicy := defaults.DefaultACLPolicy()
|
||||
aclPolicy.Tagowners = map[string]*structpb.ListValue{
|
||||
"tag:localtest": owners,
|
||||
}
|
||||
|
||||
tailnet := s.CreateTailnet()
|
||||
s.SetACLPolicy(tailnet.Id, aclPolicy)
|
||||
s.SetIAMPolicy(tailnet.Id, &api.IAMPolicy{
|
||||
Subs: []string{"124"},
|
||||
})
|
||||
|
||||
newTailscaleNodeAndLoginWithOIDC(t, s, "tagged-devices", tsn.WithAdvertiseTags("tag:localtest"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestWebLoginWithMachineAuthorizationRequired(t *testing.T) {
|
||||
sc.Run(t, func(s *sc.Scenario) {
|
||||
s.PushOIDCUser("123", "john@localtest.me", "john")
|
||||
|
||||
tailnet := s.CreateTailnet()
|
||||
s.SetIAMPolicy(tailnet.Id, &api.IAMPolicy{Filters: []string{"domain == localtest.me"}})
|
||||
s.EnableMachineAutorization(tailnet.Id)
|
||||
|
||||
node := newTailscaleNodeAndLoginWithOIDC(t, s, "john@localtest.me")
|
||||
|
||||
require.NoError(t, node.Check(tsn.NeedsMachineAuth()))
|
||||
|
||||
s.AuthorizeMachines(tailnet.Id)
|
||||
|
||||
require.NoError(t, node.WaitFor(tsn.IsRunning()))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user