Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api-keys): complete revoke use case #3500

Merged
merged 7 commits into from
Nov 3, 2023
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
69 changes: 62 additions & 7 deletions apps/dashboard/components/api-keys/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Table from "@mui/joy/Table"
import { getServerSession } from "next-auth"

import { redirect } from "next/navigation"
import { Typography } from "@mui/joy"
import { Divider, Typography } from "@mui/joy"

import RevokeKey from "./revoke"

Expand All @@ -30,33 +30,88 @@ const ApiKeysList = async () => {

const keys = await apiKeys(token)

const activeKeys = keys.filter(({ expired, revoked }) => !expired && !revoked)
const expiredKeys = keys.filter(({ expired }) => expired)
const revokedKeys = keys.filter(({ revoked }) => revoked)

return (
<>
<Typography fontSize={22}>Active Keys</Typography>
<Table aria-label="basic table">
<thead>
<tr>
<th>API Key ID</th>
<th>Name</th>
<th>Created At</th>
<th>API Key ID</th>
<th>Expires At</th>
<th>Last Used</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{keys.map(({ id, name, createdAt, expiresAt }) => (
{activeKeys.map(({ id, name, expiresAt, lastUsedAt }) => (
<tr key={id}>
<td>{id}</td>
<td>{name}</td>
<td>{formatDate(createdAt)}</td>
<td>{id}</td>
<td>{formatDate(expiresAt)}</td>
<td>{lastUsedAt ? formatDate(lastUsedAt) : "-"}</td>
<td>
<RevokeKey id={id} />
</td>
</tr>
))}
</tbody>
</Table>
{keys.length === 0 && <Typography>No keys to display.</Typography>}
{activeKeys.length === 0 && <Typography>No keys to display.</Typography>}

<Divider />

<Typography fontSize={22}>Revoked Keys</Typography>
<Table aria-label="basic table">
<thead>
<tr>
<th>Name</th>
<th>API Key ID</th>
<th>Created At</th>
<th>Last Used</th>
</tr>
</thead>
<tbody>
{revokedKeys.map(({ id, name, expiresAt, createdAt }) => (
<tr key={id}>
<td>{name}</td>
<td>{id}</td>
<td>{formatDate(createdAt)}</td>
<td>{formatDate(expiresAt)}</td>
</tr>
))}
</tbody>
</Table>
{revokedKeys.length === 0 && <Typography>No keys to display.</Typography>}

<Divider />

<Typography fontSize={22}>Expired Keys</Typography>
<Table aria-label="basic table">
<thead>
<tr>
<th>Name</th>
<th>API Key ID</th>
<th>Created At</th>
<th>Last Used</th>
</tr>
</thead>
<tbody>
{expiredKeys.map(({ id, name, expiresAt, createdAt }) => (
<tr key={id}>
<td>{name}</td>
<td>{id}</td>
<td>{formatDate(createdAt)}</td>
<td>{formatDate(expiresAt)}</td>
</tr>
))}
</tbody>
</Table>
{expiredKeys.length === 0 && <Typography>No keys to display.</Typography>}
</>
)
}
Expand Down
16 changes: 14 additions & 2 deletions apps/dashboard/services/graphql/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,12 @@ export type AccountUpdateNotificationSettingsPayload = {
export type ApiKey = {
readonly __typename: 'ApiKey';
readonly createdAt: Scalars['DateTime']['output'];
readonly expired: Scalars['Boolean']['output'];
readonly expiresAt: Scalars['DateTime']['output'];
readonly id: Scalars['ID']['output'];
readonly lastUsedAt?: Maybe<Scalars['DateTime']['output']>;
readonly name: Scalars['String']['output'];
readonly revoked: Scalars['Boolean']['output'];
};

export type ApiKeyCreateInput = {
Expand Down Expand Up @@ -1906,7 +1909,7 @@ export type ApiKeyCreateMutationVariables = Exact<{
}>;


export type ApiKeyCreateMutation = { readonly __typename: 'Mutation', readonly apiKeyCreate: { readonly __typename: 'ApiKeyCreatePayload', readonly apiKeySecret: string, readonly apiKey: { readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly createdAt: string, readonly expiresAt: string } } };
export type ApiKeyCreateMutation = { readonly __typename: 'Mutation', readonly apiKeyCreate: { readonly __typename: 'ApiKeyCreatePayload', readonly apiKeySecret: string, readonly apiKey: { readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly createdAt: string, readonly revoked: boolean, readonly expired: boolean, readonly lastUsedAt?: string | null, readonly expiresAt: string } } };

export type ApiKeyRevokeMutationVariables = Exact<{
input: ApiKeyRevokeInput;
Expand Down Expand Up @@ -1937,7 +1940,7 @@ export type UserEmailDeleteMutation = { readonly __typename: 'Mutation', readonl
export type ApiKeysQueryVariables = Exact<{ [key: string]: never; }>;


export type ApiKeysQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly apiKeys: ReadonlyArray<{ readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly createdAt: string, readonly expiresAt: string }> } | null };
export type ApiKeysQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly apiKeys: ReadonlyArray<{ readonly __typename: 'ApiKey', readonly id: string, readonly name: string, readonly createdAt: string, readonly revoked: boolean, readonly expired: boolean, readonly lastUsedAt?: string | null, readonly expiresAt: string }> } | null };

export type GetPaginatedTransactionsQueryVariables = Exact<{
first?: InputMaybe<Scalars['Int']['input']>;
Expand Down Expand Up @@ -1968,6 +1971,9 @@ export const ApiKeyCreateDocument = gql`
id
name
createdAt
revoked
expired
lastUsedAt
expiresAt
}
apiKeySecret
Expand Down Expand Up @@ -2146,6 +2152,9 @@ export const ApiKeysDocument = gql`
id
name
createdAt
revoked
expired
lastUsedAt
expiresAt
}
}
Expand Down Expand Up @@ -2964,9 +2973,12 @@ export type AccountUpdateNotificationSettingsPayloadResolvers<ContextType = any,

export type ApiKeyResolvers<ContextType = any, ParentType extends ResolversParentTypes['ApiKey'] = ResolversParentTypes['ApiKey']> = {
createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
expired?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
expiresAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
lastUsedAt?: Resolver<Maybe<ResolversTypes['DateTime']>, ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
revoked?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};

Expand Down
3 changes: 3 additions & 0 deletions apps/dashboard/services/graphql/mutations/api-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ gql`
id
name
createdAt
revoked
expired
lastUsedAt
expiresAt
}
apiKeySecret
Expand Down
7 changes: 5 additions & 2 deletions apps/dashboard/services/graphql/queries/api-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ gql`
id
name
createdAt
revoked
expired
lastUsedAt
expiresAt
}
}
Expand All @@ -20,10 +23,10 @@ export async function apiKeys(token: string) {
const client = apollo(token).getClient()

try {
const data = await client.query<ApiKeysQuery>({
const { data } = await client.query<ApiKeysQuery>({
query: ApiKeysDocument,
})
return data.data.me?.apiKeys || []
return data.me?.apiKeys || []
} catch (err) {
if (err instanceof Error) {
console.error("error", err)
Expand Down
19 changes: 19 additions & 0 deletions bats/core/api-keys/api-keys.bats
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ new_key_name() {

name=$(echo "$key" | jq -r '.name')
[[ "${name}" = "${key_name}" ]] || exit 1
key_id=$(echo "$key" | jq -r '.id')
cache_value "api-key-id" "$key_id"
}

@test "api-keys: can authenticate with api key and list keys" {
Expand All @@ -53,3 +55,20 @@ new_key_name() {
keyName="$(graphql_output '.data.me.apiKeys[-1].name')"
[[ "${keyName}" = "$(read_value 'key_name')" ]] || exit 1
}

@test "api-keys: can revoke key" {
key_id=$(read_value "api-key-id")
variables="{\"input\":{\"id\":\"${key_id}\"}}"

exec_graphql 'alice' 'revoke-api-key' "$variables"

exec_graphql 'alice' 'api-keys'

revoked="$(graphql_output '.data.me.apiKeys[-1].revoked')"
[[ "${revoked}" = "true" ]] || exit 1

exec_graphql 'api-key-secret' 'api-keys'

error="$(graphql_output '.error.code')"
[[ "${error}" = "401" ]] || exit 1
}
1 change: 1 addition & 0 deletions bats/gql/api-keys.gql
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ query apiKeys {
apiKeys {
id
name
revoked
createdAt
expiresAt
}
Expand Down
3 changes: 3 additions & 0 deletions bats/gql/revoke-api-key.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mutation ApiKeyRevoke($input: ApiKeyRevokeInput!) {
apiKeyRevoke(input: $input)
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

This file was deleted.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion core/api-keys/migrations/20231103084227_api_keys_setup.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ CREATE TABLE identity_api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
identity_id UUID REFERENCES identities(id) NOT NULL,
name VARCHAR NOT NULL,
last_used_at TIMESTAMPTZ,
encrypted_key VARCHAR NOT NULL,
active BOOL NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL,
revoked BOOL NOT NULL DEFAULT false,
revoked_at TIMESTAMPTZ
);
9 changes: 9 additions & 0 deletions core/api-keys/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,13 @@ impl ApiKeysApp {
) -> Result<Vec<IdentityApiKey>, ApplicationError> {
Ok(self.identities.list_keys_for_subject(subject_id).await?)
}

pub async fn revoke_api_key_for_subject(
&self,
subject: &str,
key_id: IdentityApiKeyId,
) -> Result<(), ApplicationError> {
self.identities.revoke_api_key(subject, key_id).await?;
Ok(())
}
}
Loading
Loading