Skip to content

Commit

Permalink
Merge branch 'enhancements-6'
Browse files Browse the repository at this point in the history
  • Loading branch information
t7tran committed Feb 16, 2024
2 parents bad22bf + 1e8c725 commit 10aa51e
Show file tree
Hide file tree
Showing 18 changed files with 137 additions and 96 deletions.
9 changes: 0 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ on:

env:
GHCR_IMAGE: ghcr.io/${{ github.repository }}
DOCKERHUB_IMAGE: ${{ github.repository }}

jobs:
check-version:
Expand Down Expand Up @@ -92,21 +91,13 @@ jobs:
uses: docker/metadata-action@v5
with:
images: |
${{ env.DOCKERHUB_IMAGE }}
${{ env.GHCR_IMAGE }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=canary,enable=${{ needs.check-version.outputs.prerelease }}
- name: Login to Docker Hub
uses: docker/login-action@v3
if: env.DOCKERHUB_IMAGE
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}

- name: Login to GHCR
uses: docker/login-action@v3
if: env.GHCR_IMAGE
Expand Down
24 changes: 0 additions & 24 deletions .github/workflows/sync-dockerhub-readme.yml

This file was deleted.

14 changes: 6 additions & 8 deletions api/src/auth/drivers/ldap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import { UsersService } from '../../services/users.js';
import type { AuthDriverOptions, User } from '../../types/index.js';
import asyncHandler from '../../utils/async-handler.js';
import { getIPFromReq } from '../../utils/get-ip-from-req.js';
import { getMilliseconds } from '../../utils/get-milliseconds.js';
import { AuthDriver } from '../auth.js';
import { COOKIE_OPTIONS } from '../../constants.js';

interface UserInfo {
dn: string;
Expand Down Expand Up @@ -455,14 +455,12 @@ export function createLDAPAuthRouter(provider: string): Router {
payload['data']!['refresh_token'] = refreshToken;
}

if (env['ACCESS_TOKEN_COOKIE_NAME']) {
res.cookie(env['ACCESS_TOKEN_COOKIE_NAME'] as string, accessToken, COOKIE_OPTIONS.accessToken);
}

if (mode === 'cookie') {
res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, refreshToken, {
httpOnly: true,
domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'] as string,
maxAge: getMilliseconds(env['REFRESH_TOKEN_TTL']),
secure: (env['REFRESH_TOKEN_COOKIE_SECURE'] as boolean) ?? false,
sameSite: (env['REFRESH_TOKEN_COOKIE_SAME_SITE'] as 'lax' | 'strict' | 'none') || 'strict',
});
res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, refreshToken, COOKIE_OPTIONS.refreshToken);
}

res.locals['payload'] = payload;
Expand Down
6 changes: 5 additions & 1 deletion api/src/auth/drivers/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,12 @@ export function createLocalAuthRouter(provider: string): Router {
payload['data']!['refresh_token'] = refreshToken;
}

if (env['ACCESS_TOKEN_COOKIE_NAME']) {
res.cookie(env['ACCESS_TOKEN_COOKIE_NAME'] as string, accessToken, COOKIE_OPTIONS.accessToken);
}

if (mode === 'cookie') {
res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, refreshToken, COOKIE_OPTIONS);
res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, refreshToken, COOKIE_OPTIONS.refreshToken);
}

res.locals['payload'] = payload;
Expand Down
14 changes: 6 additions & 8 deletions api/src/auth/drivers/oauth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ import type { AuthData, AuthDriverOptions, User } from '../../types/index.js';
import asyncHandler from '../../utils/async-handler.js';
import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
import { getIPFromReq } from '../../utils/get-ip-from-req.js';
import { getMilliseconds } from '../../utils/get-milliseconds.js';
import { Url } from '../../utils/url.js';
import { LocalAuthDriver } from './local.js';
import { COOKIE_OPTIONS } from '../../constants.js';

export class OAuth2AuthDriver extends LocalAuthDriver {
client: Client;
Expand Down Expand Up @@ -399,14 +399,12 @@ export function createOAuth2AuthRouter(providerName: string): Router {

const { accessToken, refreshToken, expires } = authResponse;

if (env['ACCESS_TOKEN_COOKIE_NAME']) {
res.cookie(env['ACCESS_TOKEN_COOKIE_NAME'] as string, accessToken, COOKIE_OPTIONS.accessToken);
}

if (redirect) {
res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, refreshToken, {
httpOnly: true,
domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'] as string,
maxAge: getMilliseconds(env['REFRESH_TOKEN_TTL']),
secure: (env['REFRESH_TOKEN_COOKIE_SECURE'] as boolean) ?? false,
sameSite: (env['REFRESH_TOKEN_COOKIE_SAME_SITE'] as 'lax' | 'strict' | 'none') || 'strict',
});
res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, refreshToken, COOKIE_OPTIONS.refreshToken);

return res.redirect(redirect);
}
Expand Down
14 changes: 6 additions & 8 deletions api/src/auth/drivers/openid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ import type { AuthData, AuthDriverOptions, User } from '../../types/index.js';
import asyncHandler from '../../utils/async-handler.js';
import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
import { getIPFromReq } from '../../utils/get-ip-from-req.js';
import { getMilliseconds } from '../../utils/get-milliseconds.js';
import { Url } from '../../utils/url.js';
import { LocalAuthDriver } from './local.js';
import { COOKIE_OPTIONS } from '../../constants.js';

