Skip to content

Implement Access Control for Historian Endpoints #24839

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

11 changes: 8 additions & 3 deletions server/historian/packages/historian-base/src/routes/git/blobs.ts
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ import winston from "winston";
import { ICache, ITenantService, ISimplifiedCustomDataRetriever } from "../../services";
import * as utils from "../utils";
import { Constants } from "../../utils";
import { ScopeType } from "@fluidframework/protocol-definitions";

export function create(
config: nconf.Provider,
@@ -106,7 +107,11 @@ export function create(
"/repos/:ignored?/:tenantId/git/blobs",
validateRequestParams("tenantId"),
throttle(restTenantGeneralThrottler, winston, tenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(
revokedTokenChecker,
[ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
maxTokenLifetimeSec,
),
denyListMiddleware(denyList),
(request, response, next) => {
const blobP = createBlob(
@@ -125,7 +130,7 @@ export function create(
"/repos/:ignored?/:tenantId/git/blobs/:sha",
validateRequestParams("tenantId", "sha"),
throttle(restTenantGeneralThrottler, winston, tenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(revokedTokenChecker, [ScopeType.DocRead], maxTokenLifetimeSec),
denyListMiddleware(denyList),
(request, response, next) => {
const useCache = !("disableCache" in request.query);
@@ -146,7 +151,7 @@ export function create(
"/repos/:ignored?/:tenantId/git/blobs/raw/:sha",
validateRequestParams("tenantId", "sha"),
throttle(restTenantGeneralThrottler, winston, tenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(revokedTokenChecker, [ScopeType.DocRead], maxTokenLifetimeSec),
denyListMiddleware(denyList),
(request, response, next) => {
const useCache = !("disableCache" in request.query);
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ import {
throttle,
} from "@fluidframework/server-services-utils";
import { validateRequestParams } from "@fluidframework/server-services-shared";
import { ScopeType } from "@fluidframework/protocol-definitions";
import { Router } from "express";
import * as nconf from "nconf";
import winston from "winston";
@@ -91,7 +92,11 @@ export function create(
"/repos/:ignored?/:tenantId/git/commits",
validateRequestParams("tenantId"),
throttle(restTenantGeneralThrottler, winston, tenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(
revokedTokenChecker,
[ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
maxTokenLifetimeSec,
),
denyListMiddleware(denyList),
(request, response, next) => {
const commitP = createCommit(
@@ -108,7 +113,7 @@ export function create(
"/repos/:ignored?/:tenantId/git/commits/:sha",
validateRequestParams("tenantId", "sha"),
throttle(restTenantGeneralThrottler, winston, tenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(revokedTokenChecker, [ScopeType.DocRead], maxTokenLifetimeSec),
denyListMiddleware(denyList),
(request, response, next) => {
const useCache = !("disableCache" in request.query);
23 changes: 18 additions & 5 deletions server/historian/packages/historian-base/src/routes/git/refs.ts
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ import winston from "winston";
import { ICache, ITenantService, ISimplifiedCustomDataRetriever } from "../../services";
import * as utils from "../utils";
import { Constants } from "../../utils";
import { ScopeType } from "@fluidframework/protocol-definitions";

export function create(
config: nconf.Provider,
@@ -149,7 +150,7 @@ export function create(
"/repos/:ignored?/:tenantId/git/refs",
validateRequestParams("tenantId"),
throttle(restTenantGeneralThrottler, winston, tenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(revokedTokenChecker, [ScopeType.DocRead], maxTokenLifetimeSec),
denyListMiddleware(denyList),
(request, response, next) => {
const refsP = getRefs(request.params.tenantId, request.get("Authorization"));
@@ -161,7 +162,7 @@ export function create(
"/repos/:ignored?/:tenantId/git/refs/*",
validateRequestParams("tenantId", 0),
throttle(restTenantGeneralThrottler, winston, tenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(revokedTokenChecker, [ScopeType.DocRead], maxTokenLifetimeSec),
(request, response, next) => {
const refP = getRef(
request.params.tenantId,
@@ -176,7 +177,11 @@ export function create(
"/repos/:ignored?/:tenantId/git/refs",
validateRequestParams("tenantId"),
throttle(restTenantGeneralThrottler, winston, tenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(
revokedTokenChecker,
[ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
maxTokenLifetimeSec,
),
denyListMiddleware(denyList),
(request, response, next) => {
const refP = createRef(
@@ -192,7 +197,11 @@ export function create(
"/repos/:ignored?/:tenantId/git/refs/*",
validateRequestParams("tenantId", 0),
throttle(restTenantGeneralThrottler, winston, tenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(
revokedTokenChecker,
[ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
maxTokenLifetimeSec,
),
denyListMiddleware(denyList),
(request, response, next) => {
const refP = updateRef(
@@ -209,7 +218,11 @@ export function create(
"/repos/:ignored?/:tenantId/git/refs/*",
validateRequestParams("tenantId", 0),
throttle(restTenantGeneralThrottler, winston, tenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(
revokedTokenChecker,
[ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
maxTokenLifetimeSec,
),
// Skip documentDenyListCheck, as it is not needed for delete operations
denyListMiddleware(denyList, true /* skipDocumentDenyListCheck */),
(request, response, next) => {
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ import winston from "winston";
import { ICache, ITenantService, ISimplifiedCustomDataRetriever } from "../../services";
import * as utils from "../utils";
import { Constants } from "../../utils";
import { ScopeType } from "@fluidframework/protocol-definitions";

export function create(
config: nconf.Provider,
@@ -90,7 +91,11 @@ export function create(
"/repos/:ignored?/:tenantId/git/tags",
validateRequestParams("tenantId"),
throttle(restTenantGeneralThrottler, winston, tenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(
revokedTokenChecker,
[ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
maxTokenLifetimeSec,
),
denyListMiddleware(denyList),
(request, response, next) => {
const tagP = createTag(
@@ -106,7 +111,7 @@ export function create(
"/repos/:ignored?/:tenantId/git/tags/*",
validateRequestParams("tenantId", 0),
throttle(restTenantGeneralThrottler, winston, tenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(revokedTokenChecker, [ScopeType.DocRead], maxTokenLifetimeSec),
denyListMiddleware(denyList),
(request, response, next) => {
const tagP = getTag(
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ import winston from "winston";
import { ICache, ITenantService, ISimplifiedCustomDataRetriever } from "../../services";
import * as utils from "../utils";
import { Constants } from "../../utils";
import { ScopeType } from "@fluidframework/protocol-definitions";

export function create(
config: nconf.Provider,
@@ -91,7 +92,11 @@ export function create(
"/repos/:ignored?/:tenantId/git/trees",
validateRequestParams("tenantId"),
throttle(restTenantGeneralThrottler, winston, tenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(
revokedTokenChecker,
[ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
maxTokenLifetimeSec,
),
denyListMiddleware(denyList),
(request, response, next) => {
const treeP = createTree(
@@ -107,7 +112,7 @@ export function create(
"/repos/:ignored?/:tenantId/git/trees/:sha",
validateRequestParams("tenantId", "sha"),
throttle(restTenantGeneralThrottler, winston, tenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(revokedTokenChecker, [ScopeType.DocRead], maxTokenLifetimeSec),
denyListMiddleware(denyList),
(request, response, next) => {
const useCache = !("disableCache" in request.query);
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ import { ICache, ITenantService, ISimplifiedCustomDataRetriever } from "../../se
import * as utils from "../utils";
import { Constants } from "../../utils";
import { NetworkError } from "@fluidframework/server-services-client";
import { ScopeType } from "@fluidframework/protocol-definitions";

export function create(
config: nconf.Provider,
@@ -79,7 +80,7 @@ export function create(
"/repos/:ignored?/:tenantId/commits",
validateRequestParams("tenantId"),
throttle(restTenantGeneralThrottler, winston, tenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(revokedTokenChecker, [ScopeType.DocRead], maxTokenLifetimeSec),
denyListMiddleware(denyList),
(request, response, next) => {
const sha = utils.queryParamToString(request.query.sha);
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ import winston from "winston";
import { ICache, ITenantService, ISimplifiedCustomDataRetriever } from "../../services";
import * as utils from "../utils";
import { Constants } from "../../utils";
import { ScopeType } from "@fluidframework/protocol-definitions";

export function create(
config: nconf.Provider,
@@ -71,7 +72,7 @@ export function create(
"/repos/:ignored?/:tenantId/contents/*",
validateRequestParams("tenantId", 0),
throttle(restTenantGeneralThrottler, winston, tenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(revokedTokenChecker, [ScopeType.DocRead], maxTokenLifetimeSec),
denyListMiddleware(denyList),
(request, response, next) => {
const contentP = getContent(
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ import winston from "winston";
import { ICache, ITenantService, ISimplifiedCustomDataRetriever } from "../../services";
import * as utils from "../utils";
import { Constants } from "../../utils";
import { ScopeType } from "@fluidframework/protocol-definitions";

export function create(
config: nconf.Provider,
@@ -91,7 +92,7 @@ export function create(
"/repos/:ignored?/:tenantId/headers/:sha",
validateRequestParams("tenantId", "sha"),
throttle(restTenantGeneralThrottler, winston, tenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(revokedTokenChecker, [ScopeType.DocRead], maxTokenLifetimeSec),
denyListMiddleware(denyList),
(request, response, next) => {
const useCache = !("disableCache" in request.query);
@@ -109,7 +110,7 @@ export function create(
"/repos/:ignored?/:tenantId/tree/:sha",
validateRequestParams("tenantId", "sha"),
throttle(restTenantGeneralThrottler, winston, tenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(revokedTokenChecker, [ScopeType.DocRead], maxTokenLifetimeSec),
denyListMiddleware(denyList),
(request, response, next) => {
const useCache = !("disableCache" in request.query);
15 changes: 12 additions & 3 deletions server/historian/packages/historian-base/src/routes/summaries.ts
Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@ import { BaseTelemetryProperties, Lumberjack } from "@fluidframework/server-serv
import { ICache, ITenantService, ISimplifiedCustomDataRetriever } from "../services";
import { parseToken, Constants } from "../utils";
import * as utils from "./utils";
import { ScopeType } from "@fluidframework/protocol-definitions";

export function create(
config: nconf.Provider,
@@ -166,7 +167,7 @@ export function create(
validateRequestParams("tenantId", "sha"),
throttle(restClusterGetSummaryThrottler, winston, getSummaryPerClusterThrottleOptions),
throttle(restTenantGetSummaryThrottler, winston, getSummaryPerTenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(revokedTokenChecker, [ScopeType.DocRead], maxTokenLifetimeSec),
denyListMiddleware(denyList),
(request, response, next) => {
const useCache = !("disableCache" in request.query);
@@ -195,7 +196,11 @@ export function create(
createSummaryPerClusterThrottleOptions,
),
throttle(restTenantCreateSummaryThrottler, winston, createSummaryPerTenantThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(
revokedTokenChecker,
[ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
maxTokenLifetimeSec,
),
denyListMiddleware(denyList),
(request, response, next) => {
// request.query type is { [string]: string } but it's actually { [string]: any }
@@ -246,7 +251,11 @@ export function create(
"/repos/:ignored?/:tenantId/git/summaries",
validateRequestParams("tenantId"),
throttle(restTenantGeneralThrottler, winston, tenantGeneralThrottleOptions),
utils.verifyToken(revokedTokenChecker, maxTokenLifetimeSec),
utils.verifyToken(
revokedTokenChecker,
[ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
maxTokenLifetimeSec,
),
// Skip documentDenyListCheck, as it is not needed for delete operations
denyListMiddleware(denyList, true /* skipDocumentDenyListCheck */),
(request, response, next) => {
17 changes: 15 additions & 2 deletions server/historian/packages/historian-base/src/routes/utils.ts
Original file line number Diff line number Diff line change
@@ -344,6 +344,7 @@ export function queryParamToString(value: any): string | undefined {

export function verifyToken(
revokedTokenChecker: IRevokedTokenChecker | undefined,
requiredScopes: string[],
maxTokenLifetimeSec: number,
): RequestHandler {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
@@ -365,14 +366,14 @@ export function verifyToken(
// Prevent attempted directory traversal.
throw new NetworkError(400, `Invalid document id: ${documentId}`);
}

// Verify token not revoked if JTI claim is present
if (revokedTokenChecker && claims.jti) {
const isTokenRevoked = await revokedTokenChecker.isTokenRevoked(
tenantId,
claims.documentId,
documentId,
claims.jti,
);

if (isTokenRevoked) {
throw new NetworkError(
403,
@@ -383,6 +384,18 @@ export function verifyToken(
}
}

if (requiredScopes) {
const hasAllRequiredScopes = requiredScopes.every((scope) =>
claims.scopes.includes(scope),
);
if (!hasAllRequiredScopes) {
throw new NetworkError(
403,
`Permission denied. Insufficient scopes. Required scopes: ${requiredScopes}`,
);
}
}

if (!claims.exp || !claims.iat || claims.exp - claims.iat > maxTokenLifetimeSec) {
throw new NetworkError(403, "Invalid token expiry");
}
Loading
Oops, something went wrong.