Merged in feature/cognito-shared-environments (pull request #211)

cognito and permit shared prod environment support

* code complete

* user creattion tool

test harness
This commit is contained in:
Jay Brown
2026-02-26 12:33:35 +00:00
parent 6dccf494f8
commit c668485e6f
55 changed files with 3721 additions and 381 deletions
+50 -6
View File
@@ -115,9 +115,10 @@ func (ctrl *Controllers) CreateAdminUser(ctx echo.Context) error {
// Prepare user attributes
userAttrs := usermanagement.UserAttributes{
Email: string(req.Email),
FirstName: req.FirstName,
LastName: req.LastName,
Email: string(req.Email),
FirstName: req.FirstName,
LastName: req.LastName,
EnvironmentID: ctrl.getCognitoEnvironmentID(),
}
// Check if user already exists
@@ -347,6 +348,13 @@ func (ctrl *Controllers) GetAdminUser(ctx echo.Context, email UserEmail) error {
})
}
// Check environment ownership - return 404 for users in other environments
if err := checkUserEnvironmentOwnership(cognitoUser, ctrl.getCognitoEnvironmentID()); err != nil {
return ctx.JSON(http.StatusNotFound, ErrorMessage{
Message: fmt.Sprintf("User not found: %s", email),
})
}
// Get user roles from Permit.io
httpClient := &http.Client{Timeout: 10 * time.Second}
permitConfig := ctrl.getPermitConfig()
@@ -401,7 +409,7 @@ func (ctrl *Controllers) CheckAdminUserExists(ctx echo.Context, email UserEmail)
}
// Check if user exists in Cognito (use GetCognitoUser to get detailed error info)
_, err = usermanagement.GetCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
cognitoUser, err := usermanagement.GetCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
if err != nil {
errInfo := usermanagement.ClassifyCognitoError(err, string(email))
// For HEAD requests, we can only return status code and headers
@@ -410,6 +418,11 @@ func (ctrl *Controllers) CheckAdminUserExists(ctx echo.Context, email UserEmail)
return ctx.NoContent(errInfo.HTTPStatus)
}
// Check environment ownership - return 404 for users in other environments
if err := checkUserEnvironmentOwnership(cognitoUser, ctrl.getCognitoEnvironmentID()); err != nil {
return ctx.NoContent(http.StatusNotFound)
}
return ctx.NoContent(http.StatusOK)
}
@@ -461,9 +474,12 @@ func (ctrl *Controllers) ListAdminUsers(ctx echo.Context, params ListAdminUsersP
})
}
// Apply environment filtering when COGNITO_ENVIRONMENT_ID is configured
filteredUsers := usermanagement.FilterUsersByEnvironmentID(result.Users, ctrl.getCognitoEnvironmentID())
// Convert to AdminUserDetails format
users := make([]AdminUserDetails, 0, len(result.Users))
for _, cognitoUser := range result.Users {
users := make([]AdminUserDetails, 0, len(filteredUsers))
for _, cognitoUser := range filteredUsers {
userDetail := AdminUserDetails{
CognitoSubjectId: cognitoUser.SubjectID,
Email: types.Email(cognitoUser.Email),
@@ -711,6 +727,13 @@ func (ctrl *Controllers) UpdateAdminUser(ctx echo.Context, email UserEmail) erro
})
}
// Check environment ownership - return 404 for users in other environments
if err := checkUserEnvironmentOwnership(cognitoUser, ctrl.getCognitoEnvironmentID()); err != nil {
return ctx.JSON(http.StatusNotFound, ErrorMessage{
Message: fmt.Sprintf("User not found: %s", email),
})
}
// Update user attributes in Cognito if provided
err = ctrl.updateUserAttributesInCognito(ctx, cognitoClient, cognitoConfig, email, req, adminUser, cognitoUser)
if err != nil {
@@ -818,6 +841,13 @@ func (ctrl *Controllers) DeleteAdminUser(ctx echo.Context, email UserEmail, para
})
}
// Check environment ownership - return 404 for users in other environments
if err := checkUserEnvironmentOwnership(cognitoUser, ctrl.getCognitoEnvironmentID()); err != nil {
return ctx.JSON(http.StatusNotFound, ErrorMessage{
Message: fmt.Sprintf("User not found: %s", email),
})
}
// Delete user from Cognito
cognitoDeleted := false
err = usermanagement.DeleteCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
@@ -952,6 +982,13 @@ func (ctrl *Controllers) DisableAdminUser(ctx echo.Context, email UserEmail) err
})
}
// Check environment ownership - return 404 for users in other environments
if err := checkUserEnvironmentOwnership(cognitoUser, ctrl.getCognitoEnvironmentID()); err != nil {
return ctx.JSON(http.StatusNotFound, ErrorMessage{
Message: fmt.Sprintf("User not found: %s", email),
})
}
// Disable user in Cognito
err = usermanagement.DisableCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
if err != nil {
@@ -1056,6 +1093,13 @@ func (ctrl *Controllers) EnableAdminUser(ctx echo.Context, email UserEmail) erro
})
}
// Check environment ownership - return 404 for users in other environments
if err := checkUserEnvironmentOwnership(cognitoUser, ctrl.getCognitoEnvironmentID()); err != nil {
return ctx.JSON(http.StatusNotFound, ErrorMessage{
Message: fmt.Sprintf("User not found: %s", email),
})
}
// Enable user in Cognito
err = usermanagement.EnableCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
if err != nil {
+36
View File
@@ -0,0 +1,36 @@
// adminHandlers_envfilter.go provides environment isolation helpers for admin user handlers.
// These functions ensure that admin operations are scoped to the server's configured
// COGNITO_ENVIRONMENT_ID, preventing cross-environment user visibility.
package queryapi
import (
"fmt"
"queryorchestration/internal/usermanagement"
)
// getCognitoEnvironmentID returns the configured Cognito environment ID from the
// service configuration. Returns an empty string when no environment is configured,
// which disables environment filtering (backward compatible).
func (ctrl *Controllers) getCognitoEnvironmentID() string {
return ctrl.cfg.GetCognitoEnvironmentID()
}
// checkUserEnvironmentOwnership verifies that the given user belongs to the specified
// server environment. Returns nil if the user is allowed (same environment, untagged
// legacy user, or no server environment configured). Returns an error if the user
// belongs to a different environment.
//
// Parameters:
// - user: The Cognito user to check
// - serverEnvID: The server's configured environment ID (empty means no filtering)
//
// Returns:
// - nil if the user passes the environment check
// - an error if the user belongs to a different environment
func checkUserEnvironmentOwnership(user *usermanagement.CognitoUserResponse, serverEnvID string) error {
if usermanagement.UserBelongsToEnvironment(user, serverEnvID) {
return nil
}
return fmt.Errorf("user %s does not belong to environment %s", user.Email, serverEnvID)
}
@@ -0,0 +1,166 @@
package queryapi
import (
"testing"
"queryorchestration/internal/usermanagement"
"github.com/stretchr/testify/assert"
)
// TestGetCognitoEnvironmentID_ReturnsConfigValue verifies that the getCognitoEnvironmentID
// helper method returns the value from the config provider.
func TestGetCognitoEnvironmentID_ReturnsConfigValue(t *testing.T) {
cfg := &mockAuthConfig{}
svc := &Services{}
ctrl := NewControllers(svc, cfg)
// Default mockAuthConfig returns empty string
assert.Equal(t, "", ctrl.getCognitoEnvironmentID(),
"should return empty when config has no environment ID set")
}
// TestGetCognitoEnvironmentID_WithEnvSet verifies getCognitoEnvironmentID returns a
// non-empty value when the config has one set.
func TestGetCognitoEnvironmentID_WithEnvSet(t *testing.T) {
cfg := &envAwareMockConfig{envID: "acmehealth"}
svc := &Services{}
ctrl := NewControllers(svc, cfg)
assert.Equal(t, "acmehealth", ctrl.getCognitoEnvironmentID(),
"should return the configured environment ID")
}
// TestGetCognitoUserWithEnvCheck_UserInSameEnv verifies that getCognitoUserWithEnvCheck
// returns the user when the user's environment matches the server's.
func TestGetCognitoUserWithEnvCheck_UserInSameEnv(t *testing.T) {
user := &usermanagement.CognitoUserResponse{
SubjectID: "sub-1",
Email: "user@acme.com",
EnvironmentID: "acmehealth",
}
err := checkUserEnvironmentOwnership(user, "acmehealth")
assert.NoError(t, err, "user in same environment should pass check")
}
// TestGetCognitoUserWithEnvCheck_UntaggedUser verifies that getCognitoUserWithEnvCheck
// returns the user when the user has no environment tag (backward compat).
func TestGetCognitoUserWithEnvCheck_UntaggedUser(t *testing.T) {
user := &usermanagement.CognitoUserResponse{
SubjectID: "sub-2",
Email: "legacy@acme.com",
EnvironmentID: "",
}
err := checkUserEnvironmentOwnership(user, "acmehealth")
assert.NoError(t, err, "untagged user should pass check (backward compat)")
}
// TestGetCognitoUserWithEnvCheck_UserInDifferentEnv verifies that getCognitoUserWithEnvCheck
// returns an error when the user's environment does not match the server's.
func TestGetCognitoUserWithEnvCheck_UserInDifferentEnv(t *testing.T) {
user := &usermanagement.CognitoUserResponse{
SubjectID: "sub-3",
Email: "user@ford.com",
EnvironmentID: "fordmotorco",
}
err := checkUserEnvironmentOwnership(user, "acmehealth")
assert.Error(t, err, "user in different environment should fail check")
}
// TestGetCognitoUserWithEnvCheck_NoServerEnvSet verifies that getCognitoUserWithEnvCheck
// allows all users through when the server has no environment ID configured.
func TestGetCognitoUserWithEnvCheck_NoServerEnvSet(t *testing.T) {
user := &usermanagement.CognitoUserResponse{
SubjectID: "sub-4",
Email: "user@ford.com",
EnvironmentID: "fordmotorco",
}
err := checkUserEnvironmentOwnership(user, "")
assert.NoError(t, err, "all users should pass when server has no environment ID")
}
// TestListAdminUsers_FiltersByEnvironment verifies that ListAdminUsers applies environment
// filtering to the user list when COGNITO_ENVIRONMENT_ID is set. This test verifies the
// filtering logic at the usermanagement layer since the admin handler delegates to it.
func TestListAdminUsers_FiltersByEnvironment(t *testing.T) {
users := []usermanagement.CognitoUserResponse{
{SubjectID: "sub-1", Email: "acme1@test.com", EnvironmentID: "acmehealth"},
{SubjectID: "sub-2", Email: "ford1@test.com", EnvironmentID: "fordmotorco"},
{SubjectID: "sub-3", Email: "legacy@test.com", EnvironmentID: ""},
{SubjectID: "sub-4", Email: "acme2@test.com", EnvironmentID: "acmehealth"},
}
filtered := usermanagement.FilterUsersByEnvironmentID(users, "acmehealth")
assert.Len(t, filtered, 3, "should include matching + untagged users")
for _, u := range filtered {
assert.True(t, u.EnvironmentID == "acmehealth" || u.EnvironmentID == "",
"filtered user should be matching or untagged, got %q", u.EnvironmentID)
}
}
// TestListAdminUsers_NoFilterWhenEnvNotSet verifies that ListAdminUsers returns all users
// when COGNITO_ENVIRONMENT_ID is not set (backward compat).
func TestListAdminUsers_NoFilterWhenEnvNotSet(t *testing.T) {
users := []usermanagement.CognitoUserResponse{
{SubjectID: "sub-1", Email: "acme1@test.com", EnvironmentID: "acmehealth"},
{SubjectID: "sub-2", Email: "ford1@test.com", EnvironmentID: "fordmotorco"},
{SubjectID: "sub-3", Email: "legacy@test.com", EnvironmentID: ""},
}
filtered := usermanagement.FilterUsersByEnvironmentID(users, "")
assert.Len(t, filtered, 3, "all users should be returned when env is not set")
}
// TestCreateAdminUser_SetsEnvironmentID verifies that when COGNITO_ENVIRONMENT_ID is set,
// the CreateAdminUser handler passes it through to the user attributes.
func TestCreateAdminUser_SetsEnvironmentID(t *testing.T) {
cfg := &envAwareMockConfig{envID: "acmehealth"}
attrs := usermanagement.UserAttributes{
Email: "new@acme.com",
FirstName: "New",
LastName: "User",
EnvironmentID: cfg.GetCognitoEnvironmentID(),
}
assert.Equal(t, "acmehealth", attrs.EnvironmentID,
"EnvironmentID should be set from config")
}
// TestCreateAdminUser_NoEnvironmentIDWhenNotSet verifies that when COGNITO_ENVIRONMENT_ID
// is not set, the CreateAdminUser handler does not set an environment ID.
func TestCreateAdminUser_NoEnvironmentIDWhenNotSet(t *testing.T) {
cfg := &mockAuthConfig{}
attrs := usermanagement.UserAttributes{
Email: "new@acme.com",
FirstName: "New",
LastName: "User",
EnvironmentID: cfg.GetCognitoEnvironmentID(),
}
assert.Empty(t, attrs.EnvironmentID,
"EnvironmentID should be empty when config has none set")
}
// envAwareMockConfig extends mockAuthConfig to support a configurable CognitoEnvironmentID.
type envAwareMockConfig struct {
mockAuthConfig
envID string
}
// GetCognitoEnvironmentID returns the configured environment ID.
func (m *envAwareMockConfig) GetCognitoEnvironmentID() string {
return m.envID
}
// SetCognitoEnvironmentID sets the environment ID.
func (m *envAwareMockConfig) SetCognitoEnvironmentID(id string) {
m.envID = id
}
+2
View File
@@ -51,6 +51,8 @@ func (m *mockAuthConfig) GetPermitIOAPIKey() string { retu
func (m *mockAuthConfig) SetPermitIOAPIKey(string) {}
func (m *mockAuthConfig) GetPermitIOPDPURL() string { return "http://localhost:7766" }
func (m *mockAuthConfig) SetPermitIOPDPURL(string) {}
func (m *mockAuthConfig) GetCognitoEnvironmentID() string { return "" }
func (m *mockAuthConfig) SetCognitoEnvironmentID(string) {}
func (m *mockAuthConfig) InitializeAuthConfig(string, *slog.Logger) error { return nil }
func (m *mockAuthConfig) GetNoJWTValidation() bool { return false }
func (m *mockAuthConfig) SetNoJWTValidation(bool) {}