mirror of
https://github.com/outline/outline.git
synced 2026-02-25 14:00:27 -06:00
Allow returning team API keys for admins from apiKeys.list (#7766)
* Allow returning team apiKeys.list for admins from apiKeys.list * Filter apikeys in store
This commit is contained in:
@@ -16,10 +16,13 @@ class ApiKey extends ParanoidModel {
|
||||
@observable
|
||||
expiresAt?: string;
|
||||
|
||||
/** An optional datetime that the API key was last used at. */
|
||||
/** Timestamp that the API key was last used. */
|
||||
@observable
|
||||
lastActiveAt?: string;
|
||||
|
||||
/** The user ID that the API key belongs to. */
|
||||
userId: string;
|
||||
|
||||
/** The plain text value of the API key, only available on creation. */
|
||||
value: string;
|
||||
|
||||
|
||||
@@ -13,12 +13,14 @@ import Text from "~/components/Text";
|
||||
import { createApiKey } from "~/actions/definitions/apiKeys";
|
||||
import useActionContext from "~/hooks/useActionContext";
|
||||
import useCurrentTeam from "~/hooks/useCurrentTeam";
|
||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||
import usePolicy from "~/hooks/usePolicy";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import ApiKeyListItem from "./components/ApiKeyListItem";
|
||||
|
||||
function ApiKeys() {
|
||||
const team = useCurrentTeam();
|
||||
const user = useCurrentUser();
|
||||
const { t } = useTranslation();
|
||||
const { apiKeys } = useStores();
|
||||
const can = usePolicy(team);
|
||||
@@ -79,7 +81,8 @@ function ApiKeys() {
|
||||
</Text>
|
||||
<PaginatedList
|
||||
fetch={apiKeys.fetchPage}
|
||||
items={apiKeys.orderedData}
|
||||
items={apiKeys.personalApiKeys}
|
||||
options={{ userId: user.id }}
|
||||
heading={<h2>{t("Personal keys")}</h2>}
|
||||
renderItem={(apiKey: ApiKey) => (
|
||||
<ApiKeyListItem
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { computed } from "mobx";
|
||||
import ApiKey from "~/models/ApiKey";
|
||||
import RootStore from "./RootStore";
|
||||
import Store, { RPCAction } from "./base/Store";
|
||||
@@ -8,4 +9,12 @@ export default class ApiKeysStore extends Store<ApiKey> {
|
||||
constructor(rootStore: RootStore) {
|
||||
super(rootStore, ApiKey);
|
||||
}
|
||||
|
||||
@computed
|
||||
get personalApiKeys() {
|
||||
const userId = this.rootStore.auth.user?.id;
|
||||
return userId
|
||||
? this.orderedData.filter((key) => key.userId === userId)
|
||||
: [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { and, isOwner, isTeamModel, isTeamMutable } from "./utils";
|
||||
|
||||
allow(User, "createApiKey", Team, (actor, team) =>
|
||||
and(
|
||||
//
|
||||
isTeamModel(actor, team),
|
||||
isTeamMutable(actor),
|
||||
!actor.isViewer,
|
||||
@@ -16,4 +15,18 @@ allow(User, "createApiKey", Team, (actor, team) =>
|
||||
)
|
||||
);
|
||||
|
||||
allow(User, ["read", "update", "delete"], ApiKey, isOwner);
|
||||
allow(User, "listApiKeys", Team, (actor, team) =>
|
||||
and(
|
||||
//
|
||||
isTeamModel(actor, team),
|
||||
actor.isAdmin
|
||||
)
|
||||
);
|
||||
|
||||
allow(User, ["read", "update", "delete"], ApiKey, (actor, apiKey) =>
|
||||
and(
|
||||
isOwner(actor, apiKey),
|
||||
actor.isAdmin ||
|
||||
!!actor.team?.getPreference(TeamPreference.MembersCanCreateApiKey)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ allow(User, "inviteUser", Team, (actor, team) =>
|
||||
)
|
||||
);
|
||||
|
||||
allow(User, ["update", "readDetails"], User, (actor, user) =>
|
||||
allow(User, ["update", "readDetails", "listApiKeys"], User, (actor, user) =>
|
||||
or(
|
||||
//
|
||||
isTeamAdmin(actor, user),
|
||||
|
||||
@@ -3,6 +3,7 @@ import ApiKey from "@server/models/ApiKey";
|
||||
export default function presentApiKey(apiKey: ApiKey) {
|
||||
return {
|
||||
id: apiKey.id,
|
||||
userId: apiKey.userId,
|
||||
name: apiKey.name,
|
||||
value: apiKey.value,
|
||||
last4: apiKey.last4,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { buildApiKey, buildUser } from "@server/test/factories";
|
||||
import { buildAdmin, buildApiKey, buildUser } from "@server/test/factories";
|
||||
import { getTestServer } from "@server/test/support";
|
||||
|
||||
const server = getTestServer();
|
||||
@@ -47,25 +47,58 @@ describe("#apiKeys.create", () => {
|
||||
});
|
||||
|
||||
describe("#apiKeys.list", () => {
|
||||
it("should return api keys of a user", async () => {
|
||||
const now = new Date();
|
||||
it("should return api keys of the specified user", async () => {
|
||||
const user = await buildUser();
|
||||
await buildApiKey({
|
||||
name: "My API Key",
|
||||
userId: user.id,
|
||||
expiresAt: now,
|
||||
});
|
||||
const admin = await buildAdmin({ teamId: user.teamId });
|
||||
await buildApiKey({ userId: user.id });
|
||||
|
||||
const res = await server.post("/api/apiKeys.list", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
userId: user.id,
|
||||
token: admin.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data[0].name).toEqual("My API Key");
|
||||
expect(body.data[0].expiresAt).toEqual(now.toISOString());
|
||||
expect(body.data.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("should return api keys of the specified user for admin", async () => {
|
||||
const user = await buildUser();
|
||||
const admin = await buildAdmin({ teamId: user.teamId });
|
||||
await buildApiKey({ userId: user.id });
|
||||
await buildApiKey({ userId: admin.id });
|
||||
|
||||
const res = await server.post("/api/apiKeys.list", {
|
||||
body: {
|
||||
userId: admin.id,
|
||||
token: admin.getJwtToken(),
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("should return api keys of all users for admin", async () => {
|
||||
const admin = await buildAdmin();
|
||||
const user = await buildUser({ teamId: admin.teamId });
|
||||
await buildApiKey({ userId: admin.id });
|
||||
await buildApiKey({ userId: user.id });
|
||||
await buildApiKey();
|
||||
|
||||
const res = await server.post("/api/apiKeys.list", {
|
||||
body: {
|
||||
token: admin.getJwtToken(),
|
||||
},
|
||||
});
|
||||
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(2);
|
||||
});
|
||||
|
||||
it("should require authentication", async () => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import Router from "koa-router";
|
||||
import { WhereOptions } from "sequelize";
|
||||
import { UserRole } from "@shared/types";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { transaction } from "@server/middlewares/transaction";
|
||||
import validate from "@server/middlewares/validate";
|
||||
import { ApiKey, Event } from "@server/models";
|
||||
import { authorize } from "@server/policies";
|
||||
import { ApiKey, Event, User } from "@server/models";
|
||||
import { authorize, cannot } from "@server/policies";
|
||||
import { presentApiKey } from "@server/presenters";
|
||||
import { APIContext, AuthenticationType } from "@server/types";
|
||||
import pagination from "../middlewares/pagination";
|
||||
@@ -54,12 +55,40 @@ router.post(
|
||||
"apiKeys.list",
|
||||
auth({ role: UserRole.Member }),
|
||||
pagination(),
|
||||
async (ctx: APIContext) => {
|
||||
const { user } = ctx.state.auth;
|
||||
validate(T.APIKeysListSchema),
|
||||
async (ctx: APIContext<T.APIKeysListReq>) => {
|
||||
const { userId } = ctx.input.body;
|
||||
const actor = ctx.state.auth.user;
|
||||
|
||||
let where: WhereOptions<User> = {
|
||||
teamId: actor.teamId,
|
||||
};
|
||||
|
||||
if (cannot(actor, "listApiKeys", actor.team)) {
|
||||
where = {
|
||||
...where,
|
||||
id: actor.id,
|
||||
};
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
const user = await User.findByPk(userId);
|
||||
authorize(actor, "listApiKeys", user);
|
||||
|
||||
where = {
|
||||
...where,
|
||||
id: userId,
|
||||
};
|
||||
}
|
||||
|
||||
const keys = await ApiKey.findAll({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
required: true,
|
||||
where,
|
||||
},
|
||||
],
|
||||
order: [["createdAt", "DESC"]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
|
||||
@@ -12,6 +12,15 @@ export const APIKeysCreateSchema = BaseSchema.extend({
|
||||
|
||||
export type APIKeysCreateReq = z.infer<typeof APIKeysCreateSchema>;
|
||||
|
||||
export const APIKeysListSchema = BaseSchema.extend({
|
||||
body: z.object({
|
||||
/** The owner of the API key */
|
||||
userId: z.string().uuid().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type APIKeysListReq = z.infer<typeof APIKeysListSchema>;
|
||||
|
||||
export const APIKeysDeleteSchema = BaseSchema.extend({
|
||||
body: z.object({
|
||||
/** API Key Id */
|
||||
|
||||
Reference in New Issue
Block a user