Skip to content

Commit 89a80d7

Browse files
authored
Adds personal-access-token table (#83)
* Adds personal-access-token table * Adds graphql-endpoint for PAT creation * Adds logic to store PATs in database * Adds logic to query and delete PATs * Adds hashing to PATs and changes queries so the token does not get displayed * Updates submodule * Removes print of token * Adds last used and intros delete by token id * Adds created at field for PATs * Adds typing where missing * Outsources hash functionality into own file * Updates submodule * Changes query for tokens from user based to all of project and adds created by * Changes delete PATs by removing user id of check * Renames PersonAccessTokenMutation to keep singular convention * Adapts to renamed methods * Adds checks for kern admins at PATs endpoints
1 parent 3d8d098 commit 89a80d7

File tree

9 files changed

+231
-1
lines changed

9 files changed

+231
-1
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Adds personal-access-token-table
2+
3+
Revision ID: f803a0c4343c
4+
Revises: 09311360f8b9
5+
Create Date: 2022-11-23 13:50:24.978181
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
from sqlalchemy.dialects import postgresql
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "f803a0c4343c"
14+
down_revision = "09311360f8b9"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.create_table(
22+
"personal_access_token",
23+
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
24+
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=True),
25+
sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=True),
26+
sa.Column("name", sa.String(), nullable=True),
27+
sa.Column("scope", sa.String(), nullable=True),
28+
sa.Column("created_at", sa.DateTime(), nullable=True),
29+
sa.Column("expires_at", sa.DateTime(), nullable=True),
30+
sa.Column("last_used", sa.DateTime(), nullable=True),
31+
sa.Column("token", sa.String(), nullable=True),
32+
sa.ForeignKeyConstraint(["project_id"], ["project.id"], ondelete="CASCADE"),
33+
sa.ForeignKeyConstraint(["user_id"], ["user.id"], ondelete="CASCADE"),
34+
sa.PrimaryKeyConstraint("id"),
35+
)
36+
op.create_index(
37+
op.f("ix_personal_access_token_project_id"),
38+
"personal_access_token",
39+
["project_id"],
40+
unique=False,
41+
)
42+
op.create_index(
43+
op.f("ix_personal_access_token_user_id"),
44+
"personal_access_token",
45+
["user_id"],
46+
unique=False,
47+
)
48+
# ### end Alembic commands ###
49+
50+
51+
def downgrade():
52+
# ### commands auto generated by Alembic - please adjust! ###
53+
op.drop_index(
54+
op.f("ix_personal_access_token_user_id"), table_name="personal_access_token"
55+
)
56+
op.drop_index(
57+
op.f("ix_personal_access_token_project_id"), table_name="personal_access_token"
58+
)
59+
op.drop_table("personal_access_token")
60+
# ### end Alembic commands ###

controller/personal_access_token/__init__.py

