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

View File

@@ -9,6 +9,7 @@ JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a
JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff
JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
JWT_SERVICE_TOKEN_SECRET=f32f716d70a42c5703f4656015e76200
JWT_PROVIDER_AUTH_SECRET=f32f716d70a42c5703f4656015e76201
# JWT lifetime

View File

@@ -110,14 +110,14 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ, Permission.READ_WRITE]
requiredPermissions: [Permission.READ]
});
permissionCheckFn = (env: string, secPath: string) =>
isValidScopeV3({
authPayload: req.authData.authPayload as IServiceTokenDataV3,
environment: env,
secretPath: secPath,
acceptedPermissions: [Permission.READ, Permission.READ_WRITE]
requiredPermissions: [Permission.READ]
});
break;
}
@@ -213,7 +213,7 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ, Permission.READ_WRITE]
requiredPermissions: [Permission.READ]
});
break;
}
@@ -285,7 +285,7 @@ export const createSecretRaw = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ_WRITE]
requiredPermissions: [Permission.WRITE]
});
break;
}
@@ -384,7 +384,7 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ_WRITE]
requiredPermissions: [Permission.WRITE]
});
break;
}
@@ -464,7 +464,7 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ_WRITE]
requiredPermissions: [Permission.WRITE]
});
break;
}
@@ -559,14 +559,14 @@ export const getSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ, Permission.READ_WRITE]
requiredPermissions: [Permission.READ]
});
permissionCheckFn = (env: string, secPath: string) =>
isValidScopeV3({
authPayload: req.authData.authPayload as IServiceTokenDataV3,
environment: env,
secretPath: secPath,
acceptedPermissions: [Permission.READ, Permission.READ_WRITE]
requiredPermissions: [Permission.READ]
});
break;
}
@@ -644,7 +644,7 @@ export const getSecretByName = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ, Permission.READ_WRITE]
requiredPermissions: [Permission.READ]
});
break;
}
@@ -717,7 +717,7 @@ export const createSecret = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ_WRITE]
requiredPermissions: [Permission.WRITE]
});
break;
}
@@ -816,7 +816,7 @@ export const updateSecretByName = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ_WRITE]
requiredPermissions: [Permission.WRITE]
});
break;
}
@@ -892,7 +892,7 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ_WRITE]
requiredPermissions: [Permission.WRITE]
});
break;
}
@@ -950,7 +950,7 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ_WRITE]
requiredPermissions: [Permission.WRITE]
});
break;
}
@@ -999,7 +999,7 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ_WRITE]
requiredPermissions: [Permission.WRITE]
});
break;
}
@@ -1048,7 +1048,7 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
acceptedPermissions: [Permission.READ_WRITE]
requiredPermissions: [Permission.WRITE]
});
break;
}

View File

@@ -94,6 +94,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
user,
workspace: new Types.ObjectId(workspaceId),
publicKey,
usageCount: 0,
scopes,
isActive,
expiresAt

View File