export class OpenIDAuthDriver extends LocalAuthDriver {
client: Promise<Client>;
Expand Down Expand Up @@ -433,14 +433,12 @@ export function createOpenIDAuthRouter(providerName: string): Router {

const { accessToken, refreshToken, expires } = authResponse;

if (env['ACCESS_TOKEN_COOKIE_NAME']) {
res.cookie(env['ACCESS_TOKEN_COOKIE_NAME'] as string, accessToken, COOKIE_OPTIONS.accessToken);
}

if (redirect) {
res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, refreshToken, {
httpOnly: true,
domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'] as string,
maxAge: getMilliseconds(env['REFRESH_TOKEN_TTL']),
secure: (env['REFRESH_TOKEN_COOKIE_SECURE'] as boolean) ?? false,
sameSite: (env['REFRESH_TOKEN_COOKIE_SAME_SITE'] as 'lax' | 'strict' | 'none') || 'strict',
});
res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, refreshToken, COOKIE_OPTIONS.refreshToken);

return res.redirect(redirect);
}
Expand Down
9 changes: 7 additions & 2 deletions api/src/auth/drivers/saml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,14 @@ export function createSAMLAuthRouter(providerName: string) {

if (currentRefreshToken) {
await authService.logout(currentRefreshToken);
res.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, COOKIE_OPTIONS);
res.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, COOKIE_OPTIONS.refreshToken);
}
}

if (req.cookies[env['ACCESS_TOKEN_COOKIE_NAME'] as string]) {
res.clearCookie(env['ACCESS_TOKEN_COOKIE_NAME'] as string, COOKIE_OPTIONS.accessToken);
}

return res.redirect(context);
}),
);
Expand Down Expand Up @@ -172,8 +176,9 @@ export function createSAMLAuthRouter(providerName: string) {
},
};

// set cookie for ACCESS_TOKEN_COOKIE_NAME here?
if (relayState) {
res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, refreshToken, COOKIE_OPTIONS);
res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, refreshToken, COOKIE_OPTIONS.refreshToken);
return res.redirect(relayState);
}

Expand Down
25 changes: 17 additions & 8 deletions api/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEnv } from '@directus/env';
import type { CookieOptions } from 'express';
import type { CookieOptions } from 'express-serve-static-core';
import type { TransformationParams } from './types/index.js';
import { getMilliseconds } from './utils/get-milliseconds.js';

Expand Down Expand Up @@ -63,13 +63,22 @@ export const GENERATE_SPECIAL = ['uuid', 'date-created', 'role-created', 'user-c

export const UUID_REGEX = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';

export const COOKIE_OPTIONS: CookieOptions = {
httpOnly: true,
domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'] as string,
maxAge: getMilliseconds(env['REFRESH_TOKEN_TTL']),
secure: (env['REFRESH_TOKEN_COOKIE_SECURE'] as boolean) ?? false,
sameSite: (env['REFRESH_TOKEN_COOKIE_SAME_SITE'] as 'lax' | 'strict' | 'none') || 'strict',
};
export const COOKIE_OPTIONS = {
accessToken: {
httpOnly: true,
domain: env['ACCESS_TOKEN_COOKIE_DOMAIN'] as string,
maxAge: getMilliseconds(env['ACCESS_TOKEN_TTL']),
secure: (env['ACCESS_TOKEN_COOKIE_SECURE'] as boolean) ?? false,
sameSite: (env['ACCESS_TOKEN_COOKIE_SAME_SITE'] as 'lax' | 'strict' | 'none') || 'strict',
} as CookieOptions,
refreshToken: {
httpOnly: true,
domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'] as string,
maxAge: getMilliseconds(env['REFRESH_TOKEN_TTL']),
secure: (env['REFRESH_TOKEN_COOKIE_SECURE'] as boolean) ?? false,
sameSite: (env['REFRESH_TOKEN_COOKIE_SAME_SITE'] as 'lax' | 'strict' | 'none') || 'strict',
} as CookieOptions,
} as const;

export const OAS_REQUIRED_SCHEMAS = ['Query', 'x-metadata'];

Expand Down
17 changes: 10 additions & 7 deletions api/src/controllers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,12 @@ router.post(
payload['data']!['refresh_token'] = refreshToken;
}

if (env['ACCESS_TOKEN_COOKIE_NAME']) {
res.cookie(env['ACCESS_TOKEN_COOKIE_NAME'] as string, accessToken, COOKIE_OPTIONS.accessToken);
}

if (mode === 'cookie') {
res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, refreshToken, COOKIE_OPTIONS);
res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, refreshToken, COOKIE_OPTIONS.refreshToken);
}

