diff --git a/backend/src/controllers/v2/apiKeyDataController.ts b/backend/src/controllers/v2/apiKeyDataController.ts index fd87f7306..e4450ae90 100644 --- a/backend/src/controllers/v2/apiKeyDataController.ts +++ b/backend/src/controllers/v2/apiKeyDataController.ts @@ -50,6 +50,7 @@ export const createAPIKeyData = async (req: Request, res: Response) => { apiKeyData = await new APIKeyData({ name, + lastUsed: new Date(), expiresAt, user: req.user._id, secretHash diff --git a/backend/src/controllers/v2/serviceAccountsController.ts b/backend/src/controllers/v2/serviceAccountsController.ts index 38da62bc8..7eaf73cea 100644 --- a/backend/src/controllers/v2/serviceAccountsController.ts +++ b/backend/src/controllers/v2/serviceAccountsController.ts @@ -80,6 +80,7 @@ export const createServiceAccount = async (req: Request, res: Response) => { organization: new Types.ObjectId(organizationId), user: req.user, publicKey, + lastUsed: new Date(), expiresAt, secretHash }).save(); diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index 4b42bd373..6b3d24dfb 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -94,6 +94,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => { environment, user, serviceAccount, + lastUsed: new Date(), expiresAt, secretHash, encryptedKey, @@ -102,7 +103,6 @@ export const createServiceTokenData = async (req: Request, res: Response) => { permissions }).save(); - // return service token data without sensitive data serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id); @@ -123,25 +123,11 @@ export const createServiceTokenData = async (req: Request, res: Response) => { * @returns */ export const deleteServiceTokenData = async (req: Request, res: Response) => { - let serviceTokenData; - try { - const { serviceTokenDataId } = req.params; + const { serviceTokenDataId } = req.params; - serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to delete service token data' - }); - } + const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); return res.status(200).send({ serviceTokenData }); -} - -function UnauthorizedRequestError(arg0: { message: string; }) { - throw new Error('Function not implemented.'); } \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index ea9bb7dab..5a18ae08e 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -418,7 +418,7 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { .skip(offset) .limit(limit) .populate('actions') - .populate('user'); + .populate('user serviceAccount serviceTokenData'); } catch (err) { Sentry.setUser({ email: req.user.email }); diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index 58d338578..c2c21ded5 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/node'; +import { Types } from 'mongoose'; import jwt from 'jsonwebtoken'; import bcrypt from 'bcrypt'; import { @@ -102,25 +103,17 @@ const getAuthUserPayload = async ({ }: { authTokenValue: string; }) => { - let user; - try { - const decodedToken = ( - jwt.verify(authTokenValue, getJwtAuthSecret()) - ); + const decodedToken = ( + jwt.verify(authTokenValue, getJwtAuthSecret()) + ); - user = await User.findOne({ - _id: decodedToken.userId - }).select('+publicKey'); + const user = await User.findOne({ + _id: decodedToken.userId + }).select('+publicKey'); - if (!user) throw AccountNotFoundError({ message: 'Failed to find User' }); + if (!user) throw AccountNotFoundError({ message: 'Failed to find User' }); - if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' }); - - } catch (err) { - throw UnauthorizedRequestError({ - message: 'Failed to authenticate JWT token' - }); - } + if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' }); return user; } @@ -136,41 +129,38 @@ const getAuthSTDPayload = async ({ }: { authTokenValue: string; }) => { - let serviceTokenData; - try { - const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3); + const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3); - // TODO: optimize double query - serviceTokenData = await ServiceTokenData - .findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt'); + let serviceTokenData = await ServiceTokenData + .findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt'); - if (!serviceTokenData) { - throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); - } else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) { - // case: service token expired - await ServiceTokenData.findByIdAndDelete(serviceTokenData._id); - throw UnauthorizedRequestError({ - message: 'Failed to authenticate expired service token' - }); - } - - const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash); - if (!isMatch) throw UnauthorizedRequestError({ - message: 'Failed to authenticate service token' - }); - - serviceTokenData = await ServiceTokenData - .findById(TOKEN_IDENTIFIER) - .select('+encryptedKey +iv +tag'); - - if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); - - } catch (err) { + if (!serviceTokenData) { + throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); + } else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) { + // case: service token expired + await ServiceTokenData.findByIdAndDelete(serviceTokenData._id); throw UnauthorizedRequestError({ - message: 'Failed to authenticate service token' + message: 'Failed to authenticate expired service token' }); } + const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash); + if (!isMatch) throw UnauthorizedRequestError({ + message: 'Failed to authenticate service token' + }); + + serviceTokenData = await ServiceTokenData + .findOneAndUpdate({ + _id: new Types.ObjectId(TOKEN_IDENTIFIER) + }, { + lastUsed: new Date() + }, { + new: true + }) + .select('+encryptedKey +iv +tag'); + + if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); + return serviceTokenData; } @@ -204,7 +194,6 @@ const getAuthSAAKPayload = async ({ } /** - * TODO: deprecate API keys * Return API key data payload corresponding to API key [authTokenValue] * @param {Object} obj * @param {String} obj.authTokenValue - API key value @@ -215,33 +204,44 @@ const getAuthAPIKeyPayload = async ({ }: { authTokenValue: string; }) => { - let user; - try { - const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3); + const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3); - const apiKeyData = await APIKeyData - .findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt') - .populate<{user: IUser}>('user', '+publicKey'); + let apiKeyData = await APIKeyData + .findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt') + .populate<{user: IUser}>('user', '+publicKey'); - if (!apiKeyData) { - throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' }); - } else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) { - // case: API key expired - await APIKeyData.findByIdAndDelete(apiKeyData._id); - throw UnauthorizedRequestError({ - message: 'Failed to authenticate expired API key' - }); - } - - const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash); - if (!isMatch) throw UnauthorizedRequestError({ - message: 'Failed to authenticate API key' - }); - - user = apiKeyData.user; - } catch (err) { + if (!apiKeyData) { + throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' }); + } else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) { + // case: API key expired + await APIKeyData.findByIdAndDelete(apiKeyData._id); throw UnauthorizedRequestError({ - message: 'Failed to authenticate API key' + message: 'Failed to authenticate expired API key' + }); + } + + const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash); + if (!isMatch) throw UnauthorizedRequestError({ + message: 'Failed to authenticate API key' + }); + + apiKeyData = await APIKeyData.findOneAndUpdate({ + _id: new Types.ObjectId(TOKEN_IDENTIFIER) + }, { + lastUsed: new Date() + }, { + new: true + }); + + if (!apiKeyData) { + throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' }); + } + + const user = await User.findById(apiKeyData.user).select('+publicKey'); + + if (!user) { + throw AccountNotFoundError({ + message: 'Failed to find user' }); } @@ -257,30 +257,23 @@ const getAuthAPIKeyPayload = async ({ * @return {String} obj.refreshToken - issued refresh token */ const issueAuthTokens = async ({ userId }: { userId: string }) => { - let token: string; - let refreshToken: string; - try { - // issue tokens - token = createToken({ - payload: { - userId - }, - expiresIn: getJwtAuthLifetime(), - secret: getJwtAuthSecret() - }); - refreshToken = createToken({ - payload: { - userId - }, - expiresIn: getJwtRefreshLifetime(), - secret: getJwtRefreshSecret() - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to issue tokens'); - } + // issue tokens + const token = createToken({ + payload: { + userId + }, + expiresIn: getJwtAuthLifetime(), + secret: getJwtAuthSecret() + }); + + const refreshToken = createToken({ + payload: { + userId + }, + expiresIn: getJwtRefreshLifetime(), + secret: getJwtRefreshSecret() + }); return { token, @@ -294,19 +287,14 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => { * @param {String} obj.userId - id of user whose tokens are cleared. */ const clearTokens = async ({ userId }: { userId: string }): Promise => { - try { - // increment refreshVersion on user by 1 - User.findOneAndUpdate({ - _id: userId - }, { - $inc: { - refreshVersion: 1 - } - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - } + // increment refreshVersion on user by 1 + User.findOneAndUpdate({ + _id: userId + }, { + $inc: { + refreshVersion: 1 + } + }); }; /** @@ -326,15 +314,9 @@ const createToken = ({ expiresIn: string | number; secret: string; }) => { - try { - return jwt.sign(payload, secret, { - expiresIn - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to create a token'); - } + return jwt.sign(payload, secret, { + expiresIn + }); }; export { diff --git a/backend/src/models/apiKeyData.ts b/backend/src/models/apiKeyData.ts index af73b5f69..1b6831730 100644 --- a/backend/src/models/apiKeyData.ts +++ b/backend/src/models/apiKeyData.ts @@ -3,6 +3,7 @@ import { Schema, model, Types } from 'mongoose'; export interface IAPIKeyData { name: string; user: Types.ObjectId; + lastUsed: Date; expiresAt: Date; secretHash: string; } @@ -18,6 +19,9 @@ const apiKeyDataSchema = new Schema( ref: 'User', required: true }, + lastUsed: { + type: Date + }, expiresAt: { type: Date }, diff --git a/backend/src/models/serviceAccount.ts b/backend/src/models/serviceAccount.ts index 79e99a1eb..9ff9dcb03 100644 --- a/backend/src/models/serviceAccount.ts +++ b/backend/src/models/serviceAccount.ts @@ -6,6 +6,7 @@ export interface IServiceAccount extends Document { organization: Types.ObjectId; user: Types.ObjectId; publicKey: string; + lastUsed: Date; expiresAt: Date; secretHash: string; } @@ -30,6 +31,9 @@ const serviceAccountSchema = new Schema( type: String, required: true }, + lastUsed: { + type: Date + }, expiresAt: { type: Date }, @@ -44,6 +48,6 @@ const serviceAccountSchema = new Schema( } ); -const ServiceAccount = model('ServiceAcount', serviceAccountSchema); +const ServiceAccount = model('ServiceAccount', serviceAccountSchema); export default ServiceAccount; \ No newline at end of file diff --git a/backend/src/models/serviceTokenData.ts b/backend/src/models/serviceTokenData.ts index deeb4eb28..86f0e44b3 100644 --- a/backend/src/models/serviceTokenData.ts +++ b/backend/src/models/serviceTokenData.ts @@ -7,6 +7,7 @@ export interface IServiceTokenData extends Document { environment: string; user: Types.ObjectId; serviceAccount: Types.ObjectId; + lastUsed: Date; expiresAt: Date; secretHash: string; encryptedKey: string; @@ -26,7 +27,7 @@ const serviceTokenDataSchema = new Schema( ref: 'Workspace', required: true }, - environment: { // TODO: adapt to upcoming environment id + environment: { type: String, required: true }, @@ -38,6 +39,9 @@ const serviceTokenDataSchema = new Schema( type: Schema.Types.ObjectId, ref: 'ServiceAccount' }, + lastUsed: { + type: Date + }, expiresAt: { type: Date }, diff --git a/frontend/src/ee/components/ActivityTable.tsx b/frontend/src/ee/components/ActivityTable.tsx index 1867128d4..eec8a21fc 100644 --- a/frontend/src/ee/components/ActivityTable.tsx +++ b/frontend/src/ee/components/ActivityTable.tsx @@ -21,6 +21,12 @@ interface LogData { createdAt: string; ipAddress: string; user: string; + serviceAccount: { + name: string; + }; + serviceTokenData: { + name: string; + }; payload: PayloadProps[]; } @@ -41,6 +47,14 @@ const ActivityLogsRow = ({ const [payloadOpened, setPayloadOpened] = useState(false); const { t } = useTranslation(); + const renderUser = () => { + if (row?.user) return `User: ${row.user}`; + if (row?.serviceAccount) return `Service Account: ${row.serviceAccount.name}`; + if (row?.serviceTokenData.name) return `Service Token: ${row.serviceTokenData.name}`; + + return ''; + } + return ( <> @@ -64,7 +78,7 @@ const ActivityLogsRow = ({ ) .join(' and ')} - {row.user} + {renderUser()} {row.channel} {timeSince(new Date(row.createdAt))} diff --git a/frontend/src/pages/activity/[id].tsx b/frontend/src/pages/activity/[id].tsx index 8e16fccd7..6ad98d031 100644 --- a/frontend/src/pages/activity/[id].tsx +++ b/frontend/src/pages/activity/[id].tsx @@ -20,6 +20,12 @@ interface LogData { user: { email: string; }; + serviceAccount?: { + string: string; + }, + serviceTokenData?: { + name: string; + } actions: { _id: string; name: string; @@ -69,13 +75,16 @@ export default function Activity() { userId: '', actionNames: eventChosen }); + setLogsData( tempLogsData.map((log: LogData) => ({ _id: log._id, channel: log.channel, createdAt: log.createdAt, ipAddress: log.ipAddress, - user: log.user.email, + user: log?.user?.email, + serviceAccount: log?.serviceAccount, + serviceTokenData: log?.serviceTokenData, payload: log.actions.map((action) => ({ _id: action._id, name: action.name, @@ -106,7 +115,9 @@ export default function Activity() { channel: log.channel, createdAt: log.createdAt, ipAddress: log.ipAddress, - user: log.user.email, + user: log?.user?.email, + serviceAccount: log?.serviceAccount, + serviceTokenData: log?.serviceTokenData, payload: log.actions.map((action) => ({ _id: action._id, name: action.name,