@@ -64,7 +64,7 @@ class EELicenseService {
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: false,
rbac: true,
rbac: false,
customRateLimits: false,
customAlerts: false,
auditLogs: false,

View File

@@ -249,7 +249,8 @@ export const getAuthSTDPayload = async ({
isActive: true
},
{
lastUsed: new Date()
lastUsed: new Date(),
$inc: { usageCount: 1 }
},
{
new: true

View File

@@ -69,12 +69,12 @@ export const isValidScopeV3 = ({
authPayload,
environment,
secretPath,
acceptedPermissions
requiredPermissions
}: {
authPayload: IServiceTokenDataV3,
environment: string,
secretPath: string,
acceptedPermissions: Permission[]
requiredPermissions: Permission[]
}) => {
const { scopes } = authPayload;
@@ -84,7 +84,7 @@ export const isValidScopeV3 = ({
scope.environment === environment
);
if (validScope && !acceptedPermissions.includes(validScope.permission)) {
if (validScope && !requiredPermissions.every(permission => validScope.permissions.includes(permission))) {
return false;
}

View File

@@ -2,13 +2,13 @@ import { Document, Schema, Types, model } from "mongoose";
export enum Permission {
READ = "read",
READ_WRITE = "readWrite"
WRITE = "write"
}
export interface Scope {
environment: string;
secretPath: string;
permission: Permission;
permissions: Permission[];
}
export interface IServiceTokenDataV3 extends Document {
@@ -19,6 +19,7 @@ export interface IServiceTokenDataV3 extends Document {
publicKey: string;
isActive: boolean;
lastUsed?: Date;
usageCount: number;
expiresAt?: Date;
scopes: Array<Scope>;
}
@@ -51,6 +52,11 @@ const serviceTokenDataV3Schema = new Schema(
type: Date,
required: false
},
usageCount: {
type: Number,
default: 0,
required: true
},
expiresAt: {
type: Date,
required: false,
@@ -68,9 +74,10 @@ const serviceTokenDataV3Schema = new Schema(
default: "/",
required: true
},
permission: {
type: String,
enum: [Permission.READ, Permission.READ_WRITE],
permissions: {
type: [String],
enum: [Permission.READ, Permission.WRITE],
default: [Permission.READ],
required: true
}
}

View File

@@ -20,13 +20,13 @@ import { isValidScopeV3 } from "../helpers";
workspaceId,
environment,
secretPath = "/",
acceptedPermissions
requiredPermissions
}: {
serviceTokenData: IServiceTokenDataV3;
workspaceId: Types.ObjectId;
environment?: string;
secretPath?: string;
acceptedPermissions: Permission[];
requiredPermissions: Permission[];
}) => {
if (!serviceTokenData.workspace.equals(workspaceId)) {
@@ -41,7 +41,7 @@ import { isValidScopeV3 } from "../helpers";
authPayload: serviceTokenData,
environment,
secretPath,
acceptedPermissions
requiredPermissions
});
if (!isValid) throw UnauthorizedRequestError({
@@ -57,7 +57,7 @@ export const CreateServiceTokenV3 = z.object({
publicKey: z.string().trim(),
scopes: z
.object({
permission: z.enum(["read", "readWrite"]),
permissions: z.enum(["read", "write"]).array(),
environment: z.string().trim(),
secretPath: z.string().trim()
})
@@ -78,7 +78,7 @@ export const UpdateServiceTokenV3 = z.object({
isActive: z.boolean().optional(),
scopes: z
.object({
permission: z.enum(["read", "readWrite"]),
permissions: z.enum(["read", "write"]).array(),
environment: z.string().trim(),
secretPath: z.string().trim()
})

View File

@@ -0,0 +1,4 @@
export enum Permission {
READ = "read",
WRITE = "write"
}

View File

@@ -1,3 +1,5 @@
import { Permission } from "./enums";
export type ServiceTokenScope = {
environment: string;
secretPath: string;
@@ -37,7 +39,7 @@ export type DeleteServiceTokenRes = { serviceTokenData: ServiceToken };
// --- v3
export type ServiceTokenV3Scope = {
permission: string;
permissions: Permission[];
environment: string;
secretPath: string;
};
@@ -48,6 +50,7 @@ export type ServiceTokenDataV3 = {
workspace: string;
isActive: boolean;
lastUsed?: string;
usageCount: number;
scopes: ServiceTokenV3Scope[];
expiresAt?: string;
createdAt: string;

View File

@@ -32,6 +32,9 @@ import {
useGetUserWsKey,
useUpdateServiceTokenV3
} from "@app/hooks/api";
import {
Permission
} from "@app/hooks/api/serviceTokens/enums";
import { UsePopUpState } from "@app/hooks/usePopUp";
const expirations = [
@@ -43,7 +46,13 @@ const expirations = [
{ 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({
name: yup.string().required("ST V3 name is required"),
@@ -51,7 +60,7 @@ const schema = yup.object({
scopes: yup
.array(
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"),
secretPath: yup
.string()
@@ -112,7 +121,18 @@ export const AddServiceTokenV3Modal = ({
if (serviceTokenData) {
reset({
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 {
reset({
@@ -140,12 +160,21 @@ export const AddServiceTokenV3Modal = ({
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) {
// update
await updateMutateAsync({
serviceTokenDataId: serviceTokenData.serviceTokenDataId,
name,
scopes,
scopes: reformattedScopes,
expiresIn: expiresIn === "" ? undefined : Number(expiresIn)
});
} else {
@@ -176,7 +205,7 @@ export const AddServiceTokenV3Modal = ({
name,
workspaceId: currentWorkspace._id,
publicKey,
scopes,
scopes: reformattedScopes,
expiresIn: expiresIn === "" ? undefined : Number(expiresIn),
encryptedKey: ciphertext,
nonce

View File

@@ -21,9 +21,8 @@ import {
useGetWorkspaceServiceTokenDataV3,
useUpdateServiceTokenV3
} from "@app/hooks/api";
import {
ServiceTokenV3Scope
} from "@app/hooks/api/serviceTokens/types"
import { ServiceTokenV3Scope } from "@app/hooks/api/serviceTokens/types"
import { Permission } from "@app/hooks/api/serviceTokens/enums"
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
@@ -90,6 +89,7 @@ export const ServiceTokenV3Table = ({
<Th>Name</Th>
<Th>Status</Th>
<Th>Scopes</Th>
<Th># Times Used</Th>
<Th>Last Used</Th>
<Th>Created At</Th>
<Th>Expires At</Th>
@@ -106,6 +106,7 @@ export const ServiceTokenV3Table = ({
name,
isActive,
lastUsed,
usageCount,
scopes,
createdAt,
expiresAt
@@ -135,16 +136,25 @@ export const ServiceTokenV3Table = ({
</Td>
<Td>
{scopes.map((scope) => {
let permissionText = "read"
if (
scope.permissions.includes(Permission.WRITE) &&
scope.permissions.includes(Permission.READ)
) {
permissionText = "readWrite";
}
return (
<p key={`service-token-${_id}-scope-${scope.environment}-${scope.secretPath}`}>
<span className="font-bold">
{scope.permission}
{scope.permissions}
</span>
{` @${scope.environment} - ${scope.secretPath}`}
</p>
);
})}
</Td>
<Td>{usageCount}</Td>
<Td>{lastUsed ? formatDate(lastUsed) : "-"}</Td>
<Td>{formatDate(createdAt)}</Td>
<Td>{expiresAt ? formatDate(expiresAt) : "-"}</Td>