Merged in feature/permit-integration-1 (pull request #172)
Permit integration - part 1 * WIP permit integration * slim down build * Merge branch 'main' of bitbucket.org:aarete/query-orchestration into feature/permit-integration-1 * omit swagger from auth * clean build * comment * part 1 completed this is the initial permitio parts and tests without integration. Also testing.md since we have a new test target `task test:permitio` * feature flag for no jwt validation * permit integration * fix ci unit tests * ci fix * build fix * fix home handler * fix redirect * test fix * update docs for auth
This commit is contained in:
@@ -2,6 +2,7 @@ package queryapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
@@ -21,11 +22,42 @@ func (s *Controllers) LoginCallback(ctx echo.Context, params LoginCallbackParams
|
||||
}
|
||||
|
||||
// Logout is called by the swagger generated code for the logout endpoint.
|
||||
// It redirects to the Cognito logout endpoint to properly terminate the session,
|
||||
// which then redirects back to the home page.
|
||||
func (s *Controllers) Logout(ctx echo.Context) error {
|
||||
// the middleware will have already removed the auth cookie
|
||||
// so we can just redirect to the home page after logging out.
|
||||
return ctx.Redirect(http.StatusFound, "/home")
|
||||
|
||||
// Construct the Cognito logout URL with proper parameters
|
||||
logoutURL := s.authConfig.GetAuthLogoutURL()
|
||||
clientID := s.authConfig.GetAuthClientID()
|
||||
|
||||
// The logout_uri should redirect back to our home page
|
||||
// Use the same base URL that was used for redirect URI setup
|
||||
baseURL := ctx.Scheme() + "://" + ctx.Request().Host
|
||||
logoutRedirectURI := baseURL + s.authConfig.GetAuthHomePath()
|
||||
|
||||
// Build the full Cognito logout URL with query parameters
|
||||
parsedURL, err := url.Parse(logoutURL)
|
||||
if err != nil {
|
||||
s.authConfig.GetAuthLogger().Error("Failed to parse logout URL", "error", err)
|
||||
// Fallback to simple redirect to home
|
||||
return ctx.Redirect(http.StatusFound, s.authConfig.GetAuthHomePath())
|
||||
}
|
||||
|
||||
query := parsedURL.Query()
|
||||
query.Set("client_id", clientID)
|
||||
query.Set("logout_uri", logoutRedirectURI)
|
||||
parsedURL.RawQuery = query.Encode()
|
||||
|
||||
cognitoLogoutURL := parsedURL.String()
|
||||
|
||||
s.authConfig.GetAuthLogger().Debug("Redirecting to Cognito logout",
|
||||
"logout_url", cognitoLogoutURL,
|
||||
"client_id", clientID,
|
||||
"logout_uri", logoutRedirectURI)
|
||||
|
||||
// Redirect to Cognito logout endpoint
|
||||
return ctx.Redirect(http.StatusFound, cognitoLogoutURL)
|
||||
}
|
||||
|
||||
func (s *Controllers) GetHomePage(ctx echo.Context) error {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
queryapi "queryorchestration/api/queryAPI"
|
||||
"queryorchestration/internal/database/repository"
|
||||
"queryorchestration/internal/serviceconfig"
|
||||
"queryorchestration/internal/test"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
@@ -20,9 +21,10 @@ func TestCreateClient(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
body := queryapi.ClientCreate{
|
||||
Name: "example_name",
|
||||
@@ -48,9 +50,10 @@ func TestGetClient(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
id := "client_id"
|
||||
|
||||
@@ -81,9 +84,10 @@ func TestUpdateClient(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
id := "client_id"
|
||||
|
||||
@@ -110,6 +114,25 @@ func TestUpdateClient(t *testing.T) {
|
||||
assert.Equal(t, "new_name", client.Name)
|
||||
}
|
||||
|
||||
func initializeTestConfig(t testing.TB, cfg *ControllerConfig) {
|
||||
// Reset the singleton config state for testing
|
||||
serviceconfig.ResetConfigInitOnceTestOnly()
|
||||
|
||||
// Initialize the base configuration
|
||||
err := serviceconfig.InitializeConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Initialize auth configuration with test defaults
|
||||
cfg.SetAuthUserPoolID("test-pool-id")
|
||||
cfg.SetAuthClientID("test-client-id")
|
||||
cfg.SetAuthClientSecret("test-client-secret")
|
||||
cfg.SetAuthDomain("https://test-domain.com")
|
||||
cfg.SetAuthRegion("us-east-1")
|
||||
|
||||
err = cfg.InitializeAuthConfig("http://localhost:8080", cfg.GetLogger())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func createContext(t testing.TB) (echo.Context, *httptest.ResponseRecorder) {
|
||||
return createContextWithJSONBody(t, struct{}{})
|
||||
}
|
||||
|
||||
@@ -18,13 +18,14 @@ func TestSetCollector(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
acfg := test.CreateAWSContainer(t, cfg)
|
||||
|
||||
test.SetQueueClient(t, t.Context(), cfg, acfg.ExternalEndpoint)
|
||||
cfg.ClientSyncURL = test.CreateQueue(t, cfg, test.ClientSyncRunnerName)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
id := "clientid"
|
||||
|
||||
@@ -62,9 +63,10 @@ func TestGetCollectorByClientId(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
id := "clientid"
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"queryorchestration/internal/query"
|
||||
querytest "queryorchestration/internal/query/test"
|
||||
queryupdate "queryorchestration/internal/query/update"
|
||||
"queryorchestration/internal/serviceconfig/auth"
|
||||
)
|
||||
|
||||
const Name = "queryAPI"
|
||||
@@ -29,11 +30,13 @@ type Services struct {
|
||||
}
|
||||
|
||||
type Controllers struct {
|
||||
svc *Services
|
||||
svc *Services
|
||||
authConfig auth.ConfigProvider
|
||||
}
|
||||
|
||||
func NewControllers(services *Services) *Controllers {
|
||||
func NewControllers(services *Services, authConfig auth.ConfigProvider) *Controllers {
|
||||
return &Controllers{
|
||||
svc: services,
|
||||
svc: services,
|
||||
authConfig: authConfig,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,13 +20,14 @@ func TestUploadDocument(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
acfg := test.CreateAWSContainer(t, cfg)
|
||||
|
||||
test.SetStoreClient(t, t.Context(), cfg, acfg.ExternalEndpoint)
|
||||
test.CreateBucket(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
err := cfg.GetDBQueries().CreateClient(t.Context(), &repository.CreateClientParams{
|
||||
Clientid: "client_id",
|
||||
@@ -59,9 +60,10 @@ func TestListDocumentsByClientId(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
err := cfg.GetDBQueries().CreateClient(t.Context(), &repository.CreateClientParams{
|
||||
Clientid: "client_id",
|
||||
@@ -80,7 +82,7 @@ func TestListDocumentsByClientId(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, rec.Code)
|
||||
assertBody(t, rec, queryapi.ListDocuments{
|
||||
{
|
||||
queryapi.DocumentSummary{
|
||||
Id: docId,
|
||||
Hash: "hash",
|
||||
},
|
||||
@@ -91,9 +93,10 @@ func TestGetDocument(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
err := cfg.GetDBQueries().CreateClient(t.Context(), &repository.CreateClientParams{
|
||||
Clientid: "client_id",
|
||||
|
||||
@@ -14,9 +14,10 @@ import (
|
||||
func TestTriggerExport(t *testing.T) {
|
||||
ctx, rec := createContext(t)
|
||||
cfg := &ControllerConfig{}
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
err := cons.TriggerExport(ctx, "clientid")
|
||||
require.NoError(t, err)
|
||||
@@ -28,9 +29,10 @@ func TestExportState(t *testing.T) {
|
||||
ctx, rec := createContext(t)
|
||||
|
||||
cfg := &ControllerConfig{}
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
id := uuid.New()
|
||||
|
||||
|
||||
+31
-21
@@ -59,29 +59,41 @@ func HomeHandler(c echo.Context) error {
|
||||
isAuthenticated := (err == nil && tokenCookie.Value != "")
|
||||
|
||||
// Variables for user info
|
||||
username := ""
|
||||
email := ""
|
||||
var userGroups []string
|
||||
var subject string
|
||||
|
||||
// If authenticated, try to decode the JWT token to get user info
|
||||
if isAuthenticated {
|
||||
// Parse the JWT token without verification (just to extract info for display)
|
||||
// In a production application, you would want to verify the token here as well
|
||||
token, _ := jwt.Parse([]byte(tokenCookie.Value), jwt.WithVerify(false))
|
||||
if token != nil {
|
||||
claims, _ := token.AsMap(context.Background())
|
||||
username, _ = claims["cognito:username"].(string)
|
||||
email, _ = claims["email"].(string)
|
||||
// Use the same JWT parsing logic as the middleware to handle formatted tokens
|
||||
tokenString := tokenCookie.Value
|
||||
|
||||
// Try to extract groups
|
||||
if rawGroups, ok := claims["cognito:groups"]; ok {
|
||||
if groups, ok := rawGroups.([]interface{}); ok {
|
||||
userGroups = make([]string, len(groups))
|
||||
for i, g := range groups {
|
||||
userGroups[i] = fmt.Sprint(g)
|
||||
}
|
||||
// Handle RFC-compliant unsecured JWTs that may have only 2 parts (header.payload.)
|
||||
parts := strings.Split(tokenString, ".")
|
||||
if len(parts) == 2 {
|
||||
// Add empty signature part to make it parseable by the JWT library
|
||||
tokenString = tokenString + "."
|
||||
}
|
||||
|
||||
// Parse token without verification (just for display purposes)
|
||||
token, err := jwt.Parse(
|
||||
[]byte(tokenString),
|
||||
jwt.WithVerify(false), // Skip signature verification
|
||||
jwt.WithValidate(false), // Skip expiration and other validations
|
||||
)
|
||||
if err != nil {
|
||||
subject = "JWT parsing failed: " + err.Error()
|
||||
} else if token != nil {
|
||||
claims, err := token.AsMap(context.Background())
|
||||
if err != nil {
|
||||
subject = "Claims extraction failed: " + err.Error()
|
||||
} else {
|
||||
if sub, ok := claims["sub"].(string); ok {
|
||||
subject = sub
|
||||
} else {
|
||||
subject = "Subject claim not found or invalid type"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
subject = "Token is nil"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,12 +103,10 @@ func HomeHandler(c echo.Context) error {
|
||||
authStatusHTML = fmt.Sprintf(`
|
||||
<div class="auth-status authenticated">
|
||||
<h2>Authentication Status: Authenticated</h2>
|
||||
<p><strong>Username:</strong> %s</p>
|
||||
<p><strong>Email:</strong> %s</p>
|
||||
<p><strong>Groups:</strong> %s</p>
|
||||
<p><strong>Subject:</strong> %s</p>
|
||||
<p><a href="/logout" class="logout-btn">Logout</a></p>
|
||||
</div>
|
||||
`, username, email, strings.Join(userGroups, ", "))
|
||||
`, subject)
|
||||
} else {
|
||||
authStatusHTML = `
|
||||
<div class="auth-status unauthenticated">
|
||||
|
||||
@@ -44,9 +44,10 @@ func TestCreateQuery(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
body := queryapi.QueryCreate{
|
||||
Type: queryapi.CONTEXTFULL,
|
||||
@@ -64,9 +65,10 @@ func TestListQueries(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
ctx, rec := createContext(t)
|
||||
|
||||
@@ -92,9 +94,10 @@ func TestGetQuery(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
ctx, rec := createContext(t)
|
||||
|
||||
@@ -116,13 +119,14 @@ func TestUpdateQuery(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
acfg := test.CreateAWSContainer(t, cfg)
|
||||
|
||||
test.SetQueueClient(t, t.Context(), cfg, acfg.ExternalEndpoint)
|
||||
cfg.QueryVersionSyncURL = test.CreateQueue(t, cfg, test.QueryVersionSyncRunnerName)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
id, err := cfg.GetDBQueries().CreateQuery(t.Context(), repository.QuerytypeJsonExtractor)
|
||||
require.NoError(t, err)
|
||||
@@ -229,9 +233,10 @@ func TestTestQuery(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
contextQueryId, err := svc.Query.Create(t.Context(), &resultprocessor.Create{
|
||||
Type: resultprocessor.TypeContextFull,
|
||||
|
||||
@@ -16,9 +16,10 @@ func TestGetClientStatus(t *testing.T) {
|
||||
t.Parallel()
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(t, cfg)
|
||||
initializeTestConfig(t, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
ctx, rec := createContext(t)
|
||||
|
||||
@@ -46,9 +47,10 @@ func TestGetClientStatus(t *testing.T) {
|
||||
func BenchmarkGetClientStatus(b *testing.B) {
|
||||
cfg := &ControllerConfig{}
|
||||
test.CreateDB(b, cfg)
|
||||
initializeTestConfig(b, cfg)
|
||||
|
||||
svc := createControllerServices(cfg)
|
||||
cons := queryapi.NewControllers(svc)
|
||||
cons := queryapi.NewControllers(svc, cfg)
|
||||
|
||||
ctx, _ := createContext(b)
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package queryapi
|
||||
|
||||
import "log/slog"
|
||||
|
||||
// mockAuthConfig provides a minimal implementation of auth.ConfigProvider for testing
|
||||
type mockAuthConfig struct{}
|
||||
|
||||
func (m *mockAuthConfig) GetAuthRegion() string { return "us-east-1" }
|
||||
func (m *mockAuthConfig) SetAuthRegion(string) {}
|
||||
func (m *mockAuthConfig) GetAuthUserPoolID() string { return "test-pool" }
|
||||
func (m *mockAuthConfig) SetAuthUserPoolID(string) {}
|
||||
func (m *mockAuthConfig) GetAuthClientSecret() string { return "test-secret" }
|
||||
func (m *mockAuthConfig) SetAuthClientSecret(string) {}
|
||||
func (m *mockAuthConfig) GetAuthDomain() string { return "test-domain" }
|
||||
func (m *mockAuthConfig) SetAuthDomain(string) {}
|
||||
func (m *mockAuthConfig) GetAuthClientID() string { return "test-client-id" }
|
||||
func (m *mockAuthConfig) SetAuthClientID(string) {}
|
||||
func (m *mockAuthConfig) GetAuthRedirectURI() string { return "http://localhost/callback" }
|
||||
func (m *mockAuthConfig) SetAuthRedirectURI(string) {}
|
||||
func (m *mockAuthConfig) GetAuthTokenURL() string { return "http://localhost/token" }
|
||||
func (m *mockAuthConfig) SetAuthTokenURL(string) {}
|
||||
func (m *mockAuthConfig) GetAuthJwksURL() string { return "http://localhost/jwks" }
|
||||
func (m *mockAuthConfig) SetAuthJwksURL(string) {}
|
||||
func (m *mockAuthConfig) GetAuthURL() string { return "http://localhost/auth" }
|
||||
func (m *mockAuthConfig) SetAuthURL(string) {}
|
||||
func (m *mockAuthConfig) GetAuthLogoutURL() string { return "http://localhost/logout" }
|
||||
func (m *mockAuthConfig) SetAuthLogoutURL(string) {}
|
||||
func (m *mockAuthConfig) GetAuthLoginPath() string { return "/login" }
|
||||
func (m *mockAuthConfig) SetAuthLoginPath(string) {}
|
||||
func (m *mockAuthConfig) GetAuthCallbackPath() string { return "/callback" }
|
||||
func (m *mockAuthConfig) SetAuthCallbackPath(string) {}
|
||||
func (m *mockAuthConfig) GetAuthHomePath() string { return "/home" }
|
||||
func (m *mockAuthConfig) SetAuthHomePath(string) {}
|
||||
func (m *mockAuthConfig) GetAuthLogoutPath() string { return "/logout" }
|
||||
func (m *mockAuthConfig) SetAuthLogoutPath(string) {}
|
||||
func (m *mockAuthConfig) GetHealthPath() string { return "/health" }
|
||||
func (m *mockAuthConfig) SetHealthPath(string) {}
|
||||
func (m *mockAuthConfig) GetAuthLogger() *slog.Logger { return slog.Default() }
|
||||
func (m *mockAuthConfig) SetAuthLogger(*slog.Logger) {}
|
||||
func (m *mockAuthConfig) GetAuthRoutePermissions() map[string][]string { return nil }
|
||||
func (m *mockAuthConfig) SetAuthRoutePermissions(map[string][]string) {}
|
||||
func (m *mockAuthConfig) GetPermitIOAPIKey() string { return "test-permit-key" }
|
||||
func (m *mockAuthConfig) SetPermitIOAPIKey(string) {}
|
||||
func (m *mockAuthConfig) GetPermitIOPDPURL() string { return "http://localhost:7766" }
|
||||
func (m *mockAuthConfig) SetPermitIOPDPURL(string) {}
|
||||
func (m *mockAuthConfig) InitializeAuthConfig(string, *slog.Logger) error { return nil }
|
||||
func (m *mockAuthConfig) GetNoJWTValidation() bool { return false }
|
||||
func (m *mockAuthConfig) SetNoJWTValidation(bool) {}
|
||||
|
||||
// NewTestControllers creates a Controllers instance for testing with a mock auth config
|
||||
func NewTestControllers(services *Services) *Controllers {
|
||||
return NewControllers(services, &mockAuthConfig{})
|
||||
}
|
||||
Reference in New Issue
Block a user