Skip to content

Commit

Permalink
Merge branch 'main' into blog-last-used
Browse files Browse the repository at this point in the history
  • Loading branch information
perkinsjr authored Sep 24, 2024
2 parents cb70ff4 + 88bd752 commit 1e865a7
Show file tree
Hide file tree
Showing 40 changed files with 233 additions and 2,263 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ test("sets new ratelimits", async (t) => {

expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200);

await new Promise((r) => setTimeout(r, 2000));

const found = await h.db.primary.query.ratelimits.findMany({
where: (table, { eq }) => eq(table.identityId, identity.id),
});
Expand Down
1 change: 0 additions & 1 deletion apps/api/src/routes/v1_identities_updateIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,6 @@ export const registerV1IdentitiesUpdateIdentity = (app: App) =>
/**
* Delete undesired ratelimits
*/

for (const rl of deleteRatelimits) {
await tx.delete(schema.ratelimits).where(eq(schema.ratelimits.id, rl.id));
auditLogs.push({
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/routes/v1_keys_createKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ async function getRoleIds(
return roles.map((r) => r.id);
}

async function upsertIdentity(
export async function upsertIdentity(
db: Database,
workspaceId: string,
externalId: string,
Expand Down
201 changes: 201 additions & 0 deletions apps/api/src/routes/v1_keys_updateKey.happy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,207 @@ test("delete expires", async (t) => {
expect(found?.expires).toBeNull();
});

describe("externalId", () => {
test("set externalId connects the identity", async (t) => {
const h = await IntegrationHarness.init(t);

const root = await h.createRootKey([`api.${h.resources.userApi.id}.update_key`]);

const key = await h.createKey();
const externalId = newId("test");

const res = await h.post<V1KeysUpdateKeyRequest, V1KeysUpdateKeyResponse>({
url: "/v1/keys.updateKey",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${root.key}`,
},
body: {
keyId: key.keyId,
externalId,
},
});

expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200);

const found = await h.db.primary.query.keys.findFirst({
where: (table, { eq }) => eq(table.id, key.keyId),
with: {
identity: true,
},
});
expect(found).toBeDefined();
expect(found!.identity).toBeDefined();
expect(found!.identity!.externalId).toBe(externalId);
});

test("omitting the field does not disconnect the identity", async (t) => {
const h = await IntegrationHarness.init(t);

const root = await h.createRootKey([`api.${h.resources.userApi.id}.update_key`]);

const identityId = newId("test");
const externalId = newId("test");
await h.db.primary.insert(schema.identities).values({
id: identityId,
workspaceId: h.resources.userWorkspace.id,
externalId,
});
const key = await h.createKey({ identityId });
const before = await h.db.primary.query.keys.findFirst({
where: (table, { eq }) => eq(table.id, key.keyId),
with: {
identity: true,
},
});
expect(before?.identity).toBeDefined();

const res = await h.post<V1KeysUpdateKeyRequest, V1KeysUpdateKeyResponse>({
url: "/v1/keys.updateKey",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${root.key}`,
},
body: {
keyId: key.keyId,
externalId: undefined,
},
});

expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200);

const found = await h.db.primary.query.keys.findFirst({
where: (table, { eq }) => eq(table.id, key.keyId),
with: {
identity: true,
},
});
expect(found).toBeDefined();
expect(found!.identity).toBeDefined();
expect(found!.identity!.externalId).toBe(externalId);
});

test("set ownerId connects the identity", async (t) => {
const h = await IntegrationHarness.init(t);

const root = await h.createRootKey([`api.${h.resources.userApi.id}.update_key`]);

const key = await h.createKey();
const ownerId = newId("test");

const res = await h.post<V1KeysUpdateKeyRequest, V1KeysUpdateKeyResponse>({
url: "/v1/keys.updateKey",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${root.key}`,
},
body: {
keyId: key.keyId,
ownerId,
},
});

expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200);

const found = await h.db.primary.query.keys.findFirst({
where: (table, { eq }) => eq(table.id, key.keyId),
with: {
identity: true,
},
});
expect(found).toBeDefined();
expect(found!.identity).toBeDefined();
expect(found!.identity!.externalId).toBe(ownerId);
});

test("set externalId=null disconnects the identity", async (t) => {
const h = await IntegrationHarness.init(t);

const root = await h.createRootKey([`api.${h.resources.userApi.id}.update_key`]);

const identityId = newId("test");
await h.db.primary.insert(schema.identities).values({
id: identityId,
workspaceId: h.resources.userWorkspace.id,
externalId: newId("test"),
});
const key = await h.createKey({ identityId });
const before = await h.db.primary.query.keys.findFirst({
where: (table, { eq }) => eq(table.id, key.keyId),
with: {
identity: true,
},
});
expect(before?.identity).toBeDefined();

const res = await h.post<V1KeysUpdateKeyRequest, V1KeysUpdateKeyResponse>({
url: "/v1/keys.updateKey",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${root.key}`,
},
body: {
keyId: key.keyId,
externalId: null,
},
});

expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200);

const found = await h.db.primary.query.keys.findFirst({
where: (table, { eq }) => eq(table.id, key.keyId),
with: {
identity: true,
},
});
expect(found).toBeDefined();
expect(found!.identity).toBeNull();
});

test("set ownerId=null disconnects the identity", async (t) => {
const h = await IntegrationHarness.init(t);

const root = await h.createRootKey([`api.${h.resources.userApi.id}.update_key`]);

const identityId = newId("test");
await h.db.primary.insert(schema.identities).values({
id: identityId,
workspaceId: h.resources.userWorkspace.id,
externalId: newId("test"),
});
const key = await h.createKey({ identityId });
const before = await h.db.primary.query.keys.findFirst({
where: (table, { eq }) => eq(table.id, key.keyId),
with: {
identity: true,
},
});
expect(before?.identity).toBeDefined();

const res = await h.post<V1KeysUpdateKeyRequest, V1KeysUpdateKeyResponse>({
url: "/v1/keys.updateKey",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${root.key}`,
},
body: {
keyId: key.keyId,
ownerId: null,
},
});

expect(res.status, `expected 200, received: ${JSON.stringify(res, null, 2)}`).toBe(200);

const found = await h.db.primary.query.keys.findFirst({
where: (table, { eq }) => eq(table.id, key.keyId),
with: {
identity: true,
},
});
expect(found).toBeDefined();
expect(found!.identity).toBeNull();
});
});
test("update should not affect undefined fields", async (t) => {
const h = await IntegrationHarness.init(t);

Expand Down
33 changes: 28 additions & 5 deletions apps/api/src/routes/v1_keys_updateKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { UnkeyApiError, openApiErrorResponses } from "@/pkg/errors";
import { schema } from "@unkey/db";
import { eq } from "@unkey/db";
import { buildUnkeyQuery } from "@unkey/rbac";
import { upsertIdentity } from "./v1_keys_createKey";
import { setPermissions } from "./v1_keys_setPermissions";
import { setRoles } from "./v1_keys_setRoles";

Expand All @@ -30,11 +31,24 @@ const route = createRoute({
description: "The name of the key",
example: "Customer X",
}),
ownerId: z.string().nullish().openapi({
description:
"The id of the tenant associated with this key. Use whatever reference you have in your system to identify the tenant. When verifying the key, we will send this field back to you, so you know who is accessing your API.",
example: "user_123",
}),
ownerId: z
.string()
.nullish()
.openapi({
deprecated: true,
description: `Deprecated, use \`externalId\`
The id of the tenant associated with this key. Use whatever reference you have in your system to identify the tenant. When verifying the key, we will send this field back to you, so you know who is accessing your API.`,
example: "user_123",
}),
externalId: z
.string()
.nullish()
.openapi({
description: `The id of the tenant associated with this key. Use whatever reference you have in your system to identify the tenant. When verifying the key, we will send this back to you, so you know who is accessing your API.
Under the hood this upserts and connects an \`ìdentity\` for you.
To disconnect the key from an identity, set \`externalId: null\`.`,
example: "user_123",
}),
meta: z
.record(z.unknown())
.nullish()
Expand Down Expand Up @@ -324,12 +338,21 @@ export const registerV1KeysUpdate = (app: App) =>
const authorizedWorkspaceId = auth.authorizedWorkspaceId;
const rootKeyId = auth.key.id;

const externalId = typeof req.externalId !== "undefined" ? req.externalId : req.ownerId;
const identityId =
typeof externalId === "undefined"
? undefined
: externalId === null
? null
: (await upsertIdentity(db.primary, authorizedWorkspaceId, externalId)).id;

await db.primary
.update(schema.keys)
.set({
name: req.name,
ownerId: req.ownerId,
meta: typeof req.meta === "undefined" ? undefined : JSON.stringify(req.meta ?? {}),
identityId,
expires:
typeof req.expires === "undefined"
? undefined
Expand Down
12 changes: 0 additions & 12 deletions apps/bounce/.editorconfig

This file was deleted.

Loading

0 comments on commit 1e865a7

Please sign in to comment.