Skip to content

Commit

Permalink
Add ability to share items with people outside the platform (#10663)
Browse files Browse the repository at this point in the history
* Add directus_shares

* Don't check for usage limit on refresh

* Add all endpoints to the shares controller

* Move route `/auth/shared` to `/shared/auth`

* Add password protection

* Add `share` action in permissions

* Add `shares/:pk/info`

* Start on shared-view

* Add basic styling for full shared view

* Fixed migrations

* Add inline style for shared view

* Allow title override

* Finish /info endpoint for shares

* Add basic UUID validation to share/info endpont

* Add UUID validation to other routes

* Add not found state

* Cleanup /extract/finish share login endpoint

* Cleanup auth

* Added `share_start` and `share_end`

* Add share sidebar details.

* Allow share permissions configuration

* Hide the `new_share` button for unauthorized users

* Fix uses_left displayed value

* Show expired / upcoming shares

* Improved expired/upcoming styling

* Fixed share login query

* Fix check-ip and get-permissions middlewares behaviour when role is null

* Simplify cache key

* Fix typescript linting issues

* Handle app auth flow for shared page

* Fixed /users/me response

* Show when user is authenticated

* Try showing item drawer in shared page

* Improved shared card styling

* Add shares permissions and change share card styling

* Pull in schema/permissions on share

* Create getPermissionForShare file

* Change getPermissionsForShare signature

* Render form + item on share after auth

* Finalize public front end

* Handle fake o2m field in applyQuery

* [WIP]

* New translations en-US.yaml (Bulgarian) (#10585)

* smaller label height (#10587)

* Update to the latest Material Icons (#10573)

The icons are based on https://fonts.google.com/icons

* New translations en-US.yaml (Arabic) (#10593)

* New translations en-US.yaml (Arabic) (#10594)

* New translations en-US.yaml (Portuguese, Brazilian) (#10604)

* New translations en-US.yaml (French) (#10605)

* New translations en-US.yaml (Italian) (#10613)

* fix M2A list not updating (#10617)

* Fix filters

* Add admin filter on m2o role selection

* Add admin filter on m2o role selection

* Add o2m permissions traversing

* Finish relational tree permissions generation

* Handle implicit a2o relation

* Update implicit relation regex

* Fix regex

* Fix implicitRelation unnesting for new regex

* Fix implicitRelation length check

* Rename m2a to a2o internally

* Add auto-gen permissions for a2o

* [WIP] Improve share UX

* Add ctx menu options

* Add share dialog

* Add email notifications

* Tweak endpoint

* Tweak file interface disabled state

* Add nicer invalid state to password input

* Dont return info for expired/upcoming shares

* Tweak disabled state for relational interfaces

* Fix share button for non admin roles

* Show/hide edit/delete based on permissions to shares

* Fix imports of mutationtype

* Resolve (my own) suggestions

* Fix migration for ms sql

* Resolve last suggestion

Co-authored-by: Oreilles <oreilles.github@nitoref.io>
Co-authored-by: Oreilles <33065839+oreilles@users.noreply.github.com>
Co-authored-by: Ben Haynes <ben@rngr.org>
Co-authored-by: Thien Nguyen <72242664+tatthien@users.noreply.github.com>
Co-authored-by: Azri Kahar <42867097+azrikahar@users.noreply.github.com>
  • Loading branch information
6 people committed Dec 23, 2021
1 parent d947c4f commit dbf35a1
Show file tree
Hide file tree
Showing 89 changed files with 2,423 additions and 377 deletions.
8 changes: 5 additions & 3 deletions api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import settingsRouter from './controllers/settings';
import usersRouter from './controllers/users';
import utilsRouter from './controllers/utils';
import webhooksRouter from './controllers/webhooks';
import sharesRouter from './controllers/shares';
import { isInstalled, validateDatabaseConnection, validateDatabaseExtensions, validateMigrations } from './database';
import emitter from './emitter';
import env from './env';
Expand Down Expand Up @@ -112,7 +113,7 @@ export default async function createApp(): Promise<express.Application> {

app.use(extractToken);

app.use((req, res, next) => {
app.use((_req, res, next) => {
res.setHeader('X-Powered-By', 'Directus');
next();
});
Expand All @@ -121,7 +122,7 @@ export default async function createApp(): Promise<express.Application> {
app.use(cors);
}

app.get('/', (req, res, next) => {
app.get('/', (_req, res, next) => {
if (env.ROOT_REDIRECT) {
res.redirect(env.ROOT_REDIRECT);
} else {
Expand All @@ -137,7 +138,7 @@ export default async function createApp(): Promise<express.Application> {
const html = await fse.readFile(adminPath, 'utf8');
const htmlWithBase = html.replace(/<base \/>/, `<base href="${adminUrl.toString({ rootRelative: true })}/" />`);

const noCacheIndexHtmlHandler = (req: Request, res: Response) => {
const noCacheIndexHtmlHandler = (_req: Request, res: Response) => {
res.setHeader('Cache-Control', 'no-cache');
res.send(htmlWithBase);
};
Expand Down Expand Up @@ -190,6 +191,7 @@ export default async function createApp(): Promise<express.Application> {
app.use('/roles', rolesRouter);
app.use('/server', serverRouter);
app.use('/settings', settingsRouter);
app.use('/shares', sharesRouter);
app.use('/users', usersRouter);
app.use('/utils', utilsRouter);
app.use('/webhooks', webhooksRouter);
Expand Down
18 changes: 7 additions & 11 deletions api/src/auth/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Knex } from 'knex';
import { AuthDriverOptions, SchemaOverview, User, SessionData } from '../types';
import { AuthDriverOptions, SchemaOverview, User } from '../types';

export abstract class AuthDriver {
knex: Knex;
Expand Down Expand Up @@ -36,30 +36,26 @@ export abstract class AuthDriver {
* @throws InvalidCredentialsException
* @returns Data to be stored with the session
*/
async login(_user: User, _payload: Record<string, any>): Promise<SessionData> {
/* Optional, though should probably be set */
return null;
async login(_user: User, _payload: Record<string, any>): Promise<void> {
return;
}

/**
* Handle user session refresh
*
* @param _user User information
* @param _sessionData Session data
* @throws InvalidCredentialsException
*/
async refresh(_user: User, sessionData: SessionData): Promise<SessionData> {
/* Optional */
return sessionData;
async refresh(_user: User): Promise<void> {
return;
}

/**
* Handle user session termination
*
* @param _user User information
* @param _sessionData Session data
*/
async logout(_user: User, _sessionData: SessionData): Promise<void> {
/* Optional */
async logout(_user: User): Promise<void> {
return;
}
}
8 changes: 3 additions & 5 deletions api/src/auth/drivers/ldap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import ldap, {
import ms from 'ms';
import Joi from 'joi';
import { AuthDriver } from '../auth';
import { AuthDriverOptions, User, SessionData } from '../../types';
import { AuthDriverOptions, User } from '../../types';
import {
InvalidCredentialsException,
InvalidPayloadException,
Expand Down Expand Up @@ -318,20 +318,18 @@ export class LDAPAuthDriver extends AuthDriver {
});
}

async login(user: User, payload: Record<string, any>): Promise<SessionData> {
async login(user: User, payload: Record<string, any>): Promise<void> {
await this.verify(user, payload.password);
return null;
}

async refresh(user: User): Promise<SessionData> {
async refresh(user: User): Promise<void> {
await this.validateBindClient();

const userInfo = await this.fetchUserInfo(user.external_identifier!);

if (userInfo?.userAccountControl && userInfo.userAccountControl & INVALID_ACCOUNT_FLAGS) {
throw new InvalidCredentialsException();
}
return null;
}
}

Expand Down
19 changes: 6 additions & 13 deletions api/src/auth/drivers/local.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Router } from 'express';
import argon2 from 'argon2';
import ms from 'ms';
import Joi from 'joi';
import { AuthDriver } from '../auth';
import { User, SessionData } from '../../types';
import { User } from '../../types';
import { InvalidCredentialsException, InvalidPayloadException } from '../../exceptions';
import { AuthenticationService } from '../../services';
import asyncHandler from '../../utils/async-handler';
import env from '../../env';
import { respond } from '../../middleware/respond';
import { COOKIE_OPTIONS } from '../../constants';

export class LocalAuthDriver extends AuthDriver {
async getUserID(payload: Record<string, any>): Promise<string> {
Expand All @@ -35,16 +35,15 @@ export class LocalAuthDriver extends AuthDriver {
}
}

async login(user: User, payload: Record<string, any>): Promise<SessionData> {
async login(user: User, payload: Record<string, any>): Promise<void> {
await this.verify(user, payload.password);
return null;
}
}

export function createLocalAuthRouter(provider: string): Router {
const router = Router();

const loginSchema = Joi.object({
const userLoginSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().required(),
mode: Joi.string().valid('cookie', 'json'),
Expand All @@ -65,7 +64,7 @@ export function createLocalAuthRouter(provider: string): Router {
schema: req.schema,
});

const { error } = loginSchema.validate(req.body);
const { error } = userLoginSchema.validate(req.body);

if (error) {
throw new InvalidPayloadException(error.message);
Expand All @@ -88,13 +87,7 @@ export function createLocalAuthRouter(provider: string): Router {
}

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

res.locals.payload = payload;
Expand Down
10 changes: 4 additions & 6 deletions api/src/auth/drivers/oauth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { LocalAuthDriver } from './local';
import { getAuthProvider } from '../../auth';
import env from '../../env';
import { AuthenticationService, UsersService } from '../../services';
import { AuthDriverOptions, User, AuthData, SessionData } from '../../types';
import { AuthDriverOptions, User, AuthData } from '../../types';
import {
InvalidCredentialsException,
ServiceUnavailableException,
Expand Down Expand Up @@ -159,11 +159,11 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
return (await this.fetchUserId(identifier)) as string;
}

async login(user: User): Promise<SessionData> {
return this.refresh(user, null);
async login(user: User): Promise<void> {
return this.refresh(user);
}

async refresh(user: User, sessionData: SessionData): Promise<SessionData> {
async refresh(user: User): Promise<void> {
let authData = user.auth_data as AuthData;

if (typeof authData === 'string') {
Expand All @@ -187,8 +187,6 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
throw handleError(e);
}
}

return sessionData;
}
}

Expand Down
10 changes: 4 additions & 6 deletions api/src/auth/drivers/openid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { LocalAuthDriver } from './local';
import { getAuthProvider } from '../../auth';
import env from '../../env';
import { AuthenticationService, UsersService } from '../../services';
import { AuthDriverOptions, User, AuthData, SessionData } from '../../types';
import { AuthDriverOptions, User, AuthData } from '../../types';
import {
InvalidCredentialsException,
ServiceUnavailableException,
Expand Down Expand Up @@ -167,11 +167,11 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
return (await this.fetchUserId(identifier)) as string;
}

async login(user: User): Promise<SessionData> {
return this.refresh(user, null);
async login(user: User): Promise<void> {
return this.refresh(user);
}

async refresh(user: User, sessionData: SessionData): Promise<SessionData> {
async refresh(user: User): Promise<void> {
let authData = user.auth_data as AuthData;

if (typeof authData === 'string') {
Expand All @@ -196,8 +196,6 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
throw handleError(e);
}
}

return sessionData;
}
}

Expand Down
2 changes: 1 addition & 1 deletion api/src/cli/commands/schema/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export async function apply(snapshotPath: string, options?: { yes: boolean }): P
continue;
}

// Related collection doesn't exist for m2a relationship types
// Related collection doesn't exist for a2o relationship types
if (related_collection) {
message += `-> ${related_collection}`;
}
Expand Down
14 changes: 13 additions & 1 deletion api/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { TransformationParams } from './types';
import env from './env';
import ms from 'ms';

export const SYSTEM_ASSET_ALLOW_LIST: TransformationParams[] = [
{
Expand Down Expand Up @@ -40,8 +42,18 @@ export const ASSET_TRANSFORM_QUERY_KEYS = [

export const FILTER_VARIABLES = ['$NOW', '$CURRENT_USER', '$CURRENT_ROLE'];

export const ALIAS_TYPES = ['alias', 'o2m', 'm2m', 'm2a', 'files', 'translations'];
export const ALIAS_TYPES = ['alias', 'o2m', 'm2m', 'm2a', 'o2a', 'files', 'translations'];

export const DEFAULT_AUTH_PROVIDER = 'default';

export const COLUMN_TRANSFORMS = ['year', 'month', 'day', 'weekday', 'hour', 'minute', 'second'];

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 = {
httpOnly: true,
domain: env.REFRESH_TOKEN_COOKIE_DOMAIN,
maxAge: ms(env.REFRESH_TOKEN_TTL as string),
secure: env.REFRESH_TOKEN_COOKIE_SECURE ?? false,
sameSite: (env.REFRESH_TOKEN_COOKIE_SAME_SITE as 'lax' | 'strict' | 'none') || 'strict',
};
1 change: 1 addition & 0 deletions api/src/controllers/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const readHandler = asyncHandler(async (req, res, next) => {
accountability: req.accountability,
schema: req.schema,
});

const metaService = new MetaService({
accountability: req.accountability,
schema: req.schema,
Expand Down

0 comments on commit dbf35a1

Please sign in to comment.