Skip to content

Commit 9509d23

Browse files
siddardh-rasiddardh
andauthored
GET and DELETE method for managing api_key (#3410)
Add GET and DELETE methods to support API Key management --------- Co-authored-by: siddardh <sira@redhat27!>
1 parent ddc3a81 commit 9509d23

File tree

11 files changed

+521
-71
lines changed

11 files changed

+521
-71
lines changed

lib/pbench/client/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,8 +346,8 @@ def create_api_key(self):
346346
Creating an API key will cause the new key to be used instead of a
347347
normal login auth_token until the API key is removed.
348348
"""
349-
response = self.post(api=API.KEY)
350-
self.api_key = response.json()["api_key"]
349+
response = self.post(api=API.KEY, uri_params={"key": ""})
350+
self.api_key = response.json()["key"]
351351
assert self.api_key, f"API key creation failed, {response.json()}"
352352

353353
def remove_api_key(self):

lib/pbench/server/api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ def register_endpoints(api: Api, app: Flask, config: PbenchServerConfig):
127127
api.add_resource(
128128
APIKeyManage,
129129
f"{base_uri}/key",
130+
f"{base_uri}/key/",
131+
f"{base_uri}/key/<string:key>",
130132
endpoint="key",
131133
resource_class_args=(config,),
132134
)

lib/pbench/server/api/resources/api_key.py

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
ApiMethod,
1414
ApiParams,
1515
ApiSchema,
16+
Parameter,
17+
ParamType,
18+
Schema,
1619
)
1720
import pbench.server.auth.auth as Auth
1821
from pbench.server.database.models.api_keys import APIKey, DuplicateApiKey
@@ -26,20 +29,75 @@ def __init__(self, config: PbenchServerConfig):
2629
ApiSchema(
2730
ApiMethod.POST,
2831
OperationCode.CREATE,
32+
query_schema=Schema(
33+
Parameter("label", ParamType.STRING, required=False),
34+
),
35+
audit_type=AuditType.API_KEY,
36+
audit_name="apikey",
37+
authorization=ApiAuthorizationType.NONE,
38+
),
39+
ApiSchema(
40+
ApiMethod.GET,
41+
OperationCode.READ,
42+
uri_schema=Schema(
43+
Parameter("key", ParamType.STRING, required=False),
44+
),
45+
authorization=ApiAuthorizationType.NONE,
46+
),
47+
ApiSchema(
48+
ApiMethod.DELETE,
49+
OperationCode.DELETE,
50+
uri_schema=Schema(
51+
Parameter("key", ParamType.STRING, required=True),
52+
),
2953
audit_type=AuditType.API_KEY,
3054
audit_name="apikey",
3155
authorization=ApiAuthorizationType.NONE,
3256
),
3357
)
3458

59+
def _get(
60+
self, params: ApiParams, request: Request, context: ApiContext
61+
) -> Response:
62+
"""Get a list of API keys associated with the user.
63+
64+
GET /api/v1/key
65+
66+
Returns:
67+
Success: 200 with response containing the requested api_key
68+
or list of api_key
69+
70+
Raises:
71+
APIAbort, reporting "UNAUTHORIZED" or "NOT_FOUND"
72+
"""
73+
user = Auth.token_auth.current_user()
74+
75+
if not user:
76+
raise APIAbort(
77+
HTTPStatus.UNAUTHORIZED,
78+
"User provided access_token is invalid or expired",
79+
)
80+
81+
key_id = params.uri.get("key")
82+
if not key_id:
83+
keys = APIKey.query(user=user)
84+
return [key.as_json() for key in keys]
85+
86+
else:
87+
key = APIKey.query(id=key_id, user=user)
88+
if not key:
89+
raise APIAbort(HTTPStatus.NOT_FOUND, "Requested key not found")
90+
return key[0].as_json()
91+
3592
def _post(
3693
self, params: ApiParams, request: Request, context: ApiContext
3794
) -> Response:
3895
"""
3996
Post request for generating a new persistent API key.
4097
41-
Required headers include
98+
POST /api/v1/key?label=label
4299
100+
Required headers include
43101
Content-Type: application/json
44102
Accept: application/json
45103
@@ -51,6 +109,12 @@ def _post(
51109
APIInternalError, reporting the failure message
52110
"""
53111
user = Auth.token_auth.current_user()
112+
label = params.query.get("label")
113+
114+
if context["raw_params"].uri:
115+
raise APIAbort(
116+
HTTPStatus.BAD_REQUEST, "Key cannot be specified by the user"
117+
)
54118

55119
if not user:
56120
raise APIAbort(
@@ -61,17 +125,51 @@ def _post(
61125
new_key = APIKey.generate_api_key(user)
62126
except Exception as e:
63127
raise APIInternalError(str(e)) from e
64-
65128
try:
66-
key = APIKey(api_key=new_key, user=user)
129+
key = APIKey(key=new_key, user=user, label=label)
67130
key.add()
68131
status = HTTPStatus.CREATED
69132
except DuplicateApiKey:
70133
status = HTTPStatus.OK
71134
except Exception as e:
72135
raise APIInternalError(str(e)) from e
73-
74-
context["auditing"]["attributes"] = {"key": new_key}
75-
response = jsonify({"api_key": new_key})
136+
context["auditing"]["attributes"] = key.as_json()
137+
response = jsonify(key.as_json())
76138
response.status_code = status
77139
return response
140+
141+
def _delete(
142+
self, params: ApiParams, request: Request, context: ApiContext
143+
) -> Response:
144+
"""Delete the requested key.
145+
146+
DELETE /api/v1/key/{key}
147+
148+
Returns:
149+
Success: 200
150+
151+
Raises:
152+
APIAbort, reporting "UNAUTHORIZED" or "NOT_FOUND"
153+
APIInternalError, reporting the failure message
154+
"""
155+
key_id = params.uri["key"]
156+
user = Auth.token_auth.current_user()
157+
158+
if not user:
159+
raise APIAbort(
160+
HTTPStatus.UNAUTHORIZED,
161+
"User provided access_token is invalid or expired",
162+
)
163+
term = {"id": key_id}
164+
if not user.is_admin():
165+
term["user"] = user
166+
keys = APIKey.query(**term)
167+
if not keys:
168+
raise APIAbort(HTTPStatus.NOT_FOUND, "Requested key not found")
169+
key = keys[0]
170+
try:
171+
context["auditing"]["attributes"] = key.as_json()
172+
key.delete()
173+
return "deleted", HTTPStatus.OK
174+
except Exception as e:
175+
raise APIInternalError(str(e)) from e

lib/pbench/server/auth/auth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ def verify_auth_api_key(api_key: str) -> Optional[User]:
119119
None if the api_key is not valid, a `User` object when the api_key is valid.
120120
121121
"""
122-
key = APIKey.query(api_key)
123-
return key.user if key else None
122+
key = APIKey.query(key=api_key)
123+
return key[0].user if key and len(key) == 1 else None
124124

125125

126126
def verify_auth_oidc(auth_token: str) -> Optional[User]:
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Update api_key table with "id" as primary_key
2+
3+
4+
Revision ID: 1a91bc68d6de
5+
Revises: 5679217a62bb
6+
Create Date: 2023-05-03 09:50:29.609672
7+
8+
"""
9+
from alembic import op
10+
import sqlalchemy as sa
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "1a91bc68d6de"
14+
down_revision = "5679217a62bb"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade() -> None:
20+
op.drop_constraint("api_keys_pkey", "api_keys", type_="primary")
21+
op.execute("ALTER TABLE api_keys ADD COLUMN id SERIAL PRIMARY KEY")
22+
op.add_column("api_keys", sa.Column("label", sa.String(length=128), nullable=True))
23+
op.add_column("api_keys", sa.Column("key", sa.String(length=500), nullable=False))
24+
op.create_unique_constraint("api_keys_key_unique", "api_keys", ["key"])
25+
op.drop_column("api_keys", "api_key")
26+
27+
28+
def downgrade() -> None:
29+
op.drop_constraint("api_keys_pkey", "api_keys", type_="primary")
30+
op.drop_column("api_keys", "label")
31+
op.drop_column("api_keys", "id")
32+
op.drop_column("api_keys", "key")
33+
op.create_primary_key("api_keys_pkey", "api_keys", ["api_key"])

lib/pbench/server/database/models/api_keys.py

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
from flask import current_app
44
import jwt
5-
from sqlalchemy import Column, ForeignKey, String
5+
from sqlalchemy import Column, ForeignKey, Integer, String
66
from sqlalchemy.orm import relationship
77

8+
from pbench.server import JSONOBJECT
89
from pbench.server.database.database import Database
910
from pbench.server.database.models import decode_integrity_error, TZDateTime
1011
from pbench.server.database.models.users import User
@@ -47,16 +48,18 @@ class APIKey(Database.Base):
4748
"""Model for storing the API key associated with a user."""
4849

4950
__tablename__ = "api_keys"
50-
api_key = Column(String(500), primary_key=True)
51+
id = Column(Integer, primary_key=True, autoincrement=True)
52+
key = Column(String(500), unique=True, nullable=False)
5153
created = Column(TZDateTime, nullable=False, default=TZDateTime.current_time)
54+
label = Column(String(128), nullable=True)
5255
# ID of the owning user
5356
user_id = Column(String, ForeignKey("users.id"), nullable=False)
5457

5558
# Indirect reference to the owning User record
5659
user = relationship("User")
5760

5861
def __str__(self):
59-
return f"API key {self.api_key}"
62+
return f"API key {self.key}"
6063

6164
def add(self):
6265
"""Add an api_key object to the database."""
@@ -69,35 +72,49 @@ def add(self):
6972
decode_exc = decode_integrity_error(
7073
e, on_duplicate=DuplicateApiKey, on_null=NullKey
7174
)
72-
if decode_exc == e:
75+
if decode_exc is e:
7376
raise APIKeyError(str(e)) from e
7477
else:
7578
raise decode_exc from e
7679

7780
@staticmethod
78-
def query(key: str) -> Optional["APIKey"]:
81+
def query(**kwargs) -> Optional["APIKey"]:
7982
"""Find the given api_key in the database.
8083
8184
Returns:
82-
An APIKey object if found, otherwise None
85+
List of APIKey object if found, otherwise []
8386
"""
84-
return Database.db_session.query(APIKey).filter_by(api_key=key).first()
8587

86-
@staticmethod
87-
def delete(api_key: str):
88-
"""Delete the given api_key.
88+
return (
89+
Database.db_session.query(APIKey)
90+
.filter_by(**kwargs)
91+
.order_by(APIKey.id)
92+
.all()
93+
)
8994

90-
Args:
91-
api_key : the api_key to delete
92-
"""
93-
dbs = Database.db_session
95+
def delete(self):
96+
"""Remove the api_key instance from the database."""
9497
try:
95-
dbs.query(APIKey).filter_by(api_key=api_key).delete()
96-
dbs.commit()
98+
Database.db_session.delete(self)
99+
Database.db_session.commit()
97100
except Exception as e:
98-
dbs.rollback()
101+
Database.db_session.rollback()
99102
raise APIKeyError(f"Error deleting api_key from db : {e}") from e
100103

104+
def as_json(self) -> JSONOBJECT:
105+
"""Return a JSON object for this APIkey object.
106+
107+
Returns:
108+
A JSONOBJECT with all the object fields mapped to appropriate names.
109+
"""
110+
return {
111+
"id": self.id,
112+
"label": self.label,
113+
"key": self.key,
114+
"username": self.user.username,
115+
"created": self.created.isoformat(),
116+
}
117+
101118
@staticmethod
102119
def generate_api_key(user: User):
103120
"""Creates an `api_key` for the requested user

lib/pbench/test/unit/server/auth/test_auth.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -700,11 +700,11 @@ def tio_exc(token: str) -> JSON:
700700
with app.app_context():
701701
monkeypatch.setattr(oidc_client, "token_introspect", tio_exc)
702702
current_app.secret_key = jwt_secret
703-
user = Auth.verify_auth(pbench_drb_api_key)
703+
user = Auth.verify_auth(pbench_drb_api_key.key)
704704
assert user.id == DRB_USER_ID
705705

706706
def test_verify_auth_api_key_invalid(
707-
self, monkeypatch, rsa_keys, make_logger, pbench_drb_api_key_invalid
707+
self, monkeypatch, rsa_keys, make_logger, pbench_invalid_api_key
708708
):
709709
"""Verify api_key verification via Auth.verify_auth() fails
710710
gracefully with an invalid token
@@ -723,5 +723,5 @@ def tio_exc(token: str) -> JSON:
723723
with app.app_context():
724724
monkeypatch.setattr(oidc_client, "token_introspect", tio_exc)
725725
current_app.secret_key = jwt_secret
726-
user = Auth.verify_auth(pbench_drb_api_key_invalid)
726+
user = Auth.verify_auth(pbench_invalid_api_key)
727727
assert user is None

0 commit comments

Comments
 (0)