Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 7 additions & 14 deletions n8n_workflows/AI_Sync_Workflow.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@
"headerParameters": {
"parameters": [
{
"name": "={{ $json.authType === \"Header\" ? $json.authentication.header : \"Authentication\" }}",
"value": "={{ $json.authType === \"Header\" ? $json.authentication.value : \"Bearer \" + $json.authentication.token }}"
"name": "={{ $json.body.authType === \"Header\" ? $json.body.authentication.header : \"Authentication\" }}",
"value": "={{ $json.body.authType === \"Header\" ? $json.body.authentication.value : \"Bearer \" + $json.body.authentication.token }}"
Comment thread
0xcad marked this conversation as resolved.
}
]
},
Expand Down Expand Up @@ -133,7 +133,7 @@
"name": "API Crawler - AI Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 3,
"position": [728, -416]
"position": [736, -416]
},
{
"parameters": {
Expand Down Expand Up @@ -172,8 +172,6 @@
"parameters": {
"method": "POST",
"url": "={{ $('Workflow Configuration').first().json.apiBaseUrl + $('Workflow Configuration').first().json.responsePath }}",
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
Expand All @@ -192,15 +190,10 @@
"name": "Send Data to VIPER API",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [1296, -416],
"credentials": {
"httpBearerAuth": {
"id": "1M7bJlVV6osJ3KV0",
"name": "production viper auth"
}
}
"position": [1296, -416]
}
],
"pinData": {},
"connections": {
"Webhook Trigger": {
"main": [
Expand Down Expand Up @@ -291,11 +284,11 @@
]
}
},
"active": false,
"active": true,
"settings": {
"executionOrder": "v1"
},
"versionId": "b1c40fa0-12d4-49b8-8878-fff285974aad",
"versionId": "6964a24b-ce7b-4d31-b193-dbc3fc5a5cb1",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "66207f7815720e24b39297468a019ba2a80d8e3ab7d395f296925a8d0e9081c5"
Expand Down
40 changes: 40 additions & 0 deletions prisma/migrations/20260327193952_user_tokens/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
Warnings:

- You are about to drop the column `apiKeyId` on the `integration` table. All the data in the column will be lost.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drop it >:)

- Made the column `integrationUserId` on table `integration` required. This step will fail if there are existing NULL values in that column.

*/
-- DropForeignKey
ALTER TABLE "public"."integration" DROP CONSTRAINT "integration_apiKeyId_fkey";

-- DropIndex
DROP INDEX "public"."integration_apiKeyId_key";

-- AlterTable
ALTER TABLE "integration" DROP COLUMN "apiKeyId",
ALTER COLUMN "integrationUserId" SET NOT NULL;

-- CreateTable
CREATE TABLE "UserToken" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"tokenHash" TEXT NOT NULL,
"permissions" TEXT,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "UserToken_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "UserToken_tokenHash_key" ON "UserToken"("tokenHash");

-- CreateIndex
CREATE INDEX "UserToken_userId_idx" ON "UserToken"("userId");

-- CreateIndex
CREATE INDEX "UserToken_expiresAt_idx" ON "UserToken"("expiresAt");

-- AddForeignKey
ALTER TABLE "UserToken" ADD CONSTRAINT "UserToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
22 changes: 16 additions & 6 deletions prisma/schema.prisma
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove api key from integration, create UserToken model.

UserToken stores a hash of a token, expiration time, user, and optional permissions.

