Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(test) post auth test #1020

Merged
merged 10 commits into from
Jan 15, 2025
99 changes: 99 additions & 0 deletions lib/lambda/postAuth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { describe, it, expect, vi, afterAll } from "vitest";
import { Context } from "aws-lambda";
import { handler } from "./postAuth";
import {
makoStateSubmitter,
setMockUsername,
superUser,
TEST_IDM_USERS,
USER_POOL_ID,
} from "mocks";

const callback = vi.fn();
describe("process emails Handler", () => {
afterAll(() => {
setMockUsername(makoStateSubmitter);
});
it("should return an error due to missing arn", async () => {
delete process.env.idmAuthzApiKeyArn;

await expect(handler({ test: "test" }, {} as Context, callback)).rejects.toThrowError(
"ERROR: process.env.idmAuthzApiKeyArn is required",
);
});
it("should return an error due to a missing endpoint", async () => {
delete process.env.idmAuthzApiEndpoint;
await expect(handler({ test: "test" }, {} as Context, callback)).rejects.toThrowError(
"ERROR: process.env.idmAuthzApiEndpoint is required",
);
});
it("should return an error due to the arn being incorrect", async () => {
process.env.idmAuthzApiKeyArn = "bad-ARN"; // pragma: allowlist secret
await expect(handler({ test: "test" }, {} as Context, callback)).rejects.toThrowError(
"Failed to fetch secret bad-ARN: Secret bad-ARN has no SecretString field present in response",
);
});

it("should return the request if it is missing an identity", async () => {
const consoleSpy = vi.spyOn(console, "log");
const missingIdentity = await handler(
{
request: {
userAttributes: TEST_IDM_USERS.testStateIDMUserMissingIdentity,
},
},
{} as Context,
callback,
);
expect(consoleSpy).toBeCalledWith("User is not managed externally. Nothing to do.");
expect(missingIdentity).toStrictEqual({
request: {
userAttributes: TEST_IDM_USERS.testStateIDMUserMissingIdentity,
},
});
});
it("should log an error since it cannot authorize the user", async () => {
const errorSpy = vi.spyOn(console, "error");
const missingIdentity = await handler(
{
request: {
userAttributes: TEST_IDM_USERS.testStateIDMUser,
},
},
{} as Context,
callback,
);
const error = new Error("Network response was not ok. Response was 401: Unauthorized");
expect(errorSpy).toHaveBeenCalledWith("Error performing post auth:", error);
expect(errorSpy).toBeCalledTimes(1);
expect(missingIdentity).toStrictEqual({
request: {
userAttributes: TEST_IDM_USERS.testStateIDMUser,
},
});
});
it("should return the user and update the user in the service", async () => {
const consoleSpy = vi.spyOn(console, "log");
const validUser = await handler(
{
request: {
userAttributes: TEST_IDM_USERS.testStateIDMUserGood,
},
userName: superUser.Username,
userPoolId: USER_POOL_ID,
},
{} as Context,
callback,
);
expect(consoleSpy).toBeCalledWith(
`Attributes for user ${superUser.Username} updated successfully.`,
);
expect(validUser).toStrictEqual({
request: {
userAttributes: TEST_IDM_USERS.testStateIDMUserGood,
},
userName: superUser.Username,
userPoolId: USER_POOL_ID,
});
});
});
54 changes: 17 additions & 37 deletions lib/lambda/postAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,16 @@ import {
import { getSecret } from "shared-utils";

// Initialize Cognito client
const client = new CognitoIdentityProviderClient({});

const client = new CognitoIdentityProviderClient({
region: process.env.region || process.env.REGION_A || "us-east-1",
});
export const handler: Handler = async (event) => {
console.log(JSON.stringify(event, null, 2));

// Check if idmInfoSecretArn is provided
if (!process.env.idmAuthzApiKeyArn) {
throw "ERROR: process.env.idmAuthzApiKeyArn is required";
}
if (!process.env.idmAuthzApiEndpoint) {
throw "ERROR: process.env.idmAuthzApiKeyArn is required";
throw "ERROR: process.env.idmAuthzApiEndpoint is required";
}

const apiEndpoint: string = process.env.idmAuthzApiEndpoint;
Expand All @@ -32,26 +31,21 @@ export const handler: Handler = async (event) => {

const { request } = event;
const { userAttributes } = request;

if (!userAttributes.identities) {
console.log("User is not managed externally. Nothing to do.");
} else {
console.log("Getting user attributes from external API");

try {
const username = userAttributes["custom:username"]; // This is the four-letter IDM username
const response = await fetch(
`${apiEndpoint}/api/v1/authz/id/all?userId=${username}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
},
const response = await fetch(`${apiEndpoint}/api/v1/authz/id/all?userId=${username}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
},
);
});
if (!response.ok) {
console.log(response);
throw new Error(
`Network response was not ok. Response was ${response.status}: ${response.statusText}`,
);
Expand Down Expand Up @@ -108,22 +102,14 @@ async function updateUserAttributes(params: any): Promise<void> {
const user = await client.send(getUserCommand);

// Check for existing "custom:cms-roles"
const cmsRolesAttribute = user.UserAttributes?.find(
(attr) => attr.Name === "custom:cms-roles",
);
const cmsRolesAttribute = user.UserAttributes?.find((attr) => attr.Name === "custom:cms-roles");
const existingRoles =
cmsRolesAttribute && cmsRolesAttribute.Value
? cmsRolesAttribute.Value.split(",")
: [];
cmsRolesAttribute && cmsRolesAttribute.Value ? cmsRolesAttribute.Value.split(",") : [];

// Check for existing "custom:state"
const stateAttribute = user.UserAttributes?.find(
(attr) => attr.Name === "custom:state",
);
const stateAttribute = user.UserAttributes?.find((attr) => attr.Name === "custom:state");
const existingStates =
stateAttribute && stateAttribute.Value
? stateAttribute.Value.split(",")
: [];
stateAttribute && stateAttribute.Value ? stateAttribute.Value.split(",") : [];

// Prepare for updating user attributes
const attributeData: any = {
Expand All @@ -146,8 +132,7 @@ async function updateUserAttributes(params: any): Promise<void> {
),
)
: new Set(["onemac-micro-super"]); // Ensure "onemac-micro-super" is always included
attributeData.UserAttributes[rolesIndex].Value =
Array.from(newRoles).join(",");
attributeData.UserAttributes[rolesIndex].Value = Array.from(newRoles).join(",");
} else {
// Add "custom:cms-roles" with "onemac-micro-super"
attributeData.UserAttributes.push({
Expand All @@ -165,14 +150,9 @@ async function updateUserAttributes(params: any): Promise<void> {
if (stateIndex !== -1) {
// Only merge if new states are not empty
const newStates = attributeData.UserAttributes[stateIndex].Value
? new Set(
attributeData.UserAttributes[stateIndex].Value.split(",").concat(
"ZZ",
),
)
? new Set(attributeData.UserAttributes[stateIndex].Value.split(",").concat("ZZ"))
: new Set(["ZZ"]); // Ensure "ZZ" is always included
attributeData.UserAttributes[stateIndex].Value =
Array.from(newStates).join(",");
attributeData.UserAttributes[stateIndex].Value = Array.from(newStates).join(",");
} else {
// Add "custom:state" with "ZZ"
attributeData.UserAttributes.push({
Expand Down
2 changes: 2 additions & 0 deletions lib/vitest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ beforeEach(() => {
process.env.DLQ_URL = "https://sqs.us-east-1.amazonaws.com/123/test";
process.env.configurationSetName = "SES";
process.env.brokerString = KAFKA_BROKERS;
process.env.idmAuthzApiKeyArn = "test-secret"; // pragma: allowlist secret
process.env.idmAuthzApiEndpoint = "https://dimAuthzEndpoint.com";
});

afterEach(() => {
Expand Down
40 changes: 40 additions & 0 deletions mocks/data/users/idmUsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export const testStateIDMUserMissingIdentity = {
sub: "0000aaaa-0000-00aa-0a0a-aaaaaa000000",
"custom:cms-roles": "onemac-micro-statesubmitter",
"custom:state": "VA,OH,SC,CO,GA,MD",
email_verified: true,
given_name: "State",
family_name: "Person",
username: "abcd",
email: "[email protected]",
};

export const testStateIDMUser = {
sub: "0000aaaa-0000-00aa-0a0a-aaaaaa000000",
"custom:cms-roles": "onemac-micro-statesubmitter",
"custom:state": "VA,OH,SC,CO,GA,MD",
email_verified: true,
given_name: "State",
family_name: "Person",
"custom:username": "fail",
email: "[email protected]",
identities:
'[{"dateCreated":"1709308952587","userId":"abc123","providerName":"IDM","providerType":"OIDC","issuer":null,"primary":"true"}]',
};
export const testStateIDMUserGood = {
sub: "0000aaaa-0000-00aa-0a0a-aaaaaa000000",
"custom:cms-roles": "onemac-micro-super",
"custom:state": "VA,OH,SC,CO,GA,MD",
email_verified: true,
given_name: "State",
family_name: "Person",
"custom:username": "abcd",
email: "[email protected]",
identities:
'[{"dateCreated":"1709308952587","userId":"abc123","providerName":"IDM","providerType":"OIDC","issuer":null,"primary":"true"}]',
};
export const TEST_IDM_USERS = {
testStateIDMUser,
testStateIDMUserGood,
testStateIDMUserMissingIdentity,
};
1 change: 1 addition & 0 deletions mocks/data/users/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ export * from "./helpDeskUsers";
export * from "./mockStorage";
export * from "./readOnlyCMSUsers";
export * from "./stateSubmitters";
export * from "./idmUsers";
34 changes: 34 additions & 0 deletions mocks/data/users/stateSubmitters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,39 @@ export const makoStateSubmitter: TestUserData = {
],
Username: "cd400c39-9e7c-4341-b62f-234e2ecb339d",
};
export const superUser: TestUserData = {
UserAttributes: [
{
Name: "email",
Value: "[email protected]",
},
{
Name: "email_verified",
Value: "true",
},
{
Name: "given_name",
Value: "Stateuser",
},
{
Name: "family_name",
Value: "Tester",
},
{
Name: "custom:state",
Value: "ZZ",
},
{
Name: "custom:cms-roles",
Value: "onemac-micro-super",
},
{
Name: "sub",
Value: "cd400c39-9e7c-4341-b62f-234e2ecb339e",
},
],
Username: "cd400c39-9e7c-4341-b62f-234e2ecb339e",
};

export const stateSubmitter: TestUserData = {
UserAttributes: [
Expand Down Expand Up @@ -275,6 +308,7 @@ export const testNewStateSubmitter: TestUserData = {

export const stateSubmitters: TestUserData[] = [
makoStateSubmitter,
superUser,
stateSubmitter,
noDataStateSubmitter,
coStateSubmitter,
Expand Down
4 changes: 3 additions & 1 deletion mocks/handlers/aws/cognito.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,9 @@ export const identityProviderServiceHandler = http.post<
);
return passthrough();
}

if (target == "AWSCognitoIdentityProviderService.AdminUpdateUserAttributes") {
return new HttpResponse(null, { status: 200 });
}
if (target == "AWSCognitoIdentityProviderService.GetUser") {
const { AccessToken } = (await request.json()) as IdpRequestSessionBody;
const username = getUsernameFromAccessToken(AccessToken) || process.env.MOCK_USER_USERNAME;
Expand Down
30 changes: 30 additions & 0 deletions mocks/handlers/aws/idm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { http, HttpResponse } from "msw";

const defaultIDMHandler = http.get(
"https://dimauthzendpoint.com/api/v1/authz/id/all",
async ({ request }) => {
const url = new URL(request.url);
const id = url.searchParams.get("userId");

if (id === "fail") {
return HttpResponse.json({ text: "Failed to retrieve user" }, { status: 401 });
} else if (id === "abcd") {
return HttpResponse.json(
{
userProfileAppRoles: {
userRolesInfoList: [
{
roleName: "onemac-micro-statesubmitter",
roleAttributes: [{ name: "State/Territory", value: "VA" }],
},
],
},
},
{ status: 200 },
);
}
return HttpResponse.json({ text: "Failed to retrieve user" }, { status: 200 });
},
);

export const idmHandlers = [defaultIDMHandler];
2 changes: 2 additions & 0 deletions mocks/handlers/aws/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { lambdaHandlers } from "./lambda";
import { secretsManagerHandlers } from "./secretsManager";
import { stepFunctionHandlers } from "./stepFunctions";
import { emailHandlers } from "./email";
import { idmHandlers } from "./idm";
export const awsHandlers = [
...cloudFormationHandlers,
...cognitoHandlers,
Expand All @@ -13,6 +14,7 @@ export const awsHandlers = [
...secretsManagerHandlers,
...stepFunctionHandlers,
...emailHandlers,
...idmHandlers,
];

export { errorCloudFormationHandler } from "./cloudFormation";
Expand Down
Loading