res.locals['payload'] = payload;
Expand Down Expand Up @@ -136,12 +140,11 @@ router.post(
await authenticationService.logout(currentRefreshToken);

if (req.cookies[env['REFRESH_TOKEN_COOKIE_NAME'] as string]) {
res.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, {
httpOnly: true,
domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'] as string,
secure: (env['REFRESH_TOKEN_COOKIE_SECURE'] as boolean) ?? false,
sameSite: (env['REFRESH_TOKEN_COOKIE_SAME_SITE'] as 'lax' | 'strict' | 'none') || 'strict',
});
res.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, COOKIE_OPTIONS.refreshToken);
}

if (req.cookies[env['ACCESS_TOKEN_COOKIE_NAME'] as string]) {
res.clearCookie(env['ACCESS_TOKEN_COOKIE_NAME'] as string, COOKIE_OPTIONS.accessToken);
}

return next();
Expand Down
36 changes: 36 additions & 0 deletions api/src/controllers/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { respond } from '../middleware/respond.js';
import useCollection from '../middleware/use-collection.js';
import { validateBatch } from '../middleware/validate-batch.js';
import { FilesService } from '../services/files.js';
import { FoldersService } from '../services/folders.js';
import { MetaService } from '../services/meta.js';
import type { PrimaryKey } from '../types/index.js';
import asyncHandler from '../utils/async-handler.js';
Expand Down Expand Up @@ -97,6 +98,41 @@ export const multipartHandler: RequestHandler = (req, res, next) => {

payload.filename_download = filename;

payload.folder = payload.folder || req.query['folder'];

if (payload.folder && !/^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(payload.folder)) {
const foldersService = new FoldersService({ accountability: req.accountability, schema: req.schema });
const path: string = payload.folder;
const parts = path.split('/').filter((f) => !!f);

if (parts.length === 0) {
return busboy.emit('error', new InvalidPayloadError({ reason: `Invalid folder ${path}` }));
}

let folderId: string | undefined;
let parentFilter: any = { parent: { _null: true } };

for (const part of parts) {
folderId = (
await foldersService.readByQuery({
fields: ['id'],
filter: {
_and: [{ name: { _eq: part } }, parentFilter],
},
limit: 1,
})
)[0]?.['id'];

if (!folderId) {
return busboy.emit('error', new InvalidPayloadError({ reason: `Invalid folder ${path}` }));
}

parentFilter = { parent: { _eq: folderId } };
}

payload.folder = folderId;
}

const payloadWithRequiredFields = {
...payload,
type: mimeType,
Expand Down
6 changes: 5 additions & 1 deletion api/src/controllers/shares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ router.post(

const { accessToken, refreshToken, expires } = await service.login(req.body);

res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, refreshToken, COOKIE_OPTIONS);
if (env['ACCESS_TOKEN_COOKIE_NAME']) {
res.cookie(env['ACCESS_TOKEN_COOKIE_NAME'] as string, accessToken, COOKIE_OPTIONS.accessToken);
}

res.cookie(env['REFRESH_TOKEN_COOKIE_NAME'] as string, refreshToken, COOKIE_OPTIONS.refreshToken);

res.locals['payload'] = { data: { access_token: accessToken, expires } };

Expand Down
8 changes: 8 additions & 0 deletions api/src/middleware/extract-token.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { useEnv } from '@directus/env';

/**
* Extract access token from:
*
* Authorization: Bearer
* access_token query parameter
* env.ACCESS_TOKEN_COOKIE_NAME cookie
*
* and store in req.token
*/
Expand All @@ -11,6 +14,7 @@ import type { RequestHandler } from 'express';

const extractToken: RequestHandler = (req, _res, next) => {
let token: string | null = null;
const env = useEnv();

if (req.query && req.query['access_token']) {
token = req.query['access_token'] as string;
Expand All @@ -24,6 +28,10 @@ const extractToken: RequestHandler = (req, _res, next) => {
}
}

if (!token && env['ACCESS_TOKEN_COOKIE_NAME']) {
token = req.cookies[env['ACCESS_TOKEN_COOKIE_NAME'] as string];
}

/**
* @TODO
* Look into RFC6750 compliance:
Expand Down
5 changes: 3 additions & 2 deletions api/src/services/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,11 @@ export class FilesService extends ItemsService {

// Is this file a replacement? if the file data already exists and we have a primary key
const isReplacement = existingFile !== null && primaryKey !== undefined;
const emitFilterOpts: MutationOptions & Record<string, any> = { emitEvents: false, emitFilters: true };

// If this is a new file upload, we need to generate a new primary key and DB record
if (isReplacement === false || primaryKey === undefined) {
primaryKey = await this.createOne(payload, { emitEvents: false });
primaryKey = await this.createOne(payload, emitFilterOpts);
}

const fileExtension =
Expand Down Expand Up @@ -201,7 +202,7 @@ export class FilesService extends ItemsService {
schema: this.schema,
});

await sudoService.updateOne(primaryKey, payload, { emitEvents: false });
await sudoService.updateOne(primaryKey, payload, emitFilterOpts);

if (opts?.emitEvents !== false) {
emitter.emitAction(
Expand Down
Loading

0 comments on commit 10aa51e

Please sign in to comment.