Merged in feature/demo-support (pull request #207)

Feature/demo support

* index logs now debug

* demo data loader
This commit is contained in:
Jay Brown
2026-02-02 19:08:08 +00:00
parent 63c12a2f44
commit 963ccc6553
15 changed files with 2240 additions and 314 deletions
-20
View File
@@ -1,26 +1,6 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## General rules DO NOT VIOLATE
NEVER code additional features that I did not explicitly ask for without asking permission.
NEVER create mock data or simplified components unless explicitly told to do so
NEVER replace existing complex components with simplified versions - always fix the actual problem or the feature that was requested.
Never just bypass tests that have regressed. Stop and find out why they are failing and fix them.
ALWAYS work with the existing codebase - do not create new simplified alternatives
ALWAYS find and fix the root cause of issues instead of creating workarounds
When debugging issues, focus on fixing the existing implementation, not replacing it
When something doesn't work, debug and fix it - don't start over with a simple version
Dont build things I am not specifically asking for unless its required to build the thing I am asking for.
When asked to make unit tests avoid mocks and use the actual thing that is being tested when reasonable.
For commands that read such as grep or cat there is no need to ask for permission.
Do not assume that we need backward compatibilty when implementing new features. If there is a decision to make that will affect this ask. Do not guess.
## IDE related
Always use ide diagnostics to validate code changes when running with ide integration (goland, vscode)
## linting all edits after every change
Always run `task lint` once you think you are done with edits to verify your changes have not caused other issues.
+68 -25
View File
@@ -321,7 +321,12 @@ func (ctrl *Controllers) GetAdminUser(ctx echo.Context, email UserEmail) error {
// Get user from Cognito
cognitoUser, err := usermanagement.GetCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to get user from Cognito", "error", err, "email", email)
errInfo := usermanagement.ClassifyCognitoError(err, string(email))
ctrl.cfg.GetAuthLogger().Error("Failed to get user from Cognito",
"error", err,
"email", email,
"error_type", errInfo.ErrorType,
"http_status", errInfo.HTTPStatus)
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionUpdate,
@@ -330,12 +335,15 @@ func (ctrl *Controllers) GetAdminUser(ctx echo.Context, email UserEmail) error {
string(email),
"",
usermanagement.ResultFailure,
fmt.Sprintf("User not found in Cognito: %v", err),
fmt.Sprintf("Cognito error (%s): %v", errInfo.ErrorType, err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
return ctx.JSON(http.StatusNotFound, ErrorMessage{
Message: fmt.Sprintf("User not found: %s", email),
errorType := string(errInfo.ErrorType)
return ctx.JSON(errInfo.HTTPStatus, ErrorMessage{
Message: errInfo.Message,
ErrorType: &errorType,
Details: &errInfo.Details,
})
}
@@ -392,11 +400,14 @@ func (ctrl *Controllers) CheckAdminUserExists(ctx echo.Context, email UserEmail)
Region: ctrl.cfg.GetAuthRegion(),
}
// Check if user exists in Cognito
exists := usermanagement.UserExistsInCognito(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
if !exists {
return ctx.NoContent(http.StatusNotFound)
// Check if user exists in Cognito (use GetCognitoUser to get detailed error info)
_, 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
ctx.Response().Header().Set("X-Error-Type", string(errInfo.ErrorType))
ctx.Response().Header().Set("X-Error-Message", errInfo.Message)
return ctx.NoContent(errInfo.HTTPStatus)
}
return ctx.NoContent(http.StatusOK)
@@ -674,7 +685,12 @@ func (ctrl *Controllers) UpdateAdminUser(ctx echo.Context, email UserEmail) erro
// Get user from Cognito to verify existence and get Subject ID
cognitoUser, err := usermanagement.GetCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to get user from Cognito", "error", err, "email", email)
errInfo := usermanagement.ClassifyCognitoError(err, string(email))
ctrl.cfg.GetAuthLogger().Error("Failed to get user from Cognito",
"error", err,
"email", email,
"error_type", errInfo.ErrorType,
"http_status", errInfo.HTTPStatus)
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionUpdate,
@@ -683,12 +699,15 @@ func (ctrl *Controllers) UpdateAdminUser(ctx echo.Context, email UserEmail) erro
string(email),
"",
usermanagement.ResultFailure,
fmt.Sprintf("User not found in Cognito: %v", err),
fmt.Sprintf("Cognito error (%s): %v", errInfo.ErrorType, err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
return ctx.JSON(http.StatusNotFound, ErrorMessage{
Message: fmt.Sprintf("User not found: %s", email),
errorType := string(errInfo.ErrorType)
return ctx.JSON(errInfo.HTTPStatus, ErrorMessage{
Message: errInfo.Message,
ErrorType: &errorType,
Details: &errInfo.Details,
})
}
@@ -773,7 +792,12 @@ func (ctrl *Controllers) DeleteAdminUser(ctx echo.Context, email UserEmail, para
// Get user from Cognito to verify existence and get Subject ID
cognitoUser, err := usermanagement.GetCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to get user from Cognito", "error", err, "email", email)
errInfo := usermanagement.ClassifyCognitoError(err, string(email))
ctrl.cfg.GetAuthLogger().Error("Failed to get user from Cognito",
"error", err,
"email", email,
"error_type", errInfo.ErrorType,
"http_status", errInfo.HTTPStatus)
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionDelete,
@@ -782,12 +806,15 @@ func (ctrl *Controllers) DeleteAdminUser(ctx echo.Context, email UserEmail, para
string(email),
"",
usermanagement.ResultFailure,
fmt.Sprintf("User not found in Cognito: %v", err),
fmt.Sprintf("Cognito error (%s): %v", errInfo.ErrorType, err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
return ctx.JSON(http.StatusNotFound, ErrorMessage{
Message: fmt.Sprintf("User not found: %s", email),
errorType := string(errInfo.ErrorType)
return ctx.JSON(errInfo.HTTPStatus, ErrorMessage{
Message: errInfo.Message,
ErrorType: &errorType,
Details: &errInfo.Details,
})
}
@@ -899,7 +926,12 @@ func (ctrl *Controllers) DisableAdminUser(ctx echo.Context, email UserEmail) err
// Get user from Cognito to verify existence and get Subject ID
cognitoUser, err := usermanagement.GetCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to get user from Cognito", "error", err, "email", email)
errInfo := usermanagement.ClassifyCognitoError(err, string(email))
ctrl.cfg.GetAuthLogger().Error("Failed to get user from Cognito",
"error", err,
"email", email,
"error_type", errInfo.ErrorType,
"http_status", errInfo.HTTPStatus)
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionDisable,
@@ -908,12 +940,15 @@ func (ctrl *Controllers) DisableAdminUser(ctx echo.Context, email UserEmail) err
string(email),
"",
usermanagement.ResultFailure,
fmt.Sprintf("User not found in Cognito: %v", err),
fmt.Sprintf("Cognito error (%s): %v", errInfo.ErrorType, err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
return ctx.JSON(http.StatusNotFound, ErrorMessage{
Message: fmt.Sprintf("User not found: %s", email),
errorType := string(errInfo.ErrorType)
return ctx.JSON(errInfo.HTTPStatus, ErrorMessage{
Message: errInfo.Message,
ErrorType: &errorType,
Details: &errInfo.Details,
})
}
@@ -995,7 +1030,12 @@ func (ctrl *Controllers) EnableAdminUser(ctx echo.Context, email UserEmail) erro
// Get user from Cognito to verify existence and get Subject ID
cognitoUser, err := usermanagement.GetCognitoUser(context.Background(), cognitoClient, cognitoConfig.UserPoolID, string(email))
if err != nil {
ctrl.cfg.GetAuthLogger().Error("Failed to get user from Cognito", "error", err, "email", email)
errInfo := usermanagement.ClassifyCognitoError(err, string(email))
ctrl.cfg.GetAuthLogger().Error("Failed to get user from Cognito",
"error", err,
"email", email,
"error_type", errInfo.ErrorType,
"http_status", errInfo.HTTPStatus)
usermanagement.LogUserAdminAction(
ctrl.cfg.GetAuthLogger(),
usermanagement.ActionEnable,
@@ -1004,12 +1044,15 @@ func (ctrl *Controllers) EnableAdminUser(ctx echo.Context, email UserEmail) erro
string(email),
"",
usermanagement.ResultFailure,
fmt.Sprintf("User not found in Cognito: %v", err),
fmt.Sprintf("Cognito error (%s): %v", errInfo.ErrorType, err),
ctx.Response().Header().Get(echo.HeaderXRequestID),
)
return ctx.JSON(http.StatusNotFound, ErrorMessage{
Message: fmt.Sprintf("User not found: %s", email),
errorType := string(errInfo.ErrorType)
return ctx.JSON(errInfo.HTTPStatus, ErrorMessage{
Message: errInfo.Message,
ErrorType: &errorType,
Details: &errInfo.Details,
})
}
+275 -266
View File
@@ -623,6 +623,12 @@ type DocumentSummary struct {
// ErrorMessage Description of error
type ErrorMessage struct {
// Details Additional error details for debugging (sanitized).
Details *string `json:"details,omitempty"`
// ErrorType Machine-readable error classification for programmatic handling.
ErrorType *string `json:"error_type,omitempty"`
// Message Message describing the cause.
Message string `json:"message"`
}
@@ -2669,272 +2675,275 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"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",
"/wkAAP//LHP6SYqxAQA=",
}
// GetSwagger returns the content of the embedded swagger specification file
+6 -1
View File
@@ -32,9 +32,14 @@ they conflict with some of the names we use for our roles.
```bash
export PERMIT_API_KEY="permit_key_omitted-here"
go run setup_permit.go -list
go run setup_permit.go -config permit_policies.yaml -project default -env dev
go run setup_permit.go -config permit_policies.yaml -project default -env <id of the env>
```
Note: When you do the list operation you will get the id of the target environment to put in the -env parameter.
### Environments in permit
You must create a new enviroment in permit.io manually at this time. This tools just sets up the resources and policies for an existing enviromment. We will have to extend this tool later to create the environment if it does not already exist once we are creating a lot of enviroments.
### Dry Run Mode
To see what would be created without making actual changes:
@@ -50,6 +50,19 @@ resources:
- get
- post
- name: field-extractions
description: Field extraction data for documents
actions:
- get
- post
- name: folders
description: Folder management for document organization
actions:
- get
- post
- patch
- name: home
description: Home page access
actions:
@@ -87,6 +100,13 @@ roles:
export:
- get
- post
field-extractions:
- get
- post
folders:
- get
- post
- patch
home:
- get
@@ -99,6 +119,12 @@ roles:
- patch
- delete
- head
field-extractions:
- get
- post
folders:
- get
- post
home:
- get
@@ -118,6 +144,10 @@ roles:
- get
export:
- get
field-extractions:
- get
folders:
- get
home:
- get
@@ -134,6 +164,13 @@ roles:
export:
- get
- post
field-extractions:
- get
- post
folders:
- get
- post
- patch
home:
- get
notes: |
@@ -221,6 +258,18 @@ policy_mappings:
methods:
GET: export:get
field_extraction_endpoints:
- path: /field-extractions
methods:
GET: field-extractions:get
POST: field-extractions:post
- path: /field-extractions/version
methods:
GET: field-extractions:get
- path: /field-extractions/history
methods:
GET: field-extractions:get
auth_endpoints:
- path: /home
methods:
@@ -253,6 +302,8 @@ dynamic_mapping:
resource: document
- url: /admin/users
resource: admin
- url: /field-extractions
resource: field-extractions
action_extraction:
logic: HTTP method converted to lowercase
+1 -1
View File
@@ -146,7 +146,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.17
github.com/aws/smithy-go v1.22.3 // indirect
github.com/aws/smithy-go v1.22.3
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
@@ -0,0 +1,4 @@
-- Revert: make effectiveDate required again
-- First set any NULL values to createdAt as a fallback
UPDATE eulaVersions SET effectiveDate = createdAt WHERE effectiveDate IS NULL;
ALTER TABLE eulaVersions ALTER COLUMN effectiveDate SET NOT NULL;
@@ -0,0 +1,3 @@
-- Make effectiveDate nullable in eulaVersions table
-- This allows EULA versions to be created without specifying an effective date
ALTER TABLE eulaVersions ALTER COLUMN effectiveDate DROP NOT NULL;
+14 -1
View File
@@ -9,6 +9,7 @@ import (
"net/http"
"os"
"strconv"
"strings"
"testing"
"time"
@@ -162,6 +163,9 @@ func getDebugMiddleware(logger *slog.Logger) echo.MiddlewareFunc {
}
}
// getSlogCustomEchoMiddleware returns a middleware that logs HTTP requests.
// Noisy endpoints (swagger, metrics, health) are logged at DEBUG level to reduce log volume.
// All other endpoints are logged at INFO level.
func getSlogCustomEchoMiddleware(logger *slog.Logger) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
@@ -175,7 +179,16 @@ func getSlogCustomEchoMiddleware(logger *slog.Logger) echo.MiddlewareFunc {
req := c.Request()
res := c.Response()
logger.Info("HTTP request",
// Use Debug level for noisy endpoints (swagger, metrics, health)
path := req.RequestURI
logLevel := slog.LevelInfo
if strings.HasPrefix(path, "/swagger") ||
strings.HasPrefix(path, "/metrics") ||
path == "/health" {
logLevel = slog.LevelDebug
}
logger.Log(req.Context(), logLevel, "HTTP request",
"method", req.Method,
"uri", req.RequestURI,
"status", res.Status,
+312
View File
@@ -0,0 +1,312 @@
// errors.go provides error classification for AWS Cognito errors.
// It maps AWS error types to appropriate HTTP status codes and sanitized error messages
// suitable for returning in API responses without leaking sensitive information.
package usermanagement
import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider/types"
"github.com/aws/smithy-go"
)
// CognitoErrorType represents a machine-readable error classification.
type CognitoErrorType string
const (
// ErrTypeUserNotFound indicates the user does not exist in Cognito.
ErrTypeUserNotFound CognitoErrorType = "UserNotFound"
// ErrTypeAccessDenied indicates the caller does not have permission to perform the operation.
ErrTypeAccessDenied CognitoErrorType = "AccessDenied"
// ErrTypeNotAuthorized indicates the caller is not authorized (e.g., invalid credentials).
ErrTypeNotAuthorized CognitoErrorType = "NotAuthorized"
// ErrTypeInvalidParameter indicates an invalid parameter was provided.
ErrTypeInvalidParameter CognitoErrorType = "InvalidParameter"
// ErrTypeRateLimited indicates too many requests were made.
ErrTypeRateLimited CognitoErrorType = "RateLimited"
// ErrTypeServiceUnavailable indicates the Cognito service is temporarily unavailable.
ErrTypeServiceUnavailable CognitoErrorType = "ServiceUnavailable"
// ErrTypeServiceError indicates a general error communicating with Cognito.
ErrTypeServiceError CognitoErrorType = "ServiceError"
// ErrTypeUserExists indicates the user already exists in Cognito.
ErrTypeUserExists CognitoErrorType = "UserExists"
)
// CognitoErrorInfo contains classified error information suitable for HTTP responses.
// The Message and Details fields are sanitized to avoid leaking sensitive information
// like ARNs, internal paths, or request IDs.
type CognitoErrorInfo struct {
// HTTPStatus is the appropriate HTTP status code for this error type.
HTTPStatus int
// ErrorType is a machine-readable error classification.
ErrorType CognitoErrorType
// Message is a user-friendly error message safe for HTTP responses.
Message string
// Details provides additional error context (sanitized).
Details string
}
// ClassifyCognitoError analyzes a Cognito error and returns appropriate HTTP response info.
// The returned CognitoErrorInfo contains sanitized messages suitable for API responses.
//
// Parameters:
// - err: The error returned from a Cognito operation
// - email: The user's email address for context in error messages
//
// Returns:
// - CognitoErrorInfo: Classified error information with appropriate HTTP status and messages
func ClassifyCognitoError(err error, email string) CognitoErrorInfo {
if err == nil {
return CognitoErrorInfo{
HTTPStatus: http.StatusOK,
ErrorType: "",
Message: "",
Details: "",
}
}
// Check for specific Cognito SDK error types
var userNotFoundErr *types.UserNotFoundException
if errors.As(err, &userNotFoundErr) {
return CognitoErrorInfo{
HTTPStatus: http.StatusNotFound,
ErrorType: ErrTypeUserNotFound,
Message: fmt.Sprintf("User not found: %s", email),
Details: "The specified user does not exist in Cognito",
}
}
var notAuthorizedErr *types.NotAuthorizedException
if errors.As(err, &notAuthorizedErr) {
return CognitoErrorInfo{
HTTPStatus: http.StatusUnauthorized,
ErrorType: ErrTypeNotAuthorized,
Message: "Not authorized to perform this operation",
Details: sanitizeErrorMessage(err.Error()),
}
}
var invalidParamErr *types.InvalidParameterException
if errors.As(err, &invalidParamErr) {
return CognitoErrorInfo{
HTTPStatus: http.StatusBadRequest,
ErrorType: ErrTypeInvalidParameter,
Message: "Invalid parameter in request",
Details: sanitizeErrorMessage(err.Error()),
}
}
var tooManyRequestsErr *types.TooManyRequestsException
if errors.As(err, &tooManyRequestsErr) {
return CognitoErrorInfo{
HTTPStatus: http.StatusTooManyRequests,
ErrorType: ErrTypeRateLimited,
Message: "Too many requests to Cognito",
Details: "Rate limit exceeded. Please retry after a short delay.",
}
}
var internalErr *types.InternalErrorException
if errors.As(err, &internalErr) {
return CognitoErrorInfo{
HTTPStatus: http.StatusServiceUnavailable,
ErrorType: ErrTypeServiceUnavailable,
Message: "Cognito service temporarily unavailable",
Details: "Internal service error. Please retry later.",
}
}
var usernameExistsErr *types.UsernameExistsException
if errors.As(err, &usernameExistsErr) {
return CognitoErrorInfo{
HTTPStatus: http.StatusConflict,
ErrorType: ErrTypeUserExists,
Message: fmt.Sprintf("User already exists: %s", email),
Details: "A user with this email already exists in Cognito",
}
}
// Check for generic AWS API errors (including AccessDeniedException)
var apiErr smithy.APIError
if errors.As(err, &apiErr) {
return classifyAPIError(apiErr, email)
}
// Default: treat as a generic service communication error
return CognitoErrorInfo{
HTTPStatus: http.StatusBadGateway,
ErrorType: ErrTypeServiceError,
Message: "Failed to communicate with Cognito",
Details: sanitizeErrorMessage(err.Error()),
}
}
// classifyAPIError handles generic AWS API errors that may not have specific SDK types.
//
// Parameters:
// - apiErr: The AWS API error
// - email: The user's email address for context in error messages
//
// Returns:
// - CognitoErrorInfo: Classified error information
func classifyAPIError(apiErr smithy.APIError, email string) CognitoErrorInfo {
code := apiErr.ErrorCode()
switch code {
case "AccessDeniedException":
return CognitoErrorInfo{
HTTPStatus: http.StatusForbidden,
ErrorType: ErrTypeAccessDenied,
Message: "Access denied to Cognito user pool",
Details: "The service does not have permission to perform this Cognito operation",
}
case "UserNotFoundException":
return CognitoErrorInfo{
HTTPStatus: http.StatusNotFound,
ErrorType: ErrTypeUserNotFound,
Message: fmt.Sprintf("User not found: %s", email),
Details: "The specified user does not exist in Cognito",
}
case "NotAuthorizedException":
return CognitoErrorInfo{
HTTPStatus: http.StatusUnauthorized,
ErrorType: ErrTypeNotAuthorized,
Message: "Not authorized to perform this operation",
Details: sanitizeErrorMessage(apiErr.ErrorMessage()),
}
case "InvalidParameterException":
return CognitoErrorInfo{
HTTPStatus: http.StatusBadRequest,
ErrorType: ErrTypeInvalidParameter,
Message: "Invalid parameter in request",
Details: sanitizeErrorMessage(apiErr.ErrorMessage()),
}
case "TooManyRequestsException":
return CognitoErrorInfo{
HTTPStatus: http.StatusTooManyRequests,
ErrorType: ErrTypeRateLimited,
Message: "Too many requests to Cognito",
Details: "Rate limit exceeded. Please retry after a short delay.",
}
case "ServiceUnavailable", "InternalError", "InternalErrorException":
return CognitoErrorInfo{
HTTPStatus: http.StatusServiceUnavailable,
ErrorType: ErrTypeServiceUnavailable,
Message: "Cognito service temporarily unavailable",
Details: "Internal service error. Please retry later.",
}
case "UsernameExistsException":
return CognitoErrorInfo{
HTTPStatus: http.StatusConflict,
ErrorType: ErrTypeUserExists,
Message: fmt.Sprintf("User already exists: %s", email),
Details: "A user with this email already exists in Cognito",
}
default:
return CognitoErrorInfo{
HTTPStatus: http.StatusBadGateway,
ErrorType: ErrTypeServiceError,
Message: "Failed to communicate with Cognito",
Details: sanitizeErrorMessage(apiErr.ErrorMessage()),
}
}
}
// sanitizeErrorMessage removes sensitive information from error messages.
// It strips out ARNs, request IDs, and internal resource names.
//
// Parameters:
// - msg: The raw error message
//
// Returns:
// - string: The sanitized error message
func sanitizeErrorMessage(msg string) string {
msg = removeARNs(msg)
msg = removeRequestIDs(msg)
msg = normalizeWhitespace(msg)
return msg
}
// removeARNs replaces all AWS ARNs with [REDACTED].
func removeARNs(msg string) string {
for {
start := strings.Index(msg, "arn:aws:")
if start == -1 {
return msg
}
end := findEndOfToken(msg, start)
msg = msg[:start] + "[REDACTED]" + msg[end:]
}
}
// removeRequestIDs removes request ID patterns and their values from the message.
func removeRequestIDs(msg string) string {
patterns := []string{"requestid:", "request-id:", "request id:"}
msgLower := strings.ToLower(msg)
for _, pattern := range patterns {
for {
idx := strings.Index(msgLower, pattern)
if idx == -1 {
break
}
end := idx + len(pattern)
end = skipWhitespace(msg, end)
end = findEndOfToken(msg, end)
msg = msg[:idx] + msg[end:]
msgLower = strings.ToLower(msg)
}
}
return msg
}
// findEndOfToken finds the end of a token (space, comma, or newline delimited).
func findEndOfToken(msg string, start int) int {
end := start
for end < len(msg) && !isTokenDelimiter(msg[end]) {
end++
}
return end
}
// isTokenDelimiter returns true if the character is a token delimiter.
func isTokenDelimiter(c byte) bool {
return c == ' ' || c == ',' || c == '\n' || c == '\r'
}
// skipWhitespace advances past whitespace characters.
func skipWhitespace(msg string, pos int) int {
for pos < len(msg) && (msg[pos] == ' ' || msg[pos] == '\t') {
pos++
}
return pos
}
// normalizeWhitespace trims and collapses multiple spaces.
func normalizeWhitespace(msg string) string {
msg = strings.TrimSpace(msg)
for strings.Contains(msg, " ") {
msg = strings.ReplaceAll(msg, " ", " ")
}
return msg
}
+240
View File
@@ -0,0 +1,240 @@
package usermanagement
import (
"errors"
"net/http"
"testing"
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider/types"
"github.com/aws/smithy-go"
"github.com/stretchr/testify/assert"
)
// mockAPIError implements smithy.APIError for testing.
type mockAPIError struct {
code string
message string
}
func (e *mockAPIError) Error() string { return e.message }
func (e *mockAPIError) ErrorCode() string { return e.code }
func (e *mockAPIError) ErrorMessage() string { return e.message }
func (e *mockAPIError) ErrorFault() smithy.ErrorFault { return smithy.FaultServer }
func TestClassifyCognitoError_NilError(t *testing.T) {
result := ClassifyCognitoError(nil, "test@example.com")
assert.Equal(t, http.StatusOK, result.HTTPStatus)
assert.Equal(t, CognitoErrorType(""), result.ErrorType)
assert.Empty(t, result.Message)
}
func TestClassifyCognitoError_UserNotFoundException(t *testing.T) {
email := "test@example.com"
err := &types.UserNotFoundException{
Message: stringPtr("User does not exist"),
}
result := ClassifyCognitoError(err, email)
assert.Equal(t, http.StatusNotFound, result.HTTPStatus)
assert.Equal(t, ErrTypeUserNotFound, result.ErrorType)
assert.Contains(t, result.Message, email)
assert.NotEmpty(t, result.Details)
}
func TestClassifyCognitoError_NotAuthorizedException(t *testing.T) {
email := "test@example.com"
err := &types.NotAuthorizedException{
Message: stringPtr("User is not authorized"),
}
result := ClassifyCognitoError(err, email)
assert.Equal(t, http.StatusUnauthorized, result.HTTPStatus)
assert.Equal(t, ErrTypeNotAuthorized, result.ErrorType)
assert.NotEmpty(t, result.Message)
}
func TestClassifyCognitoError_InvalidParameterException(t *testing.T) {
email := "test@example.com"
err := &types.InvalidParameterException{
Message: stringPtr("Invalid email format"),
}
result := ClassifyCognitoError(err, email)
assert.Equal(t, http.StatusBadRequest, result.HTTPStatus)
assert.Equal(t, ErrTypeInvalidParameter, result.ErrorType)
assert.NotEmpty(t, result.Message)
}
func TestClassifyCognitoError_TooManyRequestsException(t *testing.T) {
email := "test@example.com"
err := &types.TooManyRequestsException{
Message: stringPtr("Rate exceeded"),
}
result := ClassifyCognitoError(err, email)
assert.Equal(t, http.StatusTooManyRequests, result.HTTPStatus)
assert.Equal(t, ErrTypeRateLimited, result.ErrorType)
assert.NotEmpty(t, result.Message)
}
func TestClassifyCognitoError_InternalErrorException(t *testing.T) {
email := "test@example.com"
err := &types.InternalErrorException{
Message: stringPtr("Internal server error"),
}
result := ClassifyCognitoError(err, email)
assert.Equal(t, http.StatusServiceUnavailable, result.HTTPStatus)
assert.Equal(t, ErrTypeServiceUnavailable, result.ErrorType)
assert.NotEmpty(t, result.Message)
}
func TestClassifyCognitoError_UsernameExistsException(t *testing.T) {
email := "test@example.com"
err := &types.UsernameExistsException{
Message: stringPtr("User already exists"),
}
result := ClassifyCognitoError(err, email)
assert.Equal(t, http.StatusConflict, result.HTTPStatus)
assert.Equal(t, ErrTypeUserExists, result.ErrorType)
assert.Contains(t, result.Message, email)
}
func TestClassifyCognitoError_AccessDeniedException_ViaAPIError(t *testing.T) {
email := "test@example.com"
err := &mockAPIError{
code: "AccessDeniedException",
message: "User: arn:aws:sts::123456789:assumed-role/role is not authorized",
}
result := ClassifyCognitoError(err, email)
assert.Equal(t, http.StatusForbidden, result.HTTPStatus)
assert.Equal(t, ErrTypeAccessDenied, result.ErrorType)
assert.Contains(t, result.Message, "Access denied")
}
func TestClassifyCognitoError_GenericError(t *testing.T) {
email := "test@example.com"
err := errors.New("some unknown error")
result := ClassifyCognitoError(err, email)
assert.Equal(t, http.StatusBadGateway, result.HTTPStatus)
assert.Equal(t, ErrTypeServiceError, result.ErrorType)
assert.Contains(t, result.Message, "Failed to communicate with Cognito")
}
func TestClassifyCognitoError_UnknownAPIError(t *testing.T) {
email := "test@example.com"
err := &mockAPIError{
code: "SomeUnknownException",
message: "Unknown error occurred",
}
result := ClassifyCognitoError(err, email)
assert.Equal(t, http.StatusBadGateway, result.HTTPStatus)
assert.Equal(t, ErrTypeServiceError, result.ErrorType)
}
func TestSanitizeErrorMessage_RemovesARNs(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "removes single ARN",
input: "User: arn:aws:sts::123456789012:assumed-role/TestRole/session is not authorized",
expected: "User: [REDACTED] is not authorized",
},
{
name: "removes multiple ARNs",
input: "arn:aws:iam::123:user/test and arn:aws:s3:::bucket",
expected: "[REDACTED] and [REDACTED]",
},
{
name: "handles no ARN",
input: "Simple error message",
expected: "Simple error message",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := sanitizeErrorMessage(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestSanitizeErrorMessage_RemovesRequestIDs(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "removes RequestID",
input: "Error occurred, RequestID: abc-123-def",
expected: "Error occurred,",
},
{
name: "removes Request ID with space",
input: "Error occurred, Request ID: abc-123-def something",
expected: "Error occurred, something",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := sanitizeErrorMessage(tt.input)
assert.NotContains(t, result, "abc-123-def")
})
}
}
func TestClassifyAPIError_AllCases(t *testing.T) {
email := "user@test.com"
tests := []struct {
name string
code string
expectedStatus int
expectedType CognitoErrorType
}{
{"AccessDenied", "AccessDeniedException", http.StatusForbidden, ErrTypeAccessDenied},
{"UserNotFound", "UserNotFoundException", http.StatusNotFound, ErrTypeUserNotFound},
{"NotAuthorized", "NotAuthorizedException", http.StatusUnauthorized, ErrTypeNotAuthorized},
{"InvalidParameter", "InvalidParameterException", http.StatusBadRequest, ErrTypeInvalidParameter},
{"TooManyRequests", "TooManyRequestsException", http.StatusTooManyRequests, ErrTypeRateLimited},
{"ServiceUnavailable", "ServiceUnavailable", http.StatusServiceUnavailable, ErrTypeServiceUnavailable},
{"InternalError", "InternalError", http.StatusServiceUnavailable, ErrTypeServiceUnavailable},
{"UsernameExists", "UsernameExistsException", http.StatusConflict, ErrTypeUserExists},
{"Unknown", "SomeRandomException", http.StatusBadGateway, ErrTypeServiceError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
apiErr := &mockAPIError{code: tt.code, message: "test message"}
result := classifyAPIError(apiErr, email)
assert.Equal(t, tt.expectedStatus, result.HTTPStatus)
assert.Equal(t, tt.expectedType, result.ErrorType)
})
}
}
// stringPtr is a helper to create a pointer to a string.
func stringPtr(s string) *string {
return &s
}
+6
View File
@@ -621,6 +621,12 @@ type DocumentSummary struct {
// ErrorMessage Description of error
type ErrorMessage struct {
// Details Additional error details for debugging (sanitized).
Details *string `json:"details,omitempty"`
// ErrorType Machine-readable error classification for programmatic handling.
ErrorType *string `json:"error_type,omitempty"`
// Message Message describing the cause.
Message string `json:"message"`
}
+12
View File
@@ -3039,10 +3039,22 @@ components:
description: Message describing the cause.
maxLength: 256
example: "you must give me good information"
error_type:
type: string
description: Machine-readable error classification for programmatic handling.
maxLength: 64
example: "UserNotFound"
details:
type: string
description: Additional error details for debugging (sanitized).
maxLength: 512
example: "The specified user does not exist in Cognito"
required:
- message
example:
message: you must include a message
error_type: InvalidParameter
details: The email format is invalid
# Identity API schemas
IdentityRole:
+193
View File
@@ -0,0 +1,193 @@
# Demo Data Loader
A Python script that imports demo data from a CSV file into the Query Orchestration API. This tool is used to populate the system with sample documents and field extraction records for demos and testing.
## Overview
The script performs the following operations:
1. **Create Demo Client** - Creates a new client in the system to hold the demo data
2. **Upload Documents** - For each unique file in the CSV, creates and uploads a unique PDF document
3. **Create Field Extractions** - Imports the field extraction data from the CSV for each document
4. **Verify Records** - Retrieves and validates the created records match the input data
## Requirements
- Python 3.x (standard library only - no third-party packages required)
- Access to a running Query Orchestration API instance
## Usage
```bash
python3 demo_data_loader.py <csv_path> <base_url> [jwt_token]
python3 demo_data_loader.py ./SaaS_Demo_Data_Sam.csv http://localhost:8080
# example for uat system with token
python3 demo_data_loader.py ./SaaS_Demo_Data_Sam.csv http://queryo-query-wheg72fhas6h-1773391169.us-east-2.elb.amazonaws.com tokenfile=token.txt
```
### Arguments
| Argument | Required | Description |
| ----------- | -------- | --------------------------------------------------------- |
| `csv_path` | Yes | Path to the CSV file containing demo data |
| `base_url` | Yes | Base URL of the API (e.g., `http://localhost:8080`) |
| `jwt_token` | No | JWT token for authentication (placeholder for future use) |
### Examples
```bash
# Run against local development server
python3 demo_data_loader.py SaaS_Demo_Data_Sam.csv http://localhost:8080
# Run against a deployed environment with authentication
python3 demo_data_loader.py SaaS_Demo_Data_Sam.csv https://api.example.com "eyJhbGciOiJS..."
```
## CSV File Format
The CSV file must have:
- **Header row** - Column names in the first row
- **Data rows** - Multiple rows where rows with the same `FILE_NAME` belong to the same document
- **Single-value fields** (columns 1-18) - Should be identical for all rows with the same `FILE_NAME`
- **Array fields** (columns 19+) - Can vary per row, forming multiple array items per document
### Column Mapping
#### Single-Value Fields (Columns 1-18)
| Column | API Field |
| ----------------------------- | -------------------------- |
| FILE_NAME | fileName |
| CONTRACT_TITLE | contractTitle |
| AARETE_DERIVED_AMENDMENT_NUM | aareteDerivedAmendmentNum |
| CLIENT_NAME | clientName |
| PAYER_NAME | payerName |
| PAYER_STATE | payerState |
| PROVIDER_STATE | providerState |
| FILENAME_TIN | filenameTin |
| PROV_GROUP_TIN | provGroupTin |
| PROV_GROUP_NPI | provGroupNpi |
| PROV_GROUP_NAME_FULL | provGroupNameFull |
| PROV_OTHER_TIN | provOtherTin |
| PROV_OTHER_NPI | provOtherNpi |
| PROV_OTHER_NAME_FULL | provOtherNameFull |
| AARETE_DERIVED_EFFECTIVE_DT | aareteDerivedEffectiveDt |
| AARETE_DERIVED_TERMINATION_DT | aareteDerivedTerminationDt |
| AUTO_RENEWAL_IND | autoRenewalInd |
| AUTO_RENEWAL_TERM | autoRenewalTerm |
#### Array Fields (Columns 19+)
See the full mapping in the script source code. Array fields include:
- Exhibit information (title, page)
- Provider information (TIN, NPI, name)
- Claim type, product, LOB, program, network
- Taxonomy and specialty codes
- Reimbursement terms and rates
- Grouper information
- Outlier and stop-loss provisions
- Facility adjustments (DSH, IME, NTAP, etc.)
- Rate escalator information
## Data Type Conversions
The script automatically converts CSV values to appropriate types:
| Type | Fields | Conversion |
| ------- | ------------------------------------ | -------------------------------------- |
| Boolean | `*Ind` fields | Y/N/TRUE/FALSE -> true/false |
| Integer | `aareteDerivedAmendmentNum` | String -> integer |
| Numeric | `*Rate`, `*Amt`, `*Threshold` fields | String -> float (handles %, $, commas) |
| Date | `*Dt` fields | M/D/YYYY -> YYYY-MM-DD |
## Output
The script provides detailed progress output:
```
============================================================
Demo Data Loader
============================================================
CSV File: SaaS_Demo_Data_Sam.csv
Base URL: http://localhost:8080
JWT Token: not provided
============================================================
Step 1: Reading CSV File
============================================================
[OK] Read 220 data rows with 130 columns
============================================================
Step 2: Analyzing Data
============================================================
[OK] Found 23 unique documents to import
- 952436878_Providence MC_Amd01_01.2025: 1 array item(s)
- 952436878_Providence MC_Amd02_01.2025: 18 array item(s)
...
============================================================
Summary
============================================================
Client ID: demo-data-loader-1769734603
Documents uploaded: 23
Field extractions created: 23
Verifications passed: 23
Verifications failed: 0
[OK] All operations completed successfully!
```
## Exit Codes
| Code | Meaning |
| ---- | ---------------------------------------------------------------------- |
| 0 | All operations completed successfully |
| 1 | Error occurred (missing arguments, file read error, API failure, etc.) |
## Rate Limiting
The script includes:
- Automatic retry with exponential backoff for 429 (rate limit) responses
- Delays between API calls to avoid triggering rate limits
## API Endpoints Used
| Method | Endpoint | Purpose |
| ------ | ------------------------------------ | ----------------------- |
| POST | `/client` | Create demo client |
| POST | `/client/{id}/document` | Upload document |
| GET | `/client/{id}/document` | List documents |
| GET | `/document/{id}` | Get document details |
| POST | `/field-extractions` | Create field extraction |
| GET | `/field-extractions?documentId={id}` | Verify field extraction |
## Troubleshooting
### "No documents available after upload"
The document processing queue may be slow. Try:
1. Check that the `storeEventRunner` service is running
2. Increase the `max_attempts` parameter in `wait_for_documents()`
### Rate Limit Errors (429)
The script handles rate limits automatically with retries. If you still see errors:
1. Increase delays between operations in the script
2. Reduce the number of documents being imported at once
### "Failed to create extraction"
Check that:
1. The document exists in the system
2. The field values are valid (proper date formats, numeric values, etc.)
## Source Files
- `demo_data_loader.py` - Main script
- `SaaS_Demo_Data_Sam.csv` - Sample CSV data file
- `README.md` - This documentation
File diff suppressed because it is too large Load Diff