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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {}
|
||||
|
||||
Reference in New Issue
Block a user