Whitespace-only changes.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from typing import List
2+
from controller.personal_access_token.util import get_token_and_hash
3+
from submodules.model import enums
4+
from datetime import date
5+
from dateutil.relativedelta import relativedelta
6+
7+
from submodules.model.business_objects import personal_access_token
8+
from submodules.model.models import PersonalAccessToken
9+
10+
11+
def get_personal_access_token(
12+
project_id: str, user_id: str, name: str
13+
) -> PersonalAccessToken:
14+
return personal_access_token.get_by_user_and_name(project_id, user_id, name)
15+
16+
17+
def get_all_personal_access_tokens(project_id: str) -> List[PersonalAccessToken]:
18+
return personal_access_token.get_all(project_id)
19+
20+
21+
def create_personal_access_token(
22+
project_id: str, user_id: str, name: str, scope: str, expires_at: str
23+
) -> None:
24+
if personal_access_token.get_by_user_and_name(project_id, user_id, name):
25+
raise Exception(
26+
f"Personal Access Key with name {name} already exists for user/project-combination."
27+
)
28+
29+
if expires_at == enums.TokenExpireAtValues.ONE_MONTH.value:
30+
expires_at = date.today() + relativedelta(months=+1)
31+
elif expires_at == enums.TokenExpireAtValues.THREE_MONTHS.value:
32+
expires_at = date.today() + relativedelta(months=+3)
33+
elif expires_at == enums.TokenExpireAtValues.NEVER.value:
34+
expires_at = None
35+
else:
36+
raise Exception(
37+
f"Option for token expiration date was invalid: Option: {expires_at}."
38+
)
39+
40+
if scope not in [enums.TokenScope.READ.value, enums.TokenScope.READ_WRITE.value]:
41+
raise Exception(f"Option for token scope was invalid: Option: {scope}.")
42+
43+
token, token_hex_dig = get_token_and_hash()
44+
personal_access_token.create(
45+
project_id=project_id,
46+
user_id=user_id,
47+
name=name,
48+
scope=scope,
49+
expires_at=expires_at,
50+
token=token_hex_dig,
51+
with_commit=True,
52+
)
53+
return token
54+
55+
56+
def delete_personal_access_token(project_id: str, token_id: str) -> None:
57+
personal_access_token.delete(project_id, token_id, with_commit=True)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import hashlib
2+
import secrets
3+
4+
5+
def get_token_and_hash():
6+
token = secrets.token_urlsafe(80)
7+
encoded_token = str.encode(token)
8+
hash_token = hashlib.sha256(encoded_token)
9+
token_hex_dig = hash_token.hexdigest()
10+
return token, token_hex_dig
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from controller.auth import manager as auth
2+
from controller.personal_access_token import manager as token_manager
3+
import graphene
4+
5+
6+
class CreatePersonalAccessToken(graphene.Mutation):
7+
class Arguments:
8+
project_id = graphene.ID(required=True)
9+
name = graphene.String(required=True)
10+
scope = graphene.String(required=True)
11+
expires_at = graphene.String(required=True)
12+
13+
token = graphene.String()
14+
15+
def mutate(self, info, project_id: str, name: str, scope: str, expires_at: str):
16+
auth.check_demo_access(info)
17+
auth.check_project_access(info, project_id)
18+
auth.check_admin_access(info)
19+
user_id = auth.get_user_id_by_info(info)
20+
token = token_manager.create_personal_access_token(
21+
project_id=project_id,
22+
user_id=user_id,
23+
name=name,
24+
scope=scope,
25+
expires_at=expires_at,
26+
)
27+
return CreatePersonalAccessToken(token=token)
28+
29+
30+
class DeletePersonalAccessToken(graphene.Mutation):
31+
class Arguments:
32+
project_id = graphene.ID(required=True)
33+
token_id = graphene.ID(required=True)
34+
35+
ok = graphene.Boolean()
36+
37+
def mutate(self, info, project_id: str, token_id: str):
38+
auth.check_demo_access(info)
39+
auth.check_project_access(info, project_id)
40+
auth.check_admin_access(info)
41+
token_manager.delete_personal_access_token(
42+
project_id=project_id, token_id=token_id
43+
)
44+
return DeletePersonalAccessToken(ok=True)
45+
46+
47+
class PersonalAccessTokenMutation(graphene.ObjectType):
48+
create_personal_access_token = CreatePersonalAccessToken.Field()
49+
delete_personal_access_token = DeletePersonalAccessToken.Field()
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import graphene
2+
3+
from controller.auth import manager as auth
4+
from controller.personal_access_token import manager as token_manager
5+
from graphql_api.types import PersonalAccessToken
6+
7+
8+
class PersonalAccessTokenQuery(graphene.ObjectType):
9+
10+
personal_access_token = graphene.Field(
11+
PersonalAccessToken,
12+
project_id=graphene.ID(required=True),
13+
name=graphene.String(required=True),
14+
)
15+
16+
all_personal_access_tokens = graphene.Field(
17+
graphene.List(PersonalAccessToken), project_id=graphene.ID(required=True)
18+
)
19+
20+
def resolve_personal_access_token(
21+
self, info, project_id: str, name: str
22+
) -> PersonalAccessToken:
23+
auth.check_demo_access(info)
24+
auth.check_project_access(info, project_id)
25+
auth.check_admin_access(info)
26+
user_id = auth.get_user_id_by_info(info)
27+
return token_manager.get_personal_access_token(project_id, user_id, name)
28+
29+
def resolve_all_personal_access_tokens(
30+
self, info, project_id: str
31+
) -> PersonalAccessToken:
32+
auth.check_demo_access(info)
33+
auth.check_project_access(info, project_id)
34+
auth.check_admin_access(info)
35+
return token_manager.get_all_personal_access_tokens(project_id)

graphql_api/schema.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from graphql_api.query.data_slice import DataSliceQuery
77
from graphql_api.query.labeling_access_link import LabelingAccessLinkQuery
88
from graphql_api.query.embedding import EmbeddingQuery
9+
from graphql_api.query.personal_access_token import PersonalAccessTokenQuery
910
from graphql_api.query.transfer import TransferQuery
1011
from graphql_api.query.information_source import InformationSourceQuery
1112
from graphql_api.query.knowledge_base import KnowledgeBaseQuery
@@ -46,6 +47,7 @@
4647
from graphql_api.mutation.tokenization import TokenizationMutation
4748
from graphql_api.mutation.weak_supervisor import WeakSupervisionMutation
4849
from graphql_api.mutation.zero_shot import ZeroShotMutation
50+
from graphql_api.mutation.personal_access_token import PersonalAccessTokenMutation
4951

5052

5153
class Query(
@@ -72,6 +74,7 @@ class Query(
7274
WeakSupervisionQuery,
7375
ZeroShotQuery,
7476
RunRecordIDEPayload,
77+
PersonalAccessTokenQuery,
7578
graphene.ObjectType,
7679
):
7780
pass
@@ -100,6 +103,7 @@ class Mutation(
100103
WeakSupervisionMutation,
101104
ZeroShotMutation,
102105
UploadTaskMutation,
106+
PersonalAccessTokenMutation,
103107
graphene.ObjectType,
104108
):
105109
pass

graphql_api/types.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,21 @@ def resolve_docs(self, info):
538538
)
539539

540540

541+
class PersonalAccessToken(graphene.ObjectType):
542+
id = graphene.ID()
543+
project_id = graphene.ID()
544+
name = graphene.String()
545+
scope = graphene.String()
546+
created_by = graphene.String()
547+
created_at = graphene.DateTime()
548+
expires_at = graphene.DateTime()
549+
last_used = graphene.DateTime()
550+
551+
def resolve_created_by(self, info):
552+
name = kratos.resolve_user_name_by_id(self.user_id)
553+
return f"{name.get('first')} {name.get('last')}"
554+
555+
541556
class KnowledgeBase(SQLAlchemyObjectType):
542557
class Meta:
543558
model = models.KnowledgeBase

0 commit comments

Comments
 (0)