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:
@@ -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
54
src/external-jwt/cache.ts
Normal 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);
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user