Files
core/http/middleware/iam/iam_test.go
2024-07-23 15:54:09 +02:00

428 lines
11 KiB
Go

package iam
import (
"bytes"
"encoding/base64"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/http/api"
apihandler "github.com/datarhei/core/v16/http/handler/api"
"github.com/datarhei/core/v16/http/validator"
"github.com/datarhei/core/v16/iam"
iamidentity "github.com/datarhei/core/v16/iam/identity"
"github.com/datarhei/core/v16/iam/policy"
"github.com/datarhei/core/v16/io/fs"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/require"
)
var basic string = "Basic"
func getIAM() (iam.IAM, error) {
dummyfs, err := fs.NewMemFilesystem(fs.MemConfig{})
if err != nil {
return nil, err
}
policyAdapter, err := policy.NewJSONAdapter(dummyfs, "./policy.json", nil)
if err != nil {
return nil, err
}
identityAdapter, err := iamidentity.NewJSONAdapter(dummyfs, "./users.json", nil)
if err != nil {
return nil, err
}
i, err := iam.New(iam.Config{
PolicyAdapter: policyAdapter,
IdentityAdapter: identityAdapter,
Superuser: iamidentity.User{
Name: "admin",
},
JWTRealm: "datarhei-core",
JWTSecret: "1234567890",
Logger: nil,
})
if err != nil {
return nil, err
}
i.CreateIdentity(iamidentity.User{
Name: "foobar",
Auth: iamidentity.UserAuth{
API: iamidentity.UserAuthAPI{
Password: "secret",
},
Services: iamidentity.UserAuthServices{
Basic: []string{"secret"},
},
},
})
i.CreateIdentity(iamidentity.User{
Name: "foobar:2",
Auth: iamidentity.UserAuth{
API: iamidentity.UserAuthAPI{
Password: "secret",
},
Services: iamidentity.UserAuthServices{
Basic: []string{"secret"},
},
},
})
return i, nil
}
func TestNoIAM(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
res := httptest.NewRecorder()
c := e.NewContext(req, res)
h := New()(func(c echo.Context) error {
return c.String(http.StatusOK, "test")
})
err := h(c)
require.Error(t, err)
he := err.(api.Error)
require.Equal(t, http.StatusForbidden, he.Code)
}
func TestBasicAuth(t *testing.T) {
iam, err := getIAM()
require.NoError(t, err)
iam.AddPolicy("foobar", "$none", []string{"fs"}, "/**", []string{"ANY"})
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
res := httptest.NewRecorder()
c := e.NewContext(req, res)
h := NewWithConfig(Config{
IAM: iam,
})(func(c echo.Context) error {
return c.String(http.StatusOK, "test")
})
// No credentials
err = h(c)
require.Error(t, err)
he := err.(api.Error)
require.Equal(t, http.StatusUnauthorized, he.Code)
require.Equal(t, basic+` realm=datarhei-core`, res.Header().Get(echo.HeaderWWWAuthenticate))
// Valid credentials
auth := basic + " " + base64.StdEncoding.EncodeToString([]byte("foobar:secret"))
req.Header.Set(echo.HeaderAuthorization, auth)
require.NoError(t, h(c))
// Case-insensitive header scheme
auth = strings.ToUpper(basic) + " " + base64.StdEncoding.EncodeToString([]byte("foobar:secret"))
req.Header.Set(echo.HeaderAuthorization, auth)
require.NoError(t, h(c))
// Invalid credentials
auth = basic + " " + base64.StdEncoding.EncodeToString([]byte("foobar:invalid-password"))
req.Header.Set(echo.HeaderAuthorization, auth)
he = h(c).(api.Error)
require.Equal(t, http.StatusUnauthorized, he.Code)
require.Equal(t, basic+` realm=datarhei-core`, res.Header().Get(echo.HeaderWWWAuthenticate))
// Invalid base64 string
auth = basic + " invalidString"
req.Header.Set(echo.HeaderAuthorization, auth)
he = h(c).(api.Error)
require.Equal(t, http.StatusBadRequest, he.Code)
// Missing Authorization header
req.Header.Del(echo.HeaderAuthorization)
he = h(c).(api.Error)
require.Equal(t, http.StatusUnauthorized, he.Code)
// Invalid Authorization header
auth = base64.StdEncoding.EncodeToString([]byte("invalid"))
req.Header.Set(echo.HeaderAuthorization, auth)
he = h(c).(api.Error)
require.Equal(t, http.StatusUnauthorized, he.Code)
}
func TestBasicAuthEncoding(t *testing.T) {
iam, err := getIAM()
require.NoError(t, err)
iam.AddPolicy("foobar:2", "$none", []string{"fs"}, "/**", []string{"ANY"})
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
res := httptest.NewRecorder()
c := e.NewContext(req, res)
h := NewWithConfig(Config{
IAM: iam,
})(func(c echo.Context) error {
return c.String(http.StatusOK, "test")
})
// Valid credentials
auth := basic + " " + base64.StdEncoding.EncodeToString([]byte("foobar%3A2:secret"))
req.Header.Set(echo.HeaderAuthorization, auth)
require.NoError(t, h(c))
// Invalid encoded credentials
auth = basic + " " + base64.StdEncoding.EncodeToString([]byte("foobar%32:secret"))
req.Header.Set(echo.HeaderAuthorization, auth)
he := h(c).(api.Error)
require.Equal(t, http.StatusUnauthorized, he.Code)
require.Equal(t, basic+` realm=datarhei-core`, res.Header().Get(echo.HeaderWWWAuthenticate))
// Invalid credentials
auth = basic + " " + base64.StdEncoding.EncodeToString([]byte("foobar:2:secret"))
req.Header.Set(echo.HeaderAuthorization, auth)
he = h(c).(api.Error)
require.Equal(t, http.StatusUnauthorized, he.Code)
require.Equal(t, basic+` realm=datarhei-core`, res.Header().Get(echo.HeaderWWWAuthenticate))
}
func TestFindDomainFromFilesystem(t *testing.T) {
iam, err := getIAM()
require.NoError(t, err)
iam.AddPolicy("$anon", "$none", []string{"fs"}, "/**", []string{"ANY"})
iam.AddPolicy("foobar", "group", []string{"fs"}, "/group/**", []string{"ANY"})
iam.AddPolicy("foobar", "anothergroup", []string{"fs"}, "/memfs/anothergroup/**", []string{"ANY"})
mw := &iammiddleware{
iam: iam,
mounts: []string{"/", "/memfs"},
}
domain := mw.findDomainFromFilesystem("/")
require.Equal(t, "", domain)
domain = mw.findDomainFromFilesystem("/group/bla")
require.Equal(t, "group", domain)
domain = mw.findDomainFromFilesystem("/anothergroup/bla")
require.Equal(t, "anothergroup", domain)
domain = mw.findDomainFromFilesystem("/memfs/anothergroup/bla")
require.Equal(t, "anothergroup", domain)
}
func TestBasicAuthDomain(t *testing.T) {
iam, err := getIAM()
require.NoError(t, err)
iam.AddPolicy("$anon", "$none", []string{"fs"}, "/**", []string{"ANY"})
iam.AddPolicy("foobar", "group", []string{"fs"}, "/group/**", []string{"ANY"})
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
res := httptest.NewRecorder()
c := e.NewContext(req, res)
h := NewWithConfig(Config{
IAM: iam,
Mounts: []string{"/"},
})(func(c echo.Context) error {
return c.String(http.StatusOK, "test")
})
// No credentials
require.NoError(t, h(c))
req = httptest.NewRequest(http.MethodGet, "/group/bla", nil)
c = e.NewContext(req, res)
err = h(c)
require.Error(t, err)
he := err.(api.Error)
require.Equal(t, http.StatusUnauthorized, he.Code)
require.Equal(t, basic+` realm=datarhei-core`, res.Header().Get(echo.HeaderWWWAuthenticate))
// Valid credentials
auth := basic + " " + base64.StdEncoding.EncodeToString([]byte("foobar:secret"))
req.Header.Set(echo.HeaderAuthorization, auth)
require.NoError(t, h(c))
// Allow anonymous group read access
iam.AddPolicy("$anon", "group", []string{"fs"}, "/group/**", []string{"GET"})
req.Header.Del(echo.HeaderAuthorization)
require.NoError(t, h(c))
}
func TestAPILoginAndRefresh(t *testing.T) {
iam, err := getIAM()
require.NoError(t, err)
iam.AddPolicy("foobar", "$none", []string{"api"}, "/**", []string{"ANY"})
jwthandler := apihandler.NewJWT(iam)
e := echo.New()
e.Validator = validator.New()
res := httptest.NewRecorder()
h := NewWithConfig(Config{
IAM: iam,
Mounts: []string{"/"},
})(func(c echo.Context) error {
if c.Request().Method == http.MethodPost {
if c.Request().URL.Path == "/api/login" {
return jwthandler.Login(c)
}
}
if c.Request().Method == http.MethodGet {
if c.Request().URL.Path == "/api/login/refresh" {
return jwthandler.Refresh(c)
}
}
return c.String(http.StatusOK, "test")
})
req := httptest.NewRequest(http.MethodPost, "/api/login", nil)
c := e.NewContext(req, res)
// No credentials
err = h(c)
require.Error(t, err)
he := err.(api.Error)
require.Equal(t, http.StatusForbidden, he.Code)
// Wrong password
login := api.Login{
Username: "foobar",
Password: "nosecret",
}
data, err := json.Marshal(login)
require.NoError(t, err)
req = httptest.NewRequest(http.MethodPost, "/api/login", bytes.NewReader(data))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
c = e.NewContext(req, res)
err = h(c)
require.Error(t, err)
he = err.(api.Error)
require.Equal(t, http.StatusForbidden, he.Code)
// Wrong username
login = api.Login{
Username: "foobaz",
Password: "secret",
}
data, err = json.Marshal(login)
require.NoError(t, err)
req = httptest.NewRequest(http.MethodPost, "/api/login", bytes.NewReader(data))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
c = e.NewContext(req, res)
err = h(c)
require.Error(t, err)
he = err.(api.Error)
require.Equal(t, http.StatusForbidden, he.Code)
// Correct credentials
login = api.Login{
Username: "foobar",
Password: "secret",
}
data, err = json.Marshal(login)
require.NoError(t, err)
req = httptest.NewRequest(http.MethodPost, "/api/login", bytes.NewReader(data))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
c = e.NewContext(req, res)
res.Body.Reset()
err = h(c)
require.NoError(t, err)
data, err = io.ReadAll(res.Body)
require.NoError(t, err)
jwt := api.JWT{}
err = json.Unmarshal(data, &jwt)
require.NoError(t, err)
// No JWT
req = httptest.NewRequest(http.MethodGet, "/api/some/endpoint", nil)
c = e.NewContext(req, res)
err = h(c)
require.Error(t, err)
he = err.(api.Error)
require.Equal(t, http.StatusForbidden, he.Code)
// With invalid JWT
req.Header.Set(echo.HeaderAuthorization, "Bearer invalid")
err = h(c)
require.Error(t, err)
// With refresh JWT
req.Header.Set(echo.HeaderAuthorization, "Bearer "+jwt.RefreshToken)
err = h(c)
require.Error(t, err)
// With access JWT
req.Header.Set(echo.HeaderAuthorization, "Bearer "+jwt.AccessToken)
err = h(c)
require.NoError(t, err)
// Refresh JWT
req = httptest.NewRequest(http.MethodGet, "/api/login/refresh", nil)
c = e.NewContext(req, res)
err = h(c)
require.Error(t, err)
he = err.(api.Error)
require.Equal(t, http.StatusForbidden, he.Code)
req.Header.Set(echo.HeaderAuthorization, "Bearer "+jwt.AccessToken)
err = h(c)
require.Error(t, err)
he = err.(api.Error)
require.Equal(t, http.StatusForbidden, he.Code)
req.Header.Set(echo.HeaderAuthorization, "Bearer "+jwt.RefreshToken)
res.Body.Reset()
err = h(c)
require.NoError(t, err)
data, err = io.ReadAll(res.Body)
require.NoError(t, err)
jwtrefresh := api.JWTRefresh{}
err = json.Unmarshal(data, &jwtrefresh)
require.NoError(t, err)
req = httptest.NewRequest(http.MethodGet, "/api/some/endpoint", nil)
c = e.NewContext(req, res)
// With new access JWT
req.Header.Set(echo.HeaderAuthorization, "Bearer "+jwtrefresh.AccessToken)
err = h(c)
require.NoError(t, err)
}