Patch service account UI in audit logs, add lastUsed for API keys and service accounts/tokens

This commit is contained in:
Tuan Dang
2023-04-06 23:52:06 +03:00
parent d54753289a
commit a7880db871
10 changed files with 145 additions and 138 deletions

View File

@@ -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

View File

@@ -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();

View File

@@ -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.');
}

View File

@@ -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 });

View File

@@ -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.UserIDJwtPayload>(
jwt.verify(authTokenValue, getJwtAuthSecret())
);
const decodedToken = <jwt.UserIDJwtPayload>(
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<void> => {
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 {

View File

@@ -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<IAPIKeyData>(
ref: 'User',
required: true
},
lastUsed: {
type: Date
},
expiresAt: {
type: Date
},

View File

@@ -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<IServiceAccount>(
type: String,
required: true
},
lastUsed: {
type: Date
},
expiresAt: {
type: Date
},
@@ -44,6 +48,6 @@ const serviceAccountSchema = new Schema<IServiceAccount>(
}
);
const ServiceAccount = model<IServiceAccount>('ServiceAcount', serviceAccountSchema);
const ServiceAccount = model<IServiceAccount>('ServiceAccount', serviceAccountSchema);
export default ServiceAccount;

View File

@@ -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<IServiceTokenData>(
ref: 'Workspace',
required: true
},
environment: { // TODO: adapt to upcoming environment id
environment: {
type: String,
required: true
},
@@ -38,6 +39,9 @@ const serviceTokenDataSchema = new Schema<IServiceTokenData>(
type: Schema.Types.ObjectId,
ref: 'ServiceAccount'
},
lastUsed: {
type: Date
},
expiresAt: {
type: Date
},

View File

@@ -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 (
<>
<tr key={guidGenerator()} className="bg-bunker-800 duration-100 w-full text-sm">
@@ -64,7 +78,7 @@ const ActivityLogsRow = ({
)
.join(' and ')}
</td>
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">{row.user}</td>
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">{renderUser()}</td>
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">{row.channel}</td>
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
{timeSince(new Date(row.createdAt))}

View File

@@ -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,