Skip to content

Commit

Permalink
Merge pull request #53 from fractal-analytics-platform/auth-changes-2
Browse files Browse the repository at this point in the history
Used allowed viewer paths received from server; Supported tokens
  • Loading branch information
zonia3000 authored Nov 22, 2024
2 parents c43c5ff + a074d1d commit 099ce1b
Show file tree
Hide file tree
Showing 16 changed files with 193 additions and 423 deletions.
3 changes: 1 addition & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
PORT=3000
FRACTAL_SERVER_URL=http://localhost:8000
ZARR_DATA_BASE_PATH=/path/to/zarr-files
VIZARR_STATIC_FILES_PATH=/path/to/vizarr/dist
BASE_PATH=/vizarr
AUTHORIZATION_SCHEME=fractal-server-viewer-paths
AUTHORIZATION_SCHEME=fractal-server
CACHE_EXPIRATION_TIME=60
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
Note: Numbers like (#123) point to closed Pull Requests on the fractal-vizarr-viewer repository.

# Unreleased

* Retrieved complete list of allowed viewer paths directly from fractal-server: (\#53);
* removed `user-folders` and `fractal-server-viewer-paths` authorization schemes;
* added `fractal-server` authorization scheme;
* Supported both tokens and cookies (\#53);

# 0.2.4

* Allowed `project_dir` outside `ZARR_DATA_BASE_PATH` (\#50);
Expand Down
27 changes: 16 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,20 @@ The application has 2 endpoints:
## How it works

When a user logins to fractal-web, the browser receives a cookie that is generated by fractal-server. The same cookie is sent by the browser to other services on the same domain. The fractal-vizarr-viewer service forwards that cookie back to fractal-server in order to obtain the user details and then decides if the user is authorized to retrieve the requested file or not:
When a user logins to fractal-web, the browser receives a cookie that is generated by fractal-server. The same cookie is sent by the browser to other services on the same domain. The fractal-vizarr-viewer service extracts the token contained in the cookie and forwards it back to fractal-server in order to obtain the allowed viewer paths for the user and then decides if the user is authorized to retrieve the requested file or not:

![Fractal Data cookie flow](./fractal-vizarr-viewer-cookie-flow.png)

Currently we support 3 different kinds of authorization checks, that can be specified using the `AUTHORIZATION_SCHEME` environment variable. The service retrieves the user details from the cookie calling fractal server and then applies the configured authorization logic. See the [environment variables](#environment-variables) section below for details about the supported authorization schemes.

### Accessing files using the token

While in the browser the authentication relies on cookies, that are automatically shared by the browser across fractal services, for command line and desktop applications it is more appropriate to use bearer tokens. For this reason, files exposed by fractal-vizarr-viewer can be retrieved both using cookies and tokens. The token can be downloaded from fractal-web user profile page. The following example shows an example of usage with `curl`:

```sh
curl -H "Authorization: Bearer $(cat /path/to/fractal-token.txt)" http://localhost:3000/vizarr/data/path/to/file
```

### Note about the domain constraint

This cookie-based technique can be used only if fractal-server and fractal-vizarr-viewer are reachable from the same domain (or different subdomains of the same main domain). The single applications can be located on different servers, but a common reverse proxy must be used to expose them on the same domain.
Expand Down Expand Up @@ -57,12 +65,10 @@ To start the application installed in this way see the section [Run fractal-viza

* `PORT`: the port where fractal-vizarr-viewer app is served;
* `FRACTAL_SERVER_URL`: the base URL of fractal-server;
* `ZARR_DATA_BASE_PATH`: path to Zarr files served by fractal-vizarr-viewer; when this variable is set the app reads files only in this directory; it is ignored if the `AUTHORIZATION_SCHEME` is set to `fractal-server-viewer-paths`;
* `VIZARR_STATIC_FILES_PATH`: path to the files generated running `npm run build` in vizarr source folder;
* `BASE_PATH`: base path of fractal-vizarr-viewer application;
* `AUTHORIZATION_SCHEME`: defines how the service verifies user authorization. The following options are available:
* `fractal-server-viewer-paths`: the paths that can be accessed by each user are retrieved calling fractal-server API.
* `user-folders`: each registered user can only access their own folder, which corresponds to a directory under `ZARR_DATA_BASE_PATH` named as their `slurm_user` field.
* `fractal-server`: the paths that can be accessed by each user are retrieved calling fractal-server API.
* `testing-basic-auth`: enables Basic Authentication for testing purposes. The credentials are specified through two additional environment variables: `TESTING_USERNAME` and `TESTING_PASSWORD`. This option should not be used in production environments.
* `none`: no authorization checks are performed, allowing access to all users, including anonymous ones. This option is useful for demonstrations and testing but should not be used in production environments.
* `CACHE_EXPIRATION_TIME`: cookie cache TTL in seconds; when user info is retrieved from a cookie calling the current user endpoint on fractal-server the information is cached for the specified amount of seconds, to reduce the number of calls to fractal-server;
Expand All @@ -79,9 +85,8 @@ You can create a script with the following content to run fractal-vizarr-viewer

export PORT=3000
export FRACTAL_SERVER_URL=http://localhost:8000
export ZARR_DATA_BASE_PATH=/path/to/zarr-files
export VIZARR_STATIC_FILES_PATH=/path/to/vizarr/dist
export AUTHORIZATION_SCHEME=fractal-server-viewer-paths
export AUTHORIZATION_SCHEME=fractal-server
# default values for logging levels (uncomment if needed)
# export LOG_LEVEL_CONSOLE=info
# export LOG_FILE=/path/to/log
Expand All @@ -102,8 +107,9 @@ node --env-file=.env dist/app.js

## Create some test data

The application reads the zarr files from the folder configured using the `ZARR_DATA_BASE_PATH` environment variable.
Assuming that you want to call the folder `zarr-files` you can fill it with some test data using the following command:
Create a folder (i.e. `zarr-files`) that will contain the zarr files served by fractal-vizarr-viewer. This folder has to be added to the allowed viewer paths exposed by fractal-server API, for example setting it as the `project_dir` for a given user.

You can fill the folder with some test data using the following command:

```bash
mkdir zarr-files
Expand Down Expand Up @@ -140,10 +146,9 @@ After=syslog.target
User=fractal
Environment="PORT=3000"
Environment="FRACTAL_SERVER_URL=https://fractal-server.example.com/"
Environment="ZARR_DATA_BASE_PATH=/path/to/zarr-files"
Environment="VIZARR_STATIC_FILES_PATH=/path/to/vizarr/dist"
Environment="BASE_PATH=/vizarr"
Environment="AUTHORIZATION_SCHEME=fractal-server-viewer-paths"
Environment="AUTHORIZATION_SCHEME=fractal-server"
Environment="CACHE_EXPIRATION_TIME=60"
Environment="LOG_FILE=/path/to/log"
Environment="LOG_LEVEL_FILE=info"
Expand Down Expand Up @@ -213,7 +218,7 @@ docker build . -t "$IMAGE_NAME"
docker run --network host \
-v /tmp/zarr-files:/zarr-files \
-e FRACTAL_SERVER_URL=http://localhost:8000 \
-e AUTHORIZATION_SCHEME=fractal-server-viewer-paths \
-e AUTHORIZATION_SCHEME=fractal-server \
"$IMAGE_NAME"
```

Expand Down
Binary file modified fractal-vizarr-viewer-cookie-flow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
151 changes: 30 additions & 121 deletions src/authorizer.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import * as path from "path";
import type { Request } from "express";
import { caching } from "cache-manager";
import { getConfig } from "./config.js";
import { getLogger } from "./logger.js";
import { User, UserSettings } from "./types";
import { getUserFromCookie } from "./user.js";
import { getUserFromRequest } from "./user.js";
import { isSubfolder } from "./path.js";

const config = getConfig();
Expand All @@ -13,23 +11,19 @@ const logger = getLogger();
// cache TTL in milliseconds
const ttl = config.cacheExpirationTime * 1000;

const settingsCache = await caching("memory", { ttl });
const viewerPathsCache = await caching("memory", { ttl });

// Track the cookies for which we are retrieving the user info from fractal-server
// Track the tokens for which we are retrieving the user info from fractal-server
// Used to avoid querying the cache while the fetch call is in progress
let loadingSettings: string[] = [];
let loadingViewerPaths: string[] = [];

/**
* Returns the class that performs the authorization logic.
*/
export function getAuthorizer() {
switch (config.authorizationScheme) {
case "fractal-server-viewer-paths":
return new ViewerPathsAuthorizer();
case "user-folders":
return new UserFoldersAuthorizer();
case "fractal-server":
return new FractalServerAuthorizer();
case "none":
logger.warn(
'Authorization scheme is set to "none": everybody will be able to access the file. Do not use in production!'
Expand Down Expand Up @@ -66,99 +60,59 @@ export class NoneAuthorizer implements Authorizer {
return true;
}

async isUserAuthorized(completePath: string): Promise<boolean> {
return isSubfolder(config.zarrDataBasePath!, completePath);
}
}

export class UserFoldersAuthorizer implements Authorizer {
async isUserValid(req: Request): Promise<boolean> {
const user = await getUserFromCookie(req.get("Cookie"));
return !!user;
}

async isUserAuthorized(completePath: string, req: Request): Promise<boolean> {
const cookie = req.get("Cookie");
const user = await getUserFromCookie(cookie);
if (!user || !cookie) {
return false;
}
const settings = await getUserSettings(user, cookie);
if (!settings) {
return false;
}

if (
settings.project_dir &&
isSubfolder(settings.project_dir, completePath)
) {
return true;
}

const username = settings.slurm_user;
if (!username) {
logger.warn('Slurm user is not defined for "%s"', user.email);
return false;
}

const userPath = path.join(config.zarrDataBasePath!, username);
return isSubfolder(userPath, completePath);
async isUserAuthorized(): Promise<boolean> {
return true;
}
}

export class ViewerPathsAuthorizer implements Authorizer {
export class FractalServerAuthorizer implements Authorizer {
async isUserValid(req: Request): Promise<boolean> {
const user = await getUserFromCookie(req.get("Cookie"));
const user = await getUserFromRequest(req);
return !!user;
}

async isUserAuthorized(completePath: string, req: Request): Promise<boolean> {
const cookie = req.get("Cookie");
const user = await getUserFromCookie(cookie);
if (!user || !cookie) {
const userData = await getUserFromRequest(req);
if (!userData) {
return false;
}
const settings = await getUserSettings(user, cookie);
while (loadingViewerPaths.includes(cookie)) {
// a fetch call for this cookie is in progress; wait for its completion
const { user, token } = userData;
while (loadingViewerPaths.includes(token)) {
// a fetch call for this token is in progress; wait for its completion
await new Promise((r) => setTimeout(r));
}
loadingViewerPaths.push(cookie);
loadingViewerPaths.push(token);
try {
let viewerPaths: string[] | undefined = await viewerPathsCache.get(
cookie
let allowedPaths: string[] | undefined = await viewerPathsCache.get(
token
);
if (viewerPaths === undefined) {
logger.trace("Retrieving viewer paths for user %s", user.email);
if (allowedPaths === undefined) {
logger.trace("Retrieving allowed viewer paths for user %s", user.email);
const response = await fetch(
`${config.fractalServerUrl}/auth/current-user/viewer-paths/`,
`${config.fractalServerUrl}/auth/current-user/allowed-viewer-paths/`,
{
headers: {
Cookie: cookie,
Authorization: `Bearer ${token}`,
},
}
);
if (response.ok) {
viewerPaths = (await response.json()) as string[];
allowedPaths = (await response.json()) as string[];
logger.trace(
"Retrieved %d viewer paths for user %s",
viewerPaths.length,
"Retrieved %d allowed viewer paths for user %s",
allowedPaths.length,
user.email
);
viewerPathsCache.set(cookie, viewerPaths);
viewerPathsCache.set(token, allowedPaths);
} else {
logger.debug(
"Fractal server replied with %d while retrieving viewer paths for user %s",
"Fractal server replied with %d while retrieving allowed viewer paths for user %s",
response.status,
user.email
);
return false;
}
}
const allowedPaths =
settings && settings.project_dir
? [settings.project_dir, ...viewerPaths]
: viewerPaths;
for (const allowedPath of allowedPaths) {
if (isSubfolder(allowedPath, completePath)) {
return true;
Expand All @@ -167,59 +121,18 @@ export class ViewerPathsAuthorizer implements Authorizer {
logger.trace("Unauthorized path %s", completePath);
return false;
} finally {
loadingViewerPaths = loadingViewerPaths.filter((c) => c !== cookie);
loadingViewerPaths = loadingViewerPaths.filter((c) => c !== token);
}
}
}

async function getUserSettings(
user: User,
cookie: string
): Promise<UserSettings | undefined> {
while (loadingSettings.includes(cookie)) {
// a fetch call for this cookie is in progress; wait for its completion
await new Promise((r) => setTimeout(r));
}
loadingSettings.push(cookie);
try {
const value: string | undefined = await settingsCache.get(cookie);
if (value) {
return JSON.parse(value) as UserSettings;
} else {
logger.trace("Retrieving settings from cookie");
const response = await fetch(
`${config.fractalServerUrl}/auth/current-user/settings/`,
{
headers: {
Cookie: cookie,
},
}
);
if (response.ok) {
const settings = (await response.json()) as UserSettings;
logger.trace("Retrieved settings for user %s", user.email);
settingsCache.set(cookie, JSON.stringify(settings));
return settings;
} else {
logger.debug(
"Fractal server replied with %d while retrieving settings from cookie",
response.status
);
return undefined;
}
}
} finally {
loadingSettings = loadingSettings.filter((c) => c !== cookie);
}
}

export class TestingBasicAuthAuthorizer implements Authorizer {
async isUserValid(req: Request): Promise<boolean> {
const authHeader = req.get("Authorization");
return !!authHeader;
}

async isUserAuthorized(completePath: string, req: Request): Promise<boolean> {
async isUserAuthorized(_: string, req: Request): Promise<boolean> {
const authHeader = req.get("Authorization")!;
const [scheme, credentials] = authHeader.split(" ");
if (scheme !== "Basic" || !credentials) {
Expand All @@ -228,12 +141,8 @@ export class TestingBasicAuthAuthorizer implements Authorizer {
const [username, password] = Buffer.from(credentials, "base64")
.toString()
.split(":");
if (
username !== config.testingUsername ||
password !== config.testingPassword
) {
return false;
}
return isSubfolder(config.zarrDataBasePath!, completePath);
return (
username === config.testingUsername && password === config.testingPassword
);
}
}
17 changes: 1 addition & 16 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ function loadConfig(): Config {
const vizarrStaticFilesPath = getRequiredEnv("VIZARR_STATIC_FILES_PATH");

const validAuthorizationSchemes = [
"fractal-server-viewer-paths",
"user-folders",
"fractal-server",
"testing-basic-auth",
"none",
];
Expand All @@ -43,16 +42,6 @@ function loadConfig(): Config {
process.exit(1);
}

let zarrDataBasePath: string | null = null;
if (authorizationScheme !== "fractal-server-viewer-paths") {
zarrDataBasePath = getRequiredEnv("ZARR_DATA_BASE_PATH");
} else if (process.env.ZARR_DATA_BASE_PATH) {
logger.error(
`ZARR_DATA_BASE_PATH will be ignored because AUTHORIZATION_SCHEME is set to fractal-server-viewer-paths`
);
process.exit(1);
}

let testingUsername: string | null = null;
let testingPassword: string | null = null;
if (authorizationScheme === "testing-basic-auth") {
Expand All @@ -72,9 +61,6 @@ function loadConfig(): Config {

logger.debug("FRACTAL_SERVER_URL: %s", fractalServerUrl);
logger.debug("BASE_PATH: %s", basePath);
if (zarrDataBasePath) {
logger.debug("ZARR_DATA_BASE_PATH: %s", zarrDataBasePath);
}
logger.debug("VIZARR_STATIC_FILES_PATH: %s", vizarrStaticFilesPath);
logger.debug("AUTHORIZATION_SCHEME: %s", authorizationScheme);
logger.debug("CACHE_EXPIRATION_TIME: %d", cacheExpirationTime);
Expand All @@ -83,7 +69,6 @@ function loadConfig(): Config {
port,
fractalServerUrl,
basePath,
zarrDataBasePath,
vizarrStaticFilesPath,
authorizationScheme: authorizationScheme as AuthorizationScheme,
cacheExpirationTime,
Expand Down
Loading

0 comments on commit 099ce1b

Please sign in to comment.