allow admin role api keys (#2124)

* allow admin role api keys

* bump to rerun deployment

* types needs explicit export now for APIKey

* remove api_key.role, use User.role instead

* fix formatting

* formatting

* formatting

---------

Co-authored-by: Richard Kuo <rkuo@rkuo.com>
This commit is contained in:
rkuo-danswer
2024-08-13 14:00:57 -07:00
committed by GitHub
parent 5dda047999
commit f15d6d2b59
8 changed files with 68 additions and 9 deletions

View File

@ -199,6 +199,9 @@ class ApiKey(Base):
DateTime(timezone=True), server_default=func.now() DateTime(timezone=True), server_default=func.now()
) )
# Add this relationship to access the User object via user_id
user: Mapped["User"] = relationship("User", foreign_keys=[user_id])
class Notification(Base): class Notification(Base):
__tablename__ = "notification" __tablename__ = "notification"

View File

@ -5,6 +5,7 @@ from fastapi import Request
from passlib.hash import sha256_crypt from passlib.hash import sha256_crypt
from pydantic import BaseModel from pydantic import BaseModel
from danswer.auth.schemas import UserRole
from ee.danswer.configs.app_configs import API_KEY_HASH_ROUNDS from ee.danswer.configs.app_configs import API_KEY_HASH_ROUNDS
@ -19,6 +20,7 @@ class ApiKeyDescriptor(BaseModel):
api_key_display: str api_key_display: str
api_key: str | None = None # only present on initial creation api_key: str | None = None # only present on initial creation
api_key_name: str | None = None api_key_name: str | None = None
api_key_role: UserRole
user_id: uuid.UUID user_id: uuid.UUID

View File

