Update permission implementation for ST V3

This commit is contained in:
Tuan Dang
2023-09-30 12:52:35 +01:00
parent d723d26d2e
commit 3779209ed5
12 changed files with 96 additions and 40 deletions
+1
View File
@@ -9,6 +9,7 @@ JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a
JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff
JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200 JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
JWT_SERVICE_TOKEN_SECRET=f32f716d70a42c5703f4656015e76200
JWT_PROVIDER_AUTH_SECRET=f32f716d70a42c5703f4656015e76201 JWT_PROVIDER_AUTH_SECRET=f32f716d70a42c5703f4656015e76201
# JWT lifetime # JWT lifetime
+15 -15
View File
@@ -110,14 +110,14 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
acceptedPermissions: [Permission.READ, Permission.READ_WRITE] requiredPermissions: [Permission.READ]
}); });
permissionCheckFn = (env: string, secPath: string) => permissionCheckFn = (env: string, secPath: string) =>
isValidScopeV3({ isValidScopeV3({
authPayload: req.authData.authPayload as IServiceTokenDataV3, authPayload: req.authData.authPayload as IServiceTokenDataV3,
environment: env, environment: env,
secretPath: secPath, secretPath: secPath,
acceptedPermissions: [Permission.READ, Permission.READ_WRITE] requiredPermissions: [Permission.READ]
}); });
break; break;
} }
@@ -213,7 +213,7 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
acceptedPermissions: [Permission.READ, Permission.READ_WRITE] requiredPermissions: [Permission.READ]
}); });
break; break;
} }
@@ -285,7 +285,7 @@ export const createSecretRaw = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
acceptedPermissions: [Permission.READ_WRITE] requiredPermissions: [Permission.WRITE]
}); });
break; break;
} }
@@ -384,7 +384,7 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
acceptedPermissions: [Permission.READ_WRITE] requiredPermissions: [Permission.WRITE]
}); });
break; break;
} }
@@ -464,7 +464,7 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
acceptedPermissions: [Permission.READ_WRITE] requiredPermissions: [Permission.WRITE]
}); });
break; break;
} }
@@ -559,14 +559,14 @@ export const getSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
acceptedPermissions: [Permission.READ, Permission.READ_WRITE] requiredPermissions: [Permission.READ]
}); });
permissionCheckFn = (env: string, secPath: string) => permissionCheckFn = (env: string, secPath: string) =>
isValidScopeV3({ isValidScopeV3({
authPayload: req.authData.authPayload as IServiceTokenDataV3, authPayload: req.authData.authPayload as IServiceTokenDataV3,
environment: env, environment: env,
secretPath: secPath, secretPath: secPath,
acceptedPermissions: [Permission.READ, Permission.READ_WRITE] requiredPermissions: [Permission.READ]
}); });
break; break;
} }
@@ -644,7 +644,7 @@ export const getSecretByName = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
acceptedPermissions: [Permission.READ, Permission.READ_WRITE] requiredPermissions: [Permission.READ]
}); });
break; break;
} }
@@ -717,7 +717,7 @@ export const createSecret = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
acceptedPermissions: [Permission.READ_WRITE] requiredPermissions: [Permission.WRITE]
}); });
break; break;
} }
@@ -816,7 +816,7 @@ export const updateSecretByName = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
acceptedPermissions: [Permission.READ_WRITE] requiredPermissions: [Permission.WRITE]
}); });
break; break;
} }
@@ -892,7 +892,7 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
acceptedPermissions: [Permission.READ_WRITE] requiredPermissions: [Permission.WRITE]
}); });
break; break;
} }
@@ -950,7 +950,7 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
acceptedPermissions: [Permission.READ_WRITE] requiredPermissions: [Permission.WRITE]
}); });
break; break;
} }
@@ -999,7 +999,7 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
acceptedPermissions: [Permission.READ_WRITE] requiredPermissions: [Permission.WRITE]
}); });
break; break;
} }
@@ -1048,7 +1048,7 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
acceptedPermissions: [Permission.READ_WRITE] requiredPermissions: [Permission.WRITE]
}); });
break; break;
} }
@@ -94,6 +94,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
user, user,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
publicKey, publicKey,
usageCount: 0,
scopes, scopes,
isActive, isActive,
expiresAt expiresAt
+1 -1
View File
@@ -64,7 +64,7 @@ class EELicenseService {
secretVersioning: true, secretVersioning: true,
pitRecovery: false, pitRecovery: false,
ipAllowlisting: false, ipAllowlisting: false,
rbac: true, rbac: false,
customRateLimits: false, customRateLimits: false,
customAlerts: false, customAlerts: false,
auditLogs: false, auditLogs: false,
+2 -1
View File
@@ -249,7 +249,8 @@ export const getAuthSTDPayload = async ({
isActive: true isActive: true
}, },
{ {
lastUsed: new Date() lastUsed: new Date(),
$inc: { usageCount: 1 }
}, },
{ {
new: true new: true
+3 -3
View File
@@ -69,12 +69,12 @@ export const isValidScopeV3 = ({
authPayload, authPayload,
environment, environment,
secretPath, secretPath,
acceptedPermissions requiredPermissions
}: { }: {
authPayload: IServiceTokenDataV3, authPayload: IServiceTokenDataV3,
environment: string, environment: string,
secretPath: string, secretPath: string,
acceptedPermissions: Permission[] requiredPermissions: Permission[]
}) => { }) => {
const { scopes } = authPayload; const { scopes } = authPayload;
@@ -84,7 +84,7 @@ export const isValidScopeV3 = ({
scope.environment === environment scope.environment === environment
); );
if (validScope && !acceptedPermissions.includes(validScope.permission)) { if (validScope && !requiredPermissions.every(permission => validScope.permissions.includes(permission))) {
return false; return false;
} }
+12 -5
View File
@@ -2,13 +2,13 @@ import { Document, Schema, Types, model } from "mongoose";
export enum Permission { export enum Permission {
READ = "read", READ = "read",
READ_WRITE = "readWrite" WRITE = "write"
} }
export interface Scope { export interface Scope {
environment: string; environment: string;
secretPath: string; secretPath: string;
permission: Permission; permissions: Permission[];
} }
export interface IServiceTokenDataV3 extends Document { export interface IServiceTokenDataV3 extends Document {
@@ -19,6 +19,7 @@ export interface IServiceTokenDataV3 extends Document {
publicKey: string; publicKey: string;
isActive: boolean; isActive: boolean;
lastUsed?: Date; lastUsed?: Date;
usageCount: number;
expiresAt?: Date; expiresAt?: Date;
scopes: Array<Scope>; scopes: Array<Scope>;
} }
@@ -51,6 +52,11 @@ const serviceTokenDataV3Schema = new Schema(
type: Date, type: Date,
required: false required: false
}, },
usageCount: {
type: Number,
default: 0,
required: true
},
expiresAt: { expiresAt: {
type: Date, type: Date,
required: false, required: false,
@@ -68,9 +74,10 @@ const serviceTokenDataV3Schema = new Schema(
default: "/", default: "/",
required: true required: true
}, },
permission: { permissions: {
type: String, type: [String],
enum: [Permission.READ, Permission.READ_WRITE], enum: [Permission.READ, Permission.WRITE],
default: [Permission.READ],
required: true required: true
} }
} }
+5 -5
View File
@@ -20,13 +20,13 @@ import { isValidScopeV3 } from "../helpers";
workspaceId, workspaceId,
environment, environment,
secretPath = "/", secretPath = "/",
acceptedPermissions requiredPermissions
}: { }: {
serviceTokenData: IServiceTokenDataV3; serviceTokenData: IServiceTokenDataV3;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment?: string; environment?: string;
secretPath?: string; secretPath?: string;
acceptedPermissions: Permission[]; requiredPermissions: Permission[];
}) => { }) => {
if (!serviceTokenData.workspace.equals(workspaceId)) { if (!serviceTokenData.workspace.equals(workspaceId)) {
@@ -41,7 +41,7 @@ import { isValidScopeV3 } from "../helpers";
authPayload: serviceTokenData, authPayload: serviceTokenData,
environment, environment,
secretPath, secretPath,
acceptedPermissions requiredPermissions
}); });
if (!isValid) throw UnauthorizedRequestError({ if (!isValid) throw UnauthorizedRequestError({
@@ -57,7 +57,7 @@ export const CreateServiceTokenV3 = z.object({
publicKey: z.string().trim(), publicKey: z.string().trim(),
scopes: z scopes: z
.object({ .object({
permission: z.enum(["read", "readWrite"]), permissions: z.enum(["read", "write"]).array(),
environment: z.string().trim(), environment: z.string().trim(),
secretPath: z.string().trim() secretPath: z.string().trim()
}) })
@@ -78,7 +78,7 @@ export const UpdateServiceTokenV3 = z.object({
isActive: z.boolean().optional(), isActive: z.boolean().optional(),
scopes: z scopes: z
.object({ .object({
permission: z.enum(["read", "readWrite"]), permissions: z.enum(["read", "write"]).array(),
environment: z.string().trim(), environment: z.string().trim(),
secretPath: z.string().trim() secretPath: z.string().trim()
}) })
@@ -0,0 +1,4 @@
export enum Permission {
READ = "read",
WRITE = "write"
}
@@ -1,3 +1,5 @@
import { Permission } from "./enums";
export type ServiceTokenScope = { export type ServiceTokenScope = {
environment: string; environment: string;
secretPath: string; secretPath: string;
@@ -37,7 +39,7 @@ export type DeleteServiceTokenRes = { serviceTokenData: ServiceToken };
// --- v3 // --- v3
export type ServiceTokenV3Scope = { export type ServiceTokenV3Scope = {
permission: string; permissions: Permission[];
environment: string; environment: string;
secretPath: string; secretPath: string;
}; };
@@ -48,6 +50,7 @@ export type ServiceTokenDataV3 = {
workspace: string; workspace: string;
isActive: boolean; isActive: boolean;
lastUsed?: string; lastUsed?: string;
usageCount: number;
scopes: ServiceTokenV3Scope[]; scopes: ServiceTokenV3Scope[];
expiresAt?: string; expiresAt?: string;
createdAt: string; createdAt: string;
@@ -32,6 +32,9 @@ import {
useGetUserWsKey, useGetUserWsKey,
useUpdateServiceTokenV3 useUpdateServiceTokenV3
} from "@app/hooks/api"; } from "@app/hooks/api";
import {
Permission
} from "@app/hooks/api/serviceTokens/enums";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const expirations = [ const expirations = [
@@ -43,7 +46,13 @@ const expirations = [
{ label: "12 months", value: "31104000" } { label: "12 months", value: "31104000" }
]; ];
const permissionValues: Array<"read" | "readWrite"> = ["read", "readWrite"]; const permissionsMap: {
"read": Permission[],
"readWrite": Permission[]
} = {
"read": [Permission.READ],
"readWrite": [Permission.READ, Permission.WRITE],
}
const schema = yup.object({ const schema = yup.object({
name: yup.string().required("ST V3 name is required"), name: yup.string().required("ST V3 name is required"),
@@ -51,7 +60,7 @@ const schema = yup.object({
scopes: yup scopes: yup
.array( .array(
yup.object({ yup.object({
permission: yup.string().oneOf(permissionValues, "Invalid permission").required().label("Permission"), permission: yup.string().oneOf(Object.keys(permissionsMap), "Invalid permission").required().label("Permission") as yup.SchemaOf<"read" | "readWrite", object>,
environment: yup.string().max(50).required().label("Environment"), environment: yup.string().max(50).required().label("Environment"),
secretPath: yup secretPath: yup
.string() .string()
@@ -112,7 +121,18 @@ export const AddServiceTokenV3Modal = ({
if (serviceTokenData) { if (serviceTokenData) {
reset({ reset({
name: serviceTokenData.name, name: serviceTokenData.name,
scopes: serviceTokenData.scopes scopes: serviceTokenData.scopes.map((scope) => {
let permission = "read";
if (scope.permissions.includes(Permission.WRITE)) {
permission = "readWrite";
}
return ({
environment: scope.environment,
secretPath: scope.secretPath,
permission
})
})
}); });
} else { } else {
reset({ reset({
@@ -140,12 +160,21 @@ export const AddServiceTokenV3Modal = ({
scopes: any; scopes: any;
}; };
// convert read/readWrite permission => ["read", "write"] format
const reformattedScopes = scopes.map((scope) => {
return ({
environment: scope.environment,
secretPath: scope.secretPath,
permissions: permissionsMap[scope.permission]
});
})
if (serviceTokenData) { if (serviceTokenData) {
// update // update
await updateMutateAsync({ await updateMutateAsync({
serviceTokenDataId: serviceTokenData.serviceTokenDataId, serviceTokenDataId: serviceTokenData.serviceTokenDataId,
name, name,
scopes, scopes: reformattedScopes,
expiresIn: expiresIn === "" ? undefined : Number(expiresIn) expiresIn: expiresIn === "" ? undefined : Number(expiresIn)
}); });
} else { } else {
@@ -176,7 +205,7 @@ export const AddServiceTokenV3Modal = ({
name, name,
workspaceId: currentWorkspace._id, workspaceId: currentWorkspace._id,
publicKey, publicKey,
scopes, scopes: reformattedScopes,
expiresIn: expiresIn === "" ? undefined : Number(expiresIn), expiresIn: expiresIn === "" ? undefined : Number(expiresIn),
encryptedKey: ciphertext, encryptedKey: ciphertext,
nonce nonce
@@ -21,9 +21,8 @@ import {
useGetWorkspaceServiceTokenDataV3, useGetWorkspaceServiceTokenDataV3,
useUpdateServiceTokenV3 useUpdateServiceTokenV3
} from "@app/hooks/api"; } from "@app/hooks/api";
import { import { ServiceTokenV3Scope } from "@app/hooks/api/serviceTokens/types"
ServiceTokenV3Scope import { Permission } from "@app/hooks/api/serviceTokens/enums"
} from "@app/hooks/api/serviceTokens/types"
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = { type Props = {
@@ -90,6 +89,7 @@ export const ServiceTokenV3Table = ({
<Th>Name</Th> <Th>Name</Th>
<Th>Status</Th> <Th>Status</Th>
<Th>Scopes</Th> <Th>Scopes</Th>
<Th># Times Used</Th>
<Th>Last Used</Th> <Th>Last Used</Th>
<Th>Created At</Th> <Th>Created At</Th>
<Th>Expires At</Th> <Th>Expires At</Th>
@@ -106,6 +106,7 @@ export const ServiceTokenV3Table = ({
name, name,
isActive, isActive,
lastUsed, lastUsed,
usageCount,
scopes, scopes,
createdAt, createdAt,
expiresAt expiresAt
@@ -135,16 +136,25 @@ export const ServiceTokenV3Table = ({
</Td> </Td>
<Td> <Td>
{scopes.map((scope) => { {scopes.map((scope) => {
let permissionText = "read"
if (
scope.permissions.includes(Permission.WRITE) &&
scope.permissions.includes(Permission.READ)
) {
permissionText = "readWrite";
}
return ( return (
<p key={`service-token-${_id}-scope-${scope.environment}-${scope.secretPath}`}> <p key={`service-token-${_id}-scope-${scope.environment}-${scope.secretPath}`}>
<span className="font-bold"> <span className="font-bold">
{scope.permission} {scope.permissions}
</span> </span>
{` @${scope.environment} - ${scope.secretPath}`} {` @${scope.environment} - ${scope.secretPath}`}
</p> </p>
); );
})} })}
</Td> </Td>
<Td>{usageCount}</Td>
<Td>{lastUsed ? formatDate(lastUsed) : "-"}</Td> <Td>{lastUsed ? formatDate(lastUsed) : "-"}</Td>
<Td>{formatDate(createdAt)}</Td> <Td>{formatDate(createdAt)}</Td>
<Td>{expiresAt ? formatDate(expiresAt) : "-"}</Td> <Td>{expiresAt ? formatDate(expiresAt) : "-"}</Td>