feat: add cache (#2)

* ci: remove main workflow
ci: add pr workflow to branch next

* docs: correct readme
ci: add docker build on release
ci: add lint and test to release flow

* feat: add cache for both memory and redis
refactor: cleanup getAccountability nested promise
refactor: import path for get-auth-providers.ts
docs: document cache options
ci: add redis file to gitignore

* ci: use test:coverage for testing to update pr

---------

Co-authored-by: Krise <krise86@users.noreply.github.com>
This commit is contained in:
Kristoffer
2023-08-02 16:44:07 +02:00
committed by GitHub
parent aea5b84c41
commit 8f8ce1e02b
12 changed files with 284 additions and 97 deletions

View File

@@ -3,7 +3,7 @@ import {JwksClient} from 'jwks-rsa';
import { Issuer } from 'openid-client';
import env from '../config/config.js';
import env from '../config/config';
import { createError } from '@directus/errors';
const InvalidJWKIssuerMetadata = createError('INVALID_JWKS_ISSUER_ERROR', 'No JWKS_URL or JWKS_KEYS and could not discover JWKS_URL from openid metadata', 500);

54
src/external-jwt/cache.ts Normal file
View File

@@ -0,0 +1,54 @@
import {default as Keyv, Store} from 'keyv';
import env from './config/config';
import {default as KeyvRedis} from '@keyv/redis';
// check if redis is defined
const cache: Keyv | null = getCache();
function getCache(): Keyv | null {
if(env['CACHE_ENABLED'] !== true) return null;
// check namespace
let namespace = env['CACHE_JWT_NAMESPACE'];
if(namespace == null || namespace === '') {
namespace = 'exjwt';
}
let ttl = env['CACHE_JWT_TTL'];
if (ttl == null || ttl === '') {
ttl = 5000
}
let uri = '';
let store: Store<string | undefined> | undefined = undefined;
if(env['CACHE_STORE'] === 'redis') {
uri = env['REDIS']
if(uri == null || uri === '') {
uri = `redis://${env['REDIS_USERNAME']}:${env['REDIS_PASSWORD']}@${env['REDIS_HOST']}:${env['REDIS_PORT']}`;
}
store = new KeyvRedis(uri);
}
return new Keyv(uri, {
namespace: namespace,
ttl,
store: store
});
}
export function CacheEnabled(): boolean {
return cache !== null;
}
export async function CacheSet(key: string, value: any) {
if(cache === null) return false;
return cache.set(key, value);
}
export async function CacheGet(key: string) {
if(cache === null) return null;
return cache.get(key);
}

View File

@@ -25,13 +25,14 @@ const allowedEnvironmentVars = [
'CACHE_VALUE_MAX_SIZE',
'CACHE_SKIP_ALLOWED',
'CACHE_HEALTHCHECK_THRESHOLD',
// Externl JWT Cache
'CACHE_JWT_NAMESPACE',
// redis
'REDIS',
'REDIS_HOST',
'REDIS_PORT',
'REDIS_USERNAME',
'REDIS_PASSWORD',
'REDIS_JWT_DB',
// auth
'AUTH_PROVIDERS',
'AUTH_.+_DRIVER',

View File

@@ -2,6 +2,7 @@ import type { Accountability } from '@directus/types';
import { getAuthProviders } from './authProvider/get-auth-providers.js';
import type { Knex } from 'knex';
import { verify_token } from './verify-token.js';
import { CacheEnabled, CacheGet, CacheSet } from './cache.js';
@@ -22,7 +23,7 @@ export async function getAccountabilityForToken(
accountability: Accountability | null,
database: Knex
): Promise<Accountability> {
if (!accountability) {
if (accountability == null) {
accountability = {
user: null,
role: null,
@@ -32,80 +33,92 @@ export async function getAccountabilityForToken(
}
if (token == null || iss == null) {
return accountability
}
const providers = authProviders.filter((provider) => provider.issuer_url && iss.includes(provider.issuer_url));
if(providers.length === 0) return accountability;
if(providers.length > 1) {
return accountability;
}
return new Promise((resolve, reject) => {
const providers = authProviders.filter((provider) => provider && iss.includes(provider.client_id));
if(providers.length === 0) return accountability;
if(providers.length > 1) {
console.log("to many matching providers");
return accountability;
}
const provider = providers[0];
const provider = providers[0];
try {
const result = await verify_token(provider, token)
verify_token(provider, token).then(async (result) => {
if(accountability) {
// check if role key is set else try role key
if(provider.role_key != null) {
accountability.role = typeof result[provider.role_key] === 'string' ? result[provider.role_key] : result[provider.role_key][0];
} else {
if (result.role) {
accountability.role = result.role;
}
}
if(provider.use_database) { // use database to get user
// TODO: Add caching to this function
const user = await database
.select('directus_users.id', 'directus_users.role', 'directus_roles.admin_access', 'directus_roles.app_access')
.from('directus_users')
.leftJoin('directus_roles', 'directus_users.role', 'directus_roles.id')
.where({
'directus_users.external_identifier': result.sub,
'directus_users.provider': provider.name,
})
.first();
if(!user) {
reject("invalid user credentials");
}
accountability.user = user.id;
accountability.role = user.role;
accountability.admin = user.admin_access === true || user.admin_access == 1;
accountability.app = user.app_access === true || user.app_access == 1;
} else {
if(provider.admin_key != null) {
accountability.admin = result[provider.admin_key];
}
if(provider.app_key != null) {
accountability.app = result[provider.app_key];
}
accountability.user = result.sub;
}
if(provider.use_database) { // use database to get user
// TODO: Add caching to this function
if (CacheEnabled() && result.sub) {
console.log(accountability);
resolve(accountability);
const cachedAccountability = await CacheGet(result.sub);
if (cachedAccountability) {
return cachedAccountability;
}
}
reject("no accountability");
const user = await database
.select('directus_users.id', 'directus_users.role', 'directus_roles.admin_access', 'directus_roles.app_access')
.from('directus_users')
.leftJoin('directus_roles', 'directus_users.role', 'directus_roles.id')
.where({
'directus_users.external_identifier': result.sub,
'directus_users.provider': provider.name,
})
.first();
if(!user) {
return accountability;
}
accountability.user = user.id;
accountability.role = user.role;
accountability.admin = user.admin_access === true || user.admin_access == 1;
accountability.app = user.app_access === true || user.app_access == 1;
if (CacheEnabled() && result.sub) {
CacheSet(result.sub, accountability);
}
})
return accountability;
}
// check if role key is set else try role key
if(provider.role_key != null) {
if(typeof result[provider.role_key] === 'string') {
accountability.role = result[provider.role_key];
}
if(typeof result[provider.role_key] === 'object') {
accountability.role = ''
}
if(result[provider.role_key].instanceOf(Array)) {
accountability.role = result[provider.role_key][0];
}
}
});
if(provider.admin_key != null) {
accountability.admin = result[provider.admin_key];
}
if(provider.app_key != null) {
accountability.app = result[provider.app_key];
}
accountability.user = result.sub;
} catch (error) {
return accountability;
}
return accountability;
}