@ -2,9 +2,9 @@ import uuid
from fastapi_users.password import PasswordHelper from fastapi_users.password import PasswordHelper
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from danswer.auth.schemas import UserRole
from danswer.configs.constants import DANSWER_API_KEY_DUMMY_EMAIL_DOMAIN from danswer.configs.constants import DANSWER_API_KEY_DUMMY_EMAIL_DOMAIN
from danswer.configs.constants import DANSWER_API_KEY_PREFIX from danswer.configs.constants import DANSWER_API_KEY_PREFIX
from danswer.configs.constants import UNNAMED_KEY_PLACEHOLDER from danswer.configs.constants import UNNAMED_KEY_PLACEHOLDER
@ -22,10 +22,15 @@ def is_api_key_email_address(email: str) -> bool:
def fetch_api_keys(db_session: Session) -> list[ApiKeyDescriptor]: def fetch_api_keys(db_session: Session) -> list[ApiKeyDescriptor]:
api_keys = db_session.scalars(select(ApiKey)).all() api_keys = (
db_session.scalars(select(ApiKey).options(joinedload(ApiKey.user)))
.unique()
.all()
)
return [ return [
ApiKeyDescriptor( ApiKeyDescriptor(
api_key_id=api_key.id, api_key_id=api_key.id,
api_key_role=api_key.user.role,
api_key_display=api_key.api_key_display, api_key_display=api_key.api_key_display,
api_key_name=api_key.name, api_key_name=api_key.name,
user_id=api_key.user_id, user_id=api_key.user_id,
@ -67,7 +72,7 @@ def insert_api_key(
is_active=True, is_active=True,
is_superuser=False, is_superuser=False,
is_verified=True, is_verified=True,
role=UserRole.BASIC, role=api_key_args.role,
) )
db_session.add(api_key_user_row) db_session.add(api_key_user_row)
@ -83,6 +88,7 @@ def insert_api_key(
db_session.commit() db_session.commit()
return ApiKeyDescriptor( return ApiKeyDescriptor(
api_key_id=api_key_row.id, api_key_id=api_key_row.id,
api_key_role=api_key_user_row.role,
api_key_display=api_key_row.api_key_display, api_key_display=api_key_row.api_key_display,
api_key=api_key, api_key=api_key,
api_key_name=api_key_args.name, api_key_name=api_key_args.name,
@ -106,12 +112,14 @@ def update_api_key(
email_name = api_key_args.name or UNNAMED_KEY_PLACEHOLDER email_name = api_key_args.name or UNNAMED_KEY_PLACEHOLDER
api_key_user.email = get_api_key_fake_email(email_name, str(api_key_user.id)) api_key_user.email = get_api_key_fake_email(email_name, str(api_key_user.id))
api_key_user.role = api_key_args.role
db_session.commit() db_session.commit()
return ApiKeyDescriptor( return ApiKeyDescriptor(
api_key_id=existing_api_key.id, api_key_id=existing_api_key.id,
api_key_display=existing_api_key.api_key_display, api_key_display=existing_api_key.api_key_display,
api_key_name=api_key_args.name, api_key_name=api_key_args.name,
api_key_role=api_key_user.role,
user_id=existing_api_key.user_id, user_id=existing_api_key.user_id,
) )
@ -122,6 +130,12 @@ def regenerate_api_key(db_session: Session, api_key_id: int) -> ApiKeyDescriptor
if existing_api_key is None: if existing_api_key is None:
raise ValueError(f"API key with id {api_key_id} does not exist") raise ValueError(f"API key with id {api_key_id} does not exist")
api_key_user = db_session.scalar(
select(User).where(User.id == existing_api_key.user_id) # type: ignore
)
if api_key_user is None:
raise RuntimeError("API Key does not have associated user.")
new_api_key = generate_api_key() new_api_key = generate_api_key()
existing_api_key.hashed_api_key = hash_api_key(new_api_key) existing_api_key.hashed_api_key = hash_api_key(new_api_key)
existing_api_key.api_key_display = build_displayable_api_key(new_api_key) existing_api_key.api_key_display = build_displayable_api_key(new_api_key)
@ -132,6 +146,7 @@ def regenerate_api_key(db_session: Session, api_key_id: int) -> ApiKeyDescriptor
api_key_display=existing_api_key.api_key_display, api_key_display=existing_api_key.api_key_display,
api_key=new_api_key, api_key=new_api_key,
api_key_name=existing_api_key.name, api_key_name=existing_api_key.name,
api_key_role=api_key_user.role,
user_id=existing_api_key.user_id, user_id=existing_api_key.user_id,
) )

View File

@ -1,5 +1,8 @@
from pydantic import BaseModel from pydantic import BaseModel
from danswer.auth.schemas import UserRole
class APIKeyArgs(BaseModel): class APIKeyArgs(BaseModel):
name: str | None = None name: str | None = None
role: UserRole = UserRole.BASIC

View File

@ -1,10 +1,15 @@
import { Form, Formik } from "formik"; import { Form, Formik } from "formik";
import { PopupSpec } from "@/components/admin/connectors/Popup"; import { PopupSpec } from "@/components/admin/connectors/Popup";
import { TextFormField } from "@/components/admin/connectors/Field"; import {
BooleanFormField,
TextFormField,
} from "@/components/admin/connectors/Field";
import { createApiKey, updateApiKey } from "./lib"; import { createApiKey, updateApiKey } from "./lib";
import { Modal } from "@/components/Modal"; import { Modal } from "@/components/Modal";
import { XIcon } from "@/components/icons/icons"; import { XIcon } from "@/components/icons/icons";
import { Button, Divider, Text } from "@tremor/react"; import { Button, Divider, Text } from "@tremor/react";
import { UserRole } from "@/lib/types";
import { APIKey } from "./types";
interface DanswerApiKeyFormProps { interface DanswerApiKeyFormProps {
onClose: () => void; onClose: () => void;
@ -42,14 +47,25 @@ export const DanswerApiKeyForm = ({
<Formik <Formik
initialValues={{ initialValues={{
name: apiKey?.api_key_name || "", name: apiKey?.api_key_name || "",
is_admin: apiKey?.api_key_role == "admin" ?? false,
}} }}
onSubmit={async (values, formikHelpers) => { onSubmit={async (values, formikHelpers) => {
formikHelpers.setSubmitting(true); formikHelpers.setSubmitting(true);
// Map the boolean to a UserRole string
const role: UserRole = values.is_admin ? "admin" : "basic";
// Prepare the payload with the UserRole
const payload = {
...values,
role, // Assign the role directly as a UserRole type
};
let response; let response;
if (isUpdate) { if (isUpdate) {
response = await updateApiKey(apiKey.api_key_id, values); response = await updateApiKey(apiKey.api_key_id, payload);
} else { } else {
response = await createApiKey(values); response = await createApiKey(payload);
} }
formikHelpers.setSubmitting(false); formikHelpers.setSubmitting(false);
if (response.ok) { if (response.ok) {
@ -88,6 +104,15 @@ export const DanswerApiKeyForm = ({
autoCompleteDisabled={true} autoCompleteDisabled={true}
/> />
<BooleanFormField
small
noPadding
alignTop
name="is_admin"
label="Is Admin?"
subtext="If set, this API key will have access to admin level server API's."
/>
<div className="flex"> <div className="flex">
<Button <Button
type="submit" type="submit"

View File

@ -26,6 +26,7 @@ import { Modal } from "@/components/Modal";
import { Spinner } from "@/components/Spinner"; import { Spinner } from "@/components/Spinner";
import { deleteApiKey, regenerateApiKey } from "./lib"; import { deleteApiKey, regenerateApiKey } from "./lib";
import { DanswerApiKeyForm } from "./DanswerApiKeyForm"; import { DanswerApiKeyForm } from "./DanswerApiKeyForm";
import { APIKey } from "./types";
const API_KEY_TEXT = ` const API_KEY_TEXT = `
API Keys allow you to access Danswer APIs programmatically. Click the button below to generate a new API Key. API Keys allow you to access Danswer APIs programmatically. Click the button below to generate a new API Key.
@ -173,6 +174,7 @@ function Main() {
<TableRow> <TableRow>
<TableHeaderCell>Name</TableHeaderCell> <TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>API Key</TableHeaderCell> <TableHeaderCell>API Key</TableHeaderCell>
<TableHeaderCell>Role</TableHeaderCell>
<TableHeaderCell>Regenerate</TableHeaderCell> <TableHeaderCell>Regenerate</TableHeaderCell>
<TableHeaderCell>Delete</TableHeaderCell> <TableHeaderCell>Delete</TableHeaderCell>
</TableRow> </TableRow>
@ -201,6 +203,9 @@ function Main() {
<TableCell className="max-w-64"> <TableCell className="max-w-64">
{apiKey.api_key_display} {apiKey.api_key_display}
</TableCell> </TableCell>
<TableCell className="max-w-64">
{apiKey.api_key_role.toUpperCase()}
</TableCell>
<TableCell> <TableCell>
<div <div
className={` className={`

View File

@ -1,11 +1,15 @@
interface APIKey { import { UserRole } from "@/lib/types";
export interface APIKey {
api_key_id: number; api_key_id: number;
api_key_display: string; api_key_display: string;
api_key: string | null; api_key: string | null;
api_key_name: string | null; api_key_name: string | null;
api_key_role: UserRole;
user_id: string; user_id: string;
} }
interface APIKeyArgs { export interface APIKeyArgs {
name?: string; name?: string;
role: UserRole;
} }

View File

@ -14,13 +14,15 @@ export enum UserStatus {
deactivated = "deactivated", deactivated = "deactivated",
} }
export type UserRole = "basic" | "admin";
export interface User { export interface User {
id: string; id: string;
email: string; email: string;
is_active: string; is_active: string;
is_superuser: string; is_superuser: string;
is_verified: string; is_verified: string;
role: "basic" | "admin"; role: UserRole;
preferences: UserPreferences; preferences: UserPreferences;
status: UserStatus; status: UserStatus;
current_token_created_at?: Date; current_token_created_at?: Date;