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
feature: add git service token management #1136
base: main
Are you sure you want to change the base?
Changes from all commits
daf7d1d
8620616
06bfccd
8437067
9aa48cd
112e0a5
7cc63f6
b756272
49e6237
a5cde53
0bc7571
46634af
8b470a0
a44a495
f40a623
a80ede2
51ac4c4
b6018ac
fb98861
d7e52c2
f3e4c84
0420a63
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Add git services(GitHub & Gitlab) token management |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,8 +1,10 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
import json | ||||||||||||||||||||||||||||||||||||||||||||||
import logging | ||||||||||||||||||||||||||||||||||||||||||||||
import re | ||||||||||||||||||||||||||||||||||||||||||||||
from typing import TYPE_CHECKING, Any, Tuple | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
import aiohttp_cors | ||||||||||||||||||||||||||||||||||||||||||||||
import sqlalchemy as sa | ||||||||||||||||||||||||||||||||||||||||||||||
import trafaret as t | ||||||||||||||||||||||||||||||||||||||||||||||
from aiohttp import web | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -11,6 +13,8 @@ | |||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
from ..models import ( | ||||||||||||||||||||||||||||||||||||||||||||||
MAXIMUM_DOTFILE_SIZE, | ||||||||||||||||||||||||||||||||||||||||||||||
git_token, | ||||||||||||||||||||||||||||||||||||||||||||||
git_tokens, | ||||||||||||||||||||||||||||||||||||||||||||||
keypairs, | ||||||||||||||||||||||||||||||||||||||||||||||
query_accessible_vfolders, | ||||||||||||||||||||||||||||||||||||||||||||||
query_bootstrap_script, | ||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -235,6 +239,47 @@ async def get_bootstrap_script(request: web.Request) -> web.Response: | |||||||||||||||||||||||||||||||||||||||||||||
return web.json_response(script) | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
@auth_required | ||||||||||||||||||||||||||||||||||||||||||||||
@server_status_required(READ_ALLOWED) | ||||||||||||||||||||||||||||||||||||||||||||||
async def get_git_tokens(request: web.Request) -> web.Response: | ||||||||||||||||||||||||||||||||||||||||||||||
root_ctx: RootContext = request.app["_root.context"] | ||||||||||||||||||||||||||||||||||||||||||||||
user_uuid = request["user"]["uuid"] | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
async with root_ctx.db.begin_readonly() as conn: | ||||||||||||||||||||||||||||||||||||||||||||||
query = sa.select([git_tokens.c.domain, git_tokens.c.token]).where( | ||||||||||||||||||||||||||||||||||||||||||||||
git_tokens.c.user_id == user_uuid | ||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+246
to
+251
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any use case of getting other user's git tokens? |
||||||||||||||||||||||||||||||||||||||||||||||
query_result = await conn.execute(query) | ||||||||||||||||||||||||||||||||||||||||||||||
rows = query_result.fetchall() | ||||||||||||||||||||||||||||||||||||||||||||||
result = [{"domain": row["domain"], "token": row["token"]} for row in rows] | ||||||||||||||||||||||||||||||||||||||||||||||
return web.json_response(result, status=200) | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
@auth_required | ||||||||||||||||||||||||||||||||||||||||||||||
@check_api_params( | ||||||||||||||||||||||||||||||||||||||||||||||
t.Dict( | ||||||||||||||||||||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||||||||||||||||||||
t.Key("params"): t.String, | ||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||||||||||
async def update_git_tokens(request: web.Request, params: Any) -> web.Response: | ||||||||||||||||||||||||||||||||||||||||||||||
resp = [] | ||||||||||||||||||||||||||||||||||||||||||||||
root_ctx: RootContext = request.app["_root.context"] | ||||||||||||||||||||||||||||||||||||||||||||||
user_uuid = request["user"]["uuid"] | ||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here. |
||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
# get token list then | ||||||||||||||||||||||||||||||||||||||||||||||
token_list = params["params"] | ||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+262
to
+272
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
First, the name of parameter is too vague. Let's change it. |
||||||||||||||||||||||||||||||||||||||||||||||
async with root_ctx.db.begin() as conn: | ||||||||||||||||||||||||||||||||||||||||||||||
await git_token.insert_update_git_tokens(conn, user_uuid, token_list) | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
token_list_json = json.loads(token_list) | ||||||||||||||||||||||||||||||||||||||||||||||
for key, value in token_list_json.items(): | ||||||||||||||||||||||||||||||||||||||||||||||
resp.append({"domain": key, "token": value}) | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
return web.json_response(resp) | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
async def init(app: web.Application) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||
pass | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -258,5 +303,7 @@ def create_app( | |||||||||||||||||||||||||||||||||||||||||||||
cors.add(app.router.add_route("DELETE", "/dotfiles", delete)) | ||||||||||||||||||||||||||||||||||||||||||||||
cors.add(app.router.add_route("POST", "/bootstrap-script", update_bootstrap_script)) | ||||||||||||||||||||||||||||||||||||||||||||||
cors.add(app.router.add_route("GET", "/bootstrap-script", get_bootstrap_script)) | ||||||||||||||||||||||||||||||||||||||||||||||
cors.add(app.router.add_route("GET", "/git-tokens", get_git_tokens)) | ||||||||||||||||||||||||||||||||||||||||||||||
cors.add(app.router.add_route("PATCH", "/git-tokens", update_git_tokens)) | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
return app, [] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
"""add git_token table | ||
|
||
Revision ID: 3d31a989dd0a | ||
Revises: 02535458c0b3 | ||
Create Date: 2023-08-31 01:04:48.109184 | ||
|
||
""" | ||
import sqlalchemy as sa | ||
from alembic import op | ||
|
||
from ai.backend.manager.models.base import GUID | ||
|
||
# revision identifiers, used by Alembic. | ||
revision = "3d31a989dd0a" | ||
down_revision = "02535458c0b3" | ||
branch_labels = None | ||
depends_on = None | ||
|
||
|
||
def upgrade(): | ||
op.create_table( | ||
"git_tokens", | ||
sa.Column("user_id", GUID(), nullable=False, index=True), | ||
sa.Column("domain", sa.String(length=200), nullable=False, index=True), | ||
sa.Column("token", sa.String(length=200)), | ||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), | ||
sa.Column( | ||
"modified_at", | ||
sa.DateTime(timezone=True), | ||
server_default=sa.text("now()"), | ||
nullable=True, | ||
), | ||
sa.ForeignKeyConstraint( | ||
["user_id"], | ||
["users.uuid"], | ||
name=op.f("fk_git_tokens_user_id_users"), | ||
onupdate="CASCADE", | ||
ondelete="CASCADE", | ||
), | ||
sa.UniqueConstraint("user_id", "domain", name="uq_git_tokens_user_id_domain"), | ||
) | ||
# Create a default github and gitlab tokens | ||
|
||
|
||
def downgrade(): | ||
op.drop_table("git_tokens") |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,93 @@ | ||||||||||
from __future__ import annotations | ||||||||||
|
||||||||||
import datetime | ||||||||||
import json | ||||||||||
import logging | ||||||||||
import uuid | ||||||||||
from typing import Sequence | ||||||||||
|
||||||||||
import graphene | ||||||||||
import sqlalchemy as sa | ||||||||||
from graphene.types.datetime import DateTime as GQLDateTime | ||||||||||
from sqlalchemy.dialects.postgresql import insert | ||||||||||
from sqlalchemy.ext.asyncio import AsyncConnection as SAConnection | ||||||||||
from sqlalchemy.sql import text | ||||||||||
|
||||||||||
from ai.backend.common.logging import BraceStyleAdapter | ||||||||||
|
||||||||||
from .base import GUID, Base, metadata | ||||||||||
|
||||||||||
log = BraceStyleAdapter(logging.getLogger(__spec__.name)) # type: ignore[name-defined] | ||||||||||
|
||||||||||
|
||||||||||
__all__: Sequence[str] = ( | ||||||||||
"git_tokens", | ||||||||||
"GitToken", | ||||||||||
"GitTokenRow", | ||||||||||
"insert_update_git_tokens", | ||||||||||
) | ||||||||||
|
||||||||||
git_tokens = sa.Table( | ||||||||||
"git_tokens", | ||||||||||
metadata, | ||||||||||
sa.Column( | ||||||||||
"user_id", | ||||||||||
GUID, | ||||||||||
sa.ForeignKey("users.uuid", onupdate="CASCADE", ondelete="CASCADE"), | ||||||||||
index=True, | ||||||||||
nullable=False, | ||||||||||
primary_key=True, | ||||||||||
), | ||||||||||
Comment on lines
+33
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nice to have a unique
Comment on lines
+33
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is git token unique for every user? |
||||||||||
sa.Column("domain", sa.String(length=200), unique=True, primary_key=True, nullable=False), | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is git token unique within each domain? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And |
||||||||||
sa.Column("token", sa.String(length=200)), | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a question! |
||||||||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), | ||||||||||
sa.Column( | ||||||||||
"modified_at", | ||||||||||
sa.DateTime(timezone=True), | ||||||||||
server_default=sa.func.now(), | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that this field should be nullable and default value should be null if we think of a token row without any modification |
||||||||||
onupdate=sa.func.current_timestamp(), | ||||||||||
), | ||||||||||
) | ||||||||||
Comment on lines
+30
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we decided to use ORM table, please define the table in ORM |
||||||||||
|
||||||||||
|
||||||||||
async def insert_update_git_tokens( | ||||||||||
conn: SAConnection, user_uuid: uuid.UUID, token_list_str: str | ||||||||||
) -> None: | ||||||||||
token_list = json.loads(token_list_str) | ||||||||||
domain_list = [] | ||||||||||
Comment on lines
+56
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This variable is actually of type
Suggested change
|
||||||||||
# check domain_list | ||||||||||
for key, value in token_list.items(): | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Let's use more clear name! |
||||||||||
domain_list.append(key) | ||||||||||
# delete some non-exist key on domain list | ||||||||||
delete_stmt = sa.delete(git_tokens).where( | ||||||||||
sa.and_(git_tokens.c.user_id == user_uuid), sa.not_(git_tokens.c.domain.in_(domain_list)) | ||||||||||
) | ||||||||||
await conn.execute(delete_stmt) | ||||||||||
|
||||||||||
for key, value in token_list.items(): | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Same here |
||||||||||
data = { | ||||||||||
"user_id": user_uuid, | ||||||||||
"domain": key, | ||||||||||
"token": value, | ||||||||||
"modified_at": datetime.datetime.now(), | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that we do not need to fill this part because of the field's |
||||||||||
} | ||||||||||
query = insert(git_tokens).values(data) | ||||||||||
query = query.on_conflict_do_update( | ||||||||||
index_elements=["user_id", "domain"], | ||||||||||
set_=dict(token=query.excluded.token, modified_at=text("NOW()")), | ||||||||||
) | ||||||||||
await conn.execute(query) | ||||||||||
|
||||||||||
await conn.commit() | ||||||||||
|
||||||||||
|
||||||||||
class GitToken(graphene.ObjectType): | ||||||||||
user_id = graphene.UUID() | ||||||||||
domain = graphene.String() | ||||||||||
token = graphene.String() | ||||||||||
created_at = GQLDateTime() | ||||||||||
modified_at = GQLDateTime() | ||||||||||
|
||||||||||
|
||||||||||
class GitTokenRow(Base): | ||||||||||
__table__ = git_tokens | ||||||||||
Comment on lines
+92
to
+93
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we define the table in ORM, we can remove this overriding part. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'd like to import classes or functions, not modules.