permissions are so that user tokens can be used more generally on VIPER if needed, and to make sure that token users "stay in their lanes". it's cheap RBAC. for integration uploads we currently set a permission on the user token for just that integration endpoint, and then when we use a token, we make sure it has the expected permission. if it doesn't the token is invalid. so Helm, which gives us vulnerabilities, can't use its token to give us assets instead.

Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ model User {
integration Integration[]
integrationUser Integration? @relation("integration_user")
webhooks Webhook[]
userTokens UserToken[]

@@unique([email])
@@map("user")
Expand Down Expand Up @@ -90,6 +91,19 @@ model Verification {
@@map("verification")
}

model UserToken {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
tokenHash String @unique
permissions String? // what can the token be used for?
expiresAt DateTime
createdAt DateTime @default(now())

@@index([userId])
@@index([expiresAt])
}

model Workflow {
id String @id @default(cuid())
name String
Expand Down Expand Up @@ -556,7 +570,6 @@ model Apikey {
metadata String?

connector ApiKeyConnector?
integration Integration?

@@map("apikey")
}
Expand All @@ -578,18 +591,15 @@ model Integration {
syncStatus SyncStatus[]
lastSuccessfulSync DateTime?

apiKeyId String? @unique
apiKey Apikey? @relation(fields: [apiKeyId], references: [id])
apiKeyConnector ApiKeyConnector?

// The user who created the integration
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

// the user that this integration creates
// TODO: should this be required? leaving it optional for migration
integrationUserId String? @unique
integrationUser User? @relation(name: "integration_user", fields: [integrationUserId], references: [id], onDelete: Cascade)
integrationUserId String @unique
integrationUser User @relation(name: "integration_user", fields: [integrationUserId], references: [id], onDelete: Cascade)

assetMappings ExternalAssetMapping[]
deviceArtifactMappings ExternalDeviceArtifactMapping[]
Expand Down
2 changes: 2 additions & 0 deletions src/app/api/inngest/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
enrichAllVulnerabilities,
enrichVulnerability,
} from "@/inngest/functions/enrich-vulnerabilities";
import { purgeExpiredTokensFn } from "@/inngest/functions/purge-expired-user-tokens";
import {
syncAllIntegrations,
syncIntegration,
Expand All @@ -18,5 +19,6 @@ export const { GET, POST, PUT } = serve({
enrichVulnerability,
enrichAllVulnerabilities,
chatAgent,
purgeExpiredTokensFn,
],
});
85 changes: 58 additions & 27 deletions src/app/api/v1/__tests__/assets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
AUTH_TOKEN,
authHeader,
BASE_URL,
createIntegrationToken,
generateCPE,
jsonHeader,
setupMockIntegration,
Expand Down Expand Up @@ -346,13 +347,16 @@ describe("Assets Endpoint (/assets)", () => {
});

it("empty Assets uploadIntegration endpoint int test", async () => {
const { apiKey } = await setupMockIntegration(mockIntegrationPayload);
const { integration } = await setupMockIntegration(mockIntegrationPayload);

// this should succeed and nothing should be created
const noAssets = { ...assetIntegrationPayload, items: [] };
const token = await createIntegrationToken(
integration.integrationUserId,
ResourceType.Asset,
);
const createAssetResp = await request(BASE_URL)
.post("/assets/integrationUpload")
.set({ Authorization: apiKey.key })
.post(`/assets/integrationUpload/${token}`)
.set(jsonHeader)
.send(noAssets);

Expand All @@ -364,8 +368,9 @@ describe("Assets Endpoint (/assets)", () => {
});

it("create Assets uploadIntegration endpoint int test", async () => {
const { integration: createdIntegration, apiKey } =
await setupMockIntegration(mockIntegrationPayload);
const { integration: createdIntegration } = await setupMockIntegration(
mockIntegrationPayload,
);

onTestFinished(async () => {
// this won't throw errors if it misses, which messes up the onTestFinished stack
Expand All @@ -383,9 +388,12 @@ describe("Assets Endpoint (/assets)", () => {
});
});

const createToken = await createIntegrationToken(
createdIntegration.integrationUserId,
ResourceType.Asset,
);
const integrationRes = await request(BASE_URL)
.post("/assets/integrationUpload")
.set({ Authorization: apiKey.key })
.post(`/assets/integrationUpload/${createToken}`)
.set(jsonHeader)
.send(assetIntegrationPayload);

Expand Down Expand Up @@ -480,8 +488,9 @@ describe("Assets Endpoint (/assets)", () => {
});

it("update Assets uploadIntegration endpoint int test", async () => {
const { integration: createdIntegration, apiKey } =
await setupMockIntegration(mockIntegrationPayload);
const { integration: createdIntegration } = await setupMockIntegration(
mockIntegrationPayload,
);

onTestFinished(async () => {
// this won't cause an error if it misses which messes up the onTestFinished stack
Expand All @@ -500,9 +509,12 @@ describe("Assets Endpoint (/assets)", () => {
});

// create the assets first
const createToken = await createIntegrationToken(
createdIntegration.integrationUserId,
ResourceType.Asset,
);
const createAssetsReq = await request(BASE_URL)
.post("/assets/integrationUpload")
.set({ Authorization: apiKey.key })
.post(`/assets/integrationUpload/${createToken}`)
.set(jsonHeader)
.send(assetIntegrationPayload);

Expand All @@ -521,9 +533,12 @@ describe("Assets Endpoint (/assets)", () => {
updateAssetsPayload.items[0].upstreamApi = newUpstreamApi;
updateAssetsPayload.items[1].upstreamApi = newUpstreamApi;

const updateToken = await createIntegrationToken(
createdIntegration.integrationUserId,
ResourceType.Asset,
);
const integrationRes = await request(BASE_URL)
.post("/assets/integrationUpload")
.set({ Authorization: apiKey.key })
.post(`/assets/integrationUpload/${updateToken}`)
.set(jsonHeader)
.send(updateAssetsPayload);

Expand Down Expand Up @@ -608,8 +623,9 @@ describe("Assets Endpoint (/assets)", () => {
});

it("mixed create+update Assets uploadIntegration endpoint int test", async () => {
const { integration: createdIntegration, apiKey } =
await setupMockIntegration(mockIntegrationPayload);
const { integration: createdIntegration } = await setupMockIntegration(
mockIntegrationPayload,
);

onTestFinished(async () => {
// this won't throw errors if it misses, which messes up the onTestFinished stack
Expand All @@ -632,9 +648,12 @@ describe("Assets Endpoint (/assets)", () => {
...assetIntegrationPayload,
items: assetIntegrationPayload.items.slice(1),
};
const createToken = await createIntegrationToken(
createdIntegration.integrationUserId,
ResourceType.Asset,
);
const createAssetResp = await request(BASE_URL)
.post("/assets/integrationUpload")
.set({ Authorization: apiKey.key })
.post(`/assets/integrationUpload/${createToken}`)
.set(jsonHeader)
.send(oneAsset);

Expand All @@ -649,9 +668,12 @@ describe("Assets Endpoint (/assets)", () => {
const newUpstreamApi = "https://mock-upstream-api.com/v2";
createWithUpdateAssets.items[0].upstreamApi = newUpstreamApi;

const updateToken = await createIntegrationToken(
createdIntegration.integrationUserId,
ResourceType.Asset,
);
const integrationResp = await request(BASE_URL)
.post("/assets/integrationUpload")
.set({ Authorization: apiKey.key })
.post(`/assets/integrationUpload/${updateToken}`)
.set(jsonHeader)
.send(createWithUpdateAssets);

Expand Down Expand Up @@ -782,9 +804,12 @@ describe("Assets Endpoint (/assets)", () => {
};

// then run the endpoint which should update the asset and create the mapping
const integrationToken = await createIntegrationToken(
createdIntegration.integrationUserId,
ResourceType.Asset,
);
const updateAssetResp = await request(BASE_URL)
.post("/assets/integrationUpload")
.set({ Authorization: apiKey.key })
.post(`/assets/integrationUpload/${integrationToken}`)
.set(jsonHeader)
.send(updateAssetPayload);

Expand Down Expand Up @@ -838,7 +863,7 @@ describe("Assets Endpoint (/assets)", () => {
});

it("all null unique field should miss Asset uploadIntegration endpoint int test", async () => {
const { apiKey } = await setupMockIntegration(mockIntegrationPayload);
const { integration } = await setupMockIntegration(mockIntegrationPayload);

onTestFinished(async () => {
// this won't throw errors if it misses, which messes up the onTestFinished stack
Expand Down Expand Up @@ -892,9 +917,12 @@ describe("Assets Endpoint (/assets)", () => {
};

// this should create a new asset because all unique fields are missing
const allNullToken = await createIntegrationToken(
integration.integrationUserId,
ResourceType.Asset,
);
const integrationRes = await request(BASE_URL)
.post("/assets/integrationUpload")
.set({ Authorization: apiKey.key })
.post(`/assets/integrationUpload/${allNullToken}`)
.set(jsonHeader)
.send({
...assetIntegrationPayload,
Expand All @@ -909,7 +937,7 @@ describe("Assets Endpoint (/assets)", () => {
});

it("partial null unique field shouldn't miss Asset uploadIntegration endpoint int test", async () => {
const { apiKey } = await setupMockIntegration(mockIntegrationPayload);
const { integration } = await setupMockIntegration(mockIntegrationPayload);

onTestFinished(async () => {
// this won't throw errors if it misses, which messes up the onTestFinished stack
Expand Down Expand Up @@ -954,9 +982,12 @@ describe("Assets Endpoint (/assets)", () => {
};

// this should produce an update based on serialNumber match
const partialNullToken = await createIntegrationToken(
integration.integrationUserId,
ResourceType.Asset,
);
const integrationRes = await request(BASE_URL)
.post("/assets/integrationUpload")
.set({ Authorization: apiKey.key })
.post(`/assets/integrationUpload/${partialNullToken}`)
.set(jsonHeader)
.send({
...assetIntegrationPayload,
Expand Down
Loading
Loading