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

[#IP-215] Add AzureAllowBodyPayloadMiddleware #167

Merged
merged 4 commits into from
Jun 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 136 additions & 1 deletion src/utils/middlewares/__tests__/azure_api_auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
/* eslint-disable sonarjs/no-identical-functions */

import { isLeft, isRight } from "fp-ts/lib/Either";
import { failure } from "fp-ts/lib/Validation";
import * as t from "io-ts";

import { AzureApiAuthMiddleware, UserGroup } from "../azure_api_auth";
import {
AzureAllowBodyPayloadMiddleware,
AzureApiAuthMiddleware,
UserGroup
} from "../azure_api_auth";

const anAllowedGroupSet = new Set([UserGroup.ApiMessageWrite]);

Expand Down Expand Up @@ -256,3 +262,132 @@ describe("AzureApiAuthMiddleware", () => {
}
});
});

describe("AzureAllowBodyPayloadMiddleware", () => {
it("should success if pattern is not matched", async () => {
const headers = {
...someHeaders
};
const aPayload = { foo: { bar: "baz" } };
const mockRequest = {
header: jest.fn(lookup(headers)),
body: aPayload
};
const aNonMatchingCodec = t.interface({ anyField: t.number });
const anyGroupSet = new Set([UserGroup.ApiMessageWrite]);

const middleware = AzureAllowBodyPayloadMiddleware(
aNonMatchingCodec,
anyGroupSet
);

const result = await middleware(mockRequest as any);

expect(isRight(result)).toBe(true);
expect(aNonMatchingCodec.decode(aPayload).isLeft()).toBe(true); // test is wrong if it fails
});

it("should success if pattern is matched and user do belongs to correct user group", async () => {
const headers = {
...someHeaders
};
const aPayload = { foo: { bar: "baz" } };
const mockRequest = {
header: jest.fn(lookup(headers)),
body: aPayload
};
const aMatchingCodec = t.interface({
foo: t.interface({ bar: t.string })
});
const allowedGroupSet = new Set([UserGroup.ApiMessageWrite]);

const middleware = AzureAllowBodyPayloadMiddleware(
aMatchingCodec,
allowedGroupSet
);

const result = await middleware(mockRequest as any);

expect(isRight(result)).toBe(true);
expect(aMatchingCodec.decode(aPayload).isRight()).toBe(true); // test is wrong if it fails
});

it("should success if pattern is not matched - test nr. 2", async () => {
const headers = {
...someHeaders
};
const aPayload = { foo: { bar: "baz" } };
const mockRequest = {
header: jest.fn(lookup(headers)),
body: aPayload
};
const aNonMatchingCodec = t.interface({ anyField: t.string });
const anyGroupSet = new Set([UserGroup.ApiDebugRead]); // any value

const middleware = AzureAllowBodyPayloadMiddleware(
aNonMatchingCodec,
anyGroupSet
);

const result = await middleware(mockRequest as any);

expect(isRight(result)).toBe(true);
expect(aNonMatchingCodec.decode(aPayload).isLeft()).toBe(true); // test is wrong if it fails
});

michaeldisaro marked this conversation as resolved.
Show resolved Hide resolved
it("should fail if pattern is matched and current user does not belongs to user group", async () => {
const headers = {
...someHeaders
};
const aPayload = { foo: { bar: "baz" } };
const mockRequest = {
header: jest.fn(lookup(headers)),
body: aPayload
};
const aMatchingCodec = t.interface({
foo: t.interface({ bar: t.string })
});
const anotherAllowedGroupSet = new Set([UserGroup.ApiDebugRead]);

const middleware = AzureAllowBodyPayloadMiddleware(
aMatchingCodec,
anotherAllowedGroupSet
);

const result = await middleware(mockRequest as any);

expect(isLeft(result)).toBe(true);
result.fold(
_ => expect(_.kind).toBe("IResponseErrorForbiddenNotAuthorized"),
_ => fail("Expecting left")
);
});

it("should fail if pattern is matched and current user has no groups", async () => {
const headers = {
...someHeaders,
"x-user-groups": ""
};
const aPayload = { foo: { bar: "baz" } };
const mockRequest = {
header: jest.fn(lookup(headers)),
body: aPayload
};
const aMatchingCodec = t.interface({
foo: t.interface({ bar: t.string })
});

const middleware = AzureAllowBodyPayloadMiddleware(
aMatchingCodec,
anAllowedGroupSet
);

const result = await middleware(mockRequest as any);

expect(isLeft(result)).toBe(true);
result.fold(
_ => expect(_.kind).toBe("IResponseErrorForbiddenNoAuthorizationGroups"),
_ => fail("Expecting left")
);
});
});
michaeldisaro marked this conversation as resolved.
Show resolved Hide resolved
56 changes: 55 additions & 1 deletion src/utils/middlewares/azure_api_auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Either, isLeft, left, right } from "fp-ts/lib/Either";
import * as t from "io-ts";
import { Either, isLeft, left, right, either } from "fp-ts/lib/Either";
import { fromNullable, isSome, Option, Some } from "fp-ts/lib/Option";

import {
Expand Down Expand Up @@ -221,3 +222,56 @@ export const AzureApiAuthMiddleware = (
)
);
});

type AzureAllowBodyPayloadMiddlewareErrorResponses =
| IResponseErrorForbiddenNoAuthorizationGroups
| IResponseErrorForbiddenNotAuthorized;

/**
* A middleware that allow a specific payload to be provided only by a specific set of user groups
*
* The middleware expects the following headers:
*
* x-user-groups: A comma separated list of names of Azure API Groups
*
* On success, the middleware just lets the request to continue,
* on failure it triggers a ResponseErrorForbidden.
*
* @param pattern a codec that matches the payload pattern to restrict access to
* @param allowedGroups a set of user groups to allow provided payload
*
*/
export const AzureAllowBodyPayloadMiddleware = <S, A>(
pattern: t.Type<A, S>,
allowedGroups: ReadonlySet<UserGroup>
): IRequestMiddleware<
| "IResponseErrorForbiddenNotAuthorized"
| "IResponseErrorForbiddenNoAuthorizationGroups",
void
> => async (
request
): Promise<Either<AzureAllowBodyPayloadMiddlewareErrorResponses, void>> =>
either
.of<AzureAllowBodyPayloadMiddlewareErrorResponses, unknown>(request.body)
.chain(payload =>
pattern.decode(payload).fold(
// if pattern does not match payload, just skip the middleware
_ => right(void 0),
_ =>
NonEmptyString.decode(request.header("x-user-groups"))
// user groups groups must be valued
.mapLeft<AzureAllowBodyPayloadMiddlewareErrorResponses>(
__ => ResponseErrorForbiddenNoAuthorizationGroups
)
.map(getGroupsFromHeader)
// check if current user belongs to at least one of the allowed groups
.map(userGroups =>
Array.from(allowedGroups).some(e => userGroups.has(e))
)
.chain(isInGroup =>
isInGroup
? right(void 0)
: left(ResponseErrorForbiddenNotAuthorized)
)
)
);