API - add routes to manage oauth clients
This commit is contained in:
parent
ca9ba138b3
commit
489710c29d
@ -30,6 +30,7 @@ def upgrade():
|
|||||||
sa.PrimaryKeyConstraint('id')
|
sa.PrimaryKeyConstraint('id')
|
||||||
)
|
)
|
||||||
op.create_index(op.f('ix_oauth2_client_client_id'), 'oauth2_client', ['client_id'], unique=False)
|
op.create_index(op.f('ix_oauth2_client_client_id'), 'oauth2_client', ['client_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_oauth2_client_user_id'), 'oauth2_client', ['user_id'], unique=False)
|
||||||
op.create_table('oauth2_code',
|
op.create_table('oauth2_code',
|
||||||
sa.Column('code', sa.String(length=120), nullable=False),
|
sa.Column('code', sa.String(length=120), nullable=False),
|
||||||
sa.Column('client_id', sa.String(length=48), nullable=True),
|
sa.Column('client_id', sa.String(length=48), nullable=True),
|
||||||
@ -46,6 +47,8 @@ def upgrade():
|
|||||||
sa.PrimaryKeyConstraint('id'),
|
sa.PrimaryKeyConstraint('id'),
|
||||||
sa.UniqueConstraint('code')
|
sa.UniqueConstraint('code')
|
||||||
)
|
)
|
||||||
|
op.create_index('ix_oauth2_code_client_id', 'oauth2_code', ['client_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_oauth2_code_user_id'), 'oauth2_code', ['user_id'], unique=False)
|
||||||
op.create_table('oauth2_token',
|
op.create_table('oauth2_token',
|
||||||
sa.Column('client_id', sa.String(length=48), nullable=True),
|
sa.Column('client_id', sa.String(length=48), nullable=True),
|
||||||
sa.Column('token_type', sa.String(length=40), nullable=True),
|
sa.Column('token_type', sa.String(length=40), nullable=True),
|
||||||
@ -63,14 +66,19 @@ def upgrade():
|
|||||||
sa.UniqueConstraint('access_token')
|
sa.UniqueConstraint('access_token')
|
||||||
)
|
)
|
||||||
op.create_index(op.f('ix_oauth2_token_refresh_token'), 'oauth2_token', ['refresh_token'], unique=False)
|
op.create_index(op.f('ix_oauth2_token_refresh_token'), 'oauth2_token', ['refresh_token'], unique=False)
|
||||||
|
op.create_index(op.f('ix_oauth2_token_user_id'), 'oauth2_token', ['user_id'], unique=False)
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index(op.f('ix_oauth2_token_user_id'), table_name='oauth2_token')
|
||||||
op.drop_index(op.f('ix_oauth2_token_refresh_token'), table_name='oauth2_token')
|
op.drop_index(op.f('ix_oauth2_token_refresh_token'), table_name='oauth2_token')
|
||||||
op.drop_table('oauth2_token')
|
op.drop_table('oauth2_token')
|
||||||
|
op.drop_index(op.f('ix_oauth2_code_user_id'), table_name='oauth2_code')
|
||||||
|
op.drop_index('ix_oauth2_code_client_id', table_name='oauth2_code')
|
||||||
op.drop_table('oauth2_code')
|
op.drop_table('oauth2_code')
|
||||||
|
op.drop_index(op.f('ix_oauth2_client_user_id'), table_name='oauth2_client')
|
||||||
op.drop_index(op.f('ix_oauth2_client_client_id'), table_name='oauth2_client')
|
op.drop_index(op.f('ix_oauth2_client_client_id'), table_name='oauth2_client')
|
||||||
op.drop_table('oauth2_client')
|
op.drop_table('oauth2_client')
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
@ -38,6 +38,7 @@ def create_oauth_client(metadata: Dict, user: User) -> OAuth2Client:
|
|||||||
"""
|
"""
|
||||||
client_metadata = {
|
client_metadata = {
|
||||||
'client_name': metadata['client_name'],
|
'client_name': metadata['client_name'],
|
||||||
|
'client_description': metadata.get('client_description'),
|
||||||
'client_uri': metadata['client_uri'],
|
'client_uri': metadata['client_uri'],
|
||||||
'redirect_uris': metadata['redirect_uris'],
|
'redirect_uris': metadata['redirect_uris'],
|
||||||
'scope': check_scope(metadata['scope']),
|
'scope': check_scope(metadata['scope']),
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import time
|
import time
|
||||||
from typing import Dict
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from authlib.integrations.sqla_oauth2 import (
|
from authlib.integrations.sqla_oauth2 import (
|
||||||
OAuth2AuthorizationCodeMixin,
|
OAuth2AuthorizationCodeMixin,
|
||||||
OAuth2ClientMixin,
|
OAuth2ClientMixin,
|
||||||
OAuth2TokenMixin,
|
OAuth2TokenMixin,
|
||||||
)
|
)
|
||||||
|
from sqlalchemy.engine.base import Connection
|
||||||
|
from sqlalchemy.event import listens_for
|
||||||
from sqlalchemy.ext.declarative import DeclarativeMeta
|
from sqlalchemy.ext.declarative import DeclarativeMeta
|
||||||
|
from sqlalchemy.orm.mapper import Mapper
|
||||||
|
from sqlalchemy.orm.session import Session
|
||||||
|
|
||||||
from fittrackee import db
|
from fittrackee import db
|
||||||
|
|
||||||
@ -18,27 +22,59 @@ class OAuth2Client(BaseModel, OAuth2ClientMixin):
|
|||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_id = db.Column(
|
user_id = db.Column(
|
||||||
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')
|
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True
|
||||||
)
|
)
|
||||||
user = db.relationship('User')
|
user = db.relationship('User')
|
||||||
|
|
||||||
def serialize(self) -> Dict:
|
def serialize(self, with_secret: bool = False) -> Dict:
|
||||||
return {
|
client = {
|
||||||
'client_id': self.client_id,
|
'client_id': self.client_id,
|
||||||
'client_secret': self.client_secret,
|
'client_description': self.client_description,
|
||||||
'id': self.id,
|
'id': self.id,
|
||||||
|
'issued_at': time.strftime(
|
||||||
|
'%a, %d %B %Y %H:%M:%S GMT',
|
||||||
|
time.gmtime(self.client_id_issued_at),
|
||||||
|
),
|
||||||
'name': self.client_name,
|
'name': self.client_name,
|
||||||
'redirect_uris': self.redirect_uris,
|
'redirect_uris': self.redirect_uris,
|
||||||
|
'scope': self.scope,
|
||||||
'website': self.client_uri,
|
'website': self.client_uri,
|
||||||
}
|
}
|
||||||
|
if with_secret:
|
||||||
|
client['client_secret'] = self.client_secret
|
||||||
|
return client
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client_description(self) -> Optional[str]:
|
||||||
|
return self.client_metadata.get('client_description')
|
||||||
|
|
||||||
|
|
||||||
|
@listens_for(OAuth2Client, 'after_delete')
|
||||||
|
def on_old_oauth2_delete(
|
||||||
|
mapper: Mapper, connection: Connection, old_oauth2_client: OAuth2Client
|
||||||
|
) -> None:
|
||||||
|
@listens_for(db.Session, 'after_flush', once=True)
|
||||||
|
def receive_after_flush(session: Session, context: Any) -> None:
|
||||||
|
session.query(OAuth2AuthorizationCode).filter(
|
||||||
|
OAuth2AuthorizationCode.client_id == old_oauth2_client.client_id
|
||||||
|
).delete(synchronize_session=False)
|
||||||
|
session.query(OAuth2Token).filter(
|
||||||
|
OAuth2Token.client_id == old_oauth2_client.client_id
|
||||||
|
).delete(synchronize_session=False)
|
||||||
|
|
||||||
|
|
||||||
class OAuth2AuthorizationCode(BaseModel, OAuth2AuthorizationCodeMixin):
|
class OAuth2AuthorizationCode(BaseModel, OAuth2AuthorizationCodeMixin):
|
||||||
__tablename__ = 'oauth2_code'
|
__tablename__ = 'oauth2_code'
|
||||||
|
__table_args__ = (
|
||||||
|
db.Index(
|
||||||
|
'ix_oauth2_code_client_id',
|
||||||
|
'client_id',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_id = db.Column(
|
user_id = db.Column(
|
||||||
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')
|
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True
|
||||||
)
|
)
|
||||||
user = db.relationship('User')
|
user = db.relationship('User')
|
||||||
|
|
||||||
@ -48,7 +84,7 @@ class OAuth2Token(BaseModel, OAuth2TokenMixin):
|
|||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
user_id = db.Column(
|
user_id = db.Column(
|
||||||
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')
|
db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), index=True
|
||||||
)
|
)
|
||||||
user = db.relationship('User')
|
user = db.relationship('User')
|
||||||
|
|
||||||
|
@ -3,8 +3,13 @@ from typing import Dict, Tuple, Union
|
|||||||
from flask import Blueprint, Response, request
|
from flask import Blueprint, Response, request
|
||||||
|
|
||||||
from fittrackee import db
|
from fittrackee import db
|
||||||
|
from fittrackee.oauth2.models import OAuth2Client
|
||||||
from fittrackee.oauth2.server import require_auth
|
from fittrackee.oauth2.server import require_auth
|
||||||
from fittrackee.responses import HttpResponse, InvalidPayloadErrorResponse
|
from fittrackee.responses import (
|
||||||
|
HttpResponse,
|
||||||
|
InvalidPayloadErrorResponse,
|
||||||
|
NotFoundErrorResponse,
|
||||||
|
)
|
||||||
from fittrackee.users.models import User
|
from fittrackee.users.models import User
|
||||||
|
|
||||||
from .client import create_oauth_client
|
from .client import create_oauth_client
|
||||||
@ -18,6 +23,36 @@ EXPECTED_METADATA_KEYS = [
|
|||||||
'redirect_uris',
|
'redirect_uris',
|
||||||
'scope',
|
'scope',
|
||||||
]
|
]
|
||||||
|
DEFAULT_PER_PAGE = 5
|
||||||
|
|
||||||
|
|
||||||
|
@oauth_blueprint.route('/oauth/apps', methods=['GET'])
|
||||||
|
@require_auth()
|
||||||
|
def get_clients(auth_user: User) -> Dict:
|
||||||
|
params = request.args.copy()
|
||||||
|
page = int(params.get('page', 1))
|
||||||
|
per_page = DEFAULT_PER_PAGE
|
||||||
|
clients_pagination = (
|
||||||
|
OAuth2Client.query.filter_by(user_id=auth_user.id)
|
||||||
|
.order_by(OAuth2Client.id.desc())
|
||||||
|
.paginate(page, per_page, False)
|
||||||
|
)
|
||||||
|
clients = clients_pagination.items
|
||||||
|
return {
|
||||||
|
'status': 'success',
|
||||||
|
'data': {
|
||||||
|
'clients': [
|
||||||
|
client.serialize(with_secret=False) for client in clients
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'pagination': {
|
||||||
|
'has_next': clients_pagination.has_next,
|
||||||
|
'has_prev': clients_pagination.has_prev,
|
||||||
|
'page': clients_pagination.page,
|
||||||
|
'pages': clients_pagination.pages,
|
||||||
|
'total': clients_pagination.total,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@oauth_blueprint.route('/oauth/apps', methods=['POST'])
|
@oauth_blueprint.route('/oauth/apps', methods=['POST'])
|
||||||
@ -48,12 +83,47 @@ def create_client(auth_user: User) -> Union[HttpResponse, Tuple[Dict, int]]:
|
|||||||
return (
|
return (
|
||||||
{
|
{
|
||||||
'status': 'created',
|
'status': 'created',
|
||||||
'data': {'client': new_client.serialize()},
|
'data': {'client': new_client.serialize(with_secret=True)},
|
||||||
},
|
},
|
||||||
201,
|
201,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@oauth_blueprint.route('/oauth/apps/<string:client_id>', methods=['GET'])
|
||||||
|
@require_auth()
|
||||||
|
def get_client(auth_user: User, client_id: str) -> Union[Dict, HttpResponse]:
|
||||||
|
client = OAuth2Client.query.filter_by(
|
||||||
|
id=client_id,
|
||||||
|
user_id=auth_user.id,
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not client:
|
||||||
|
return NotFoundErrorResponse('OAuth client not found')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': 'success',
|
||||||
|
'data': {'client': client.serialize(with_secret=False)},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@oauth_blueprint.route('/oauth/apps/<string:client_id>', methods=['DELETE'])
|
||||||
|
@require_auth()
|
||||||
|
def delete_client(
|
||||||
|
auth_user: User, client_id: str
|
||||||
|
) -> Union[Tuple[Dict, int], HttpResponse]:
|
||||||
|
client = OAuth2Client.query.filter_by(
|
||||||
|
id=client_id,
|
||||||
|
user_id=auth_user.id,
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not client:
|
||||||
|
return NotFoundErrorResponse('OAuth client not found')
|
||||||
|
|
||||||
|
db.session.delete(client)
|
||||||
|
db.session.commit()
|
||||||
|
return {'status': 'no content'}, 204
|
||||||
|
|
||||||
|
|
||||||
@oauth_blueprint.route('/oauth/authorize', methods=['POST'])
|
@oauth_blueprint.route('/oauth/authorize', methods=['POST'])
|
||||||
@require_auth()
|
@require_auth()
|
||||||
def authorize(auth_user: User) -> Response:
|
def authorize(auth_user: User) -> Response:
|
||||||
|
@ -102,7 +102,7 @@ class ApiTestCaseMixin(RandomMixin):
|
|||||||
|
|
||||||
def create_oauth_client_and_issue_token(
|
def create_oauth_client_and_issue_token(
|
||||||
self, app: Flask, user: User, scope: Optional[str] = None
|
self, app: Flask, user: User, scope: Optional[str] = None
|
||||||
) -> Tuple[FlaskClient, OAuth2Client, str]:
|
) -> Tuple[FlaskClient, OAuth2Client, str, str]:
|
||||||
client, auth_token = self.get_test_client_and_auth_token(
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
app, user.email
|
app, user.email
|
||||||
)
|
)
|
||||||
@ -121,7 +121,7 @@ class ApiTestCaseMixin(RandomMixin):
|
|||||||
headers=dict(content_type='multipart/form-data'),
|
headers=dict(content_type='multipart/form-data'),
|
||||||
)
|
)
|
||||||
data = json.loads(response.data.decode())
|
data = json.loads(response.data.decode())
|
||||||
return client, oauth_client, data.get('access_token')
|
return client, oauth_client, data.get('access_token'), auth_token
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def assert_400(
|
def assert_400(
|
||||||
|
@ -58,6 +58,26 @@ class TestCreateOAuth2Client:
|
|||||||
|
|
||||||
assert oauth_client.client_name == client_name
|
assert oauth_client.client_name == client_name
|
||||||
|
|
||||||
|
def test_oauth_client_has_no_description_when_not_provided_in_metadata(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
oauth_client = create_oauth_client(TEST_METADATA, user_1)
|
||||||
|
|
||||||
|
assert oauth_client.client_description is None
|
||||||
|
|
||||||
|
def test_oauth_client_has_expected_description(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client_description = random_string()
|
||||||
|
client_metadata: Dict = {
|
||||||
|
**TEST_METADATA,
|
||||||
|
'client_description': client_description,
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth_client = create_oauth_client(client_metadata, user_1)
|
||||||
|
|
||||||
|
assert oauth_client.client_description == client_description
|
||||||
|
|
||||||
def test_oauth_client_has_expected_client_uri(
|
def test_oauth_client_has_expected_client_uri(
|
||||||
self, app: Flask, user_1: User
|
self, app: Flask, user_1: User
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -7,6 +7,44 @@ from ..mixins import RandomMixin
|
|||||||
|
|
||||||
class TestOAuthClientSerialize(RandomMixin):
|
class TestOAuthClientSerialize(RandomMixin):
|
||||||
def test_it_returns_oauth_client(self, app: Flask) -> None:
|
def test_it_returns_oauth_client(self, app: Flask) -> None:
|
||||||
|
oauth_client = OAuth2Client(
|
||||||
|
id=self.random_int(),
|
||||||
|
client_id=self.random_string(),
|
||||||
|
client_id_issued_at=1653738796,
|
||||||
|
)
|
||||||
|
oauth_client.set_client_metadata(
|
||||||
|
{
|
||||||
|
'client_name': self.random_string(),
|
||||||
|
'client_description': self.random_string(),
|
||||||
|
'redirect_uris': [self.random_string()],
|
||||||
|
'client_uri': self.random_domain(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
serialized_oauth_client = oauth_client.serialize()
|
||||||
|
|
||||||
|
assert serialized_oauth_client['client_id'] == oauth_client.client_id
|
||||||
|
assert (
|
||||||
|
serialized_oauth_client['client_description']
|
||||||
|
== oauth_client.client_description
|
||||||
|
)
|
||||||
|
assert 'client_secret' not in serialized_oauth_client
|
||||||
|
assert (
|
||||||
|
serialized_oauth_client['issued_at']
|
||||||
|
== 'Sat, 28 May 2022 11:53:16 GMT'
|
||||||
|
)
|
||||||
|
assert serialized_oauth_client['id'] == oauth_client.id
|
||||||
|
assert serialized_oauth_client['name'] == oauth_client.client_name
|
||||||
|
assert (
|
||||||
|
serialized_oauth_client['redirect_uris']
|
||||||
|
== oauth_client.redirect_uris
|
||||||
|
)
|
||||||
|
assert serialized_oauth_client['scope'] == oauth_client.scope
|
||||||
|
assert serialized_oauth_client['website'] == oauth_client.client_uri
|
||||||
|
|
||||||
|
def test_it_returns_oauth_client_with_client_secret(
|
||||||
|
self, app: Flask
|
||||||
|
) -> None:
|
||||||
oauth_client = OAuth2Client(
|
oauth_client = OAuth2Client(
|
||||||
id=self.random_int(),
|
id=self.random_int(),
|
||||||
client_id=self.random_string(),
|
client_id=self.random_string(),
|
||||||
@ -20,17 +58,9 @@ class TestOAuthClientSerialize(RandomMixin):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
serialized_oauth_client = oauth_client.serialize()
|
serialized_oauth_client = oauth_client.serialize(with_secret=True)
|
||||||
|
|
||||||
assert serialized_oauth_client['client_id'] == oauth_client.client_id
|
|
||||||
assert (
|
assert (
|
||||||
serialized_oauth_client['client_secret']
|
serialized_oauth_client['client_secret']
|
||||||
== oauth_client.client_secret
|
== oauth_client.client_secret
|
||||||
)
|
)
|
||||||
assert serialized_oauth_client['id'] == oauth_client.id
|
|
||||||
assert serialized_oauth_client['name'] == oauth_client.client_name
|
|
||||||
assert (
|
|
||||||
serialized_oauth_client['redirect_uris']
|
|
||||||
== oauth_client.redirect_uris
|
|
||||||
)
|
|
||||||
assert serialized_oauth_client['website'] == oauth_client.client_uri
|
|
||||||
|
@ -8,7 +8,11 @@ from flask import Flask
|
|||||||
from urllib3.util import parse_url
|
from urllib3.util import parse_url
|
||||||
from werkzeug.test import TestResponse
|
from werkzeug.test import TestResponse
|
||||||
|
|
||||||
from fittrackee.oauth2.models import OAuth2Client, OAuth2Token
|
from fittrackee.oauth2.models import (
|
||||||
|
OAuth2AuthorizationCode,
|
||||||
|
OAuth2Client,
|
||||||
|
OAuth2Token,
|
||||||
|
)
|
||||||
from fittrackee.users.models import User
|
from fittrackee.users.models import User
|
||||||
|
|
||||||
from ..mixins import ApiTestCaseMixin
|
from ..mixins import ApiTestCaseMixin
|
||||||
@ -120,6 +124,7 @@ class TestOAuthClientCreation(ApiTestCaseMixin):
|
|||||||
data = json.loads(response.data.decode())
|
data = json.loads(response.data.decode())
|
||||||
assert data['data']['client']['client_id'] == client_id
|
assert data['data']['client']['client_id'] == client_id
|
||||||
assert data['data']['client']['client_secret'] == client_secret
|
assert data['data']['client']['client_secret'] == client_secret
|
||||||
|
assert data['data']['client']['id'] is not None
|
||||||
assert (
|
assert (
|
||||||
data['data']['client']['name']
|
data['data']['client']['name']
|
||||||
== TEST_OAUTH_CLIENT_METADATA['client_name']
|
== TEST_OAUTH_CLIENT_METADATA['client_name']
|
||||||
@ -222,6 +227,31 @@ class TestOAuthClientAuthorization(ApiTestCaseMixin):
|
|||||||
|
|
||||||
self.assert_400(response, error_message='invalid payload')
|
self.assert_400(response, error_message='invalid payload')
|
||||||
|
|
||||||
|
def test_it_creates_authorization_code(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
oauth_client = self.create_oauth_client(user_1)
|
||||||
|
|
||||||
|
client.post(
|
||||||
|
self.route,
|
||||||
|
data={
|
||||||
|
'client_id': oauth_client.client_id,
|
||||||
|
'response_type': 'code',
|
||||||
|
},
|
||||||
|
headers=dict(
|
||||||
|
Authorization=f'Bearer {auth_token}',
|
||||||
|
content_type='multipart/form-data',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
code = OAuth2AuthorizationCode.query.filter_by(
|
||||||
|
client_id=oauth_client.client_id
|
||||||
|
).first()
|
||||||
|
assert code is not None
|
||||||
|
|
||||||
def test_it_returns_code_in_url(self, app: Flask, user_1: User) -> None:
|
def test_it_returns_code_in_url(self, app: Flask, user_1: User) -> None:
|
||||||
client, auth_token = self.get_test_client_and_auth_token(
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
app, user_1.email
|
app, user_1.email
|
||||||
@ -384,6 +414,7 @@ class TestOAuthTokenRevocation(ApiTestCaseMixin):
|
|||||||
client,
|
client,
|
||||||
oauth_client,
|
oauth_client,
|
||||||
access_token,
|
access_token,
|
||||||
|
_,
|
||||||
) = self.create_oauth_client_and_issue_token(app, user_1)
|
) = self.create_oauth_client_and_issue_token(app, user_1)
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
@ -401,3 +432,342 @@ class TestOAuthTokenRevocation(ApiTestCaseMixin):
|
|||||||
client_id=oauth_client.client_id
|
client_id=oauth_client.client_id
|
||||||
).first()
|
).first()
|
||||||
assert token.access_token_revoked_at is not None
|
assert token.access_token_revoked_at is not None
|
||||||
|
|
||||||
|
|
||||||
|
class TestOAuthGetClients(ApiTestCaseMixin):
|
||||||
|
route = '/api/oauth/apps'
|
||||||
|
|
||||||
|
def test_it_returns_error_if_not_authenticated(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.get(self.route, content_type='application/json')
|
||||||
|
|
||||||
|
self.assert_401(response)
|
||||||
|
|
||||||
|
def test_it_returns_empty_list_when_no_clients(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
self.route,
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data['status'] == 'success'
|
||||||
|
assert data['data']['clients'] == []
|
||||||
|
|
||||||
|
def test_it_returns_pagination(self, app: Flask, user_1: User) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
[self.create_oauth_client(user_1) for _ in range(7)]
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
self.route,
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data['status'] == 'success'
|
||||||
|
assert len(data['data']['clients']) == 5
|
||||||
|
assert data['pagination'] == {
|
||||||
|
'has_next': True,
|
||||||
|
'has_prev': False,
|
||||||
|
'page': 1,
|
||||||
|
'pages': 2,
|
||||||
|
'total': 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_it_returns_page_2(self, app: Flask, user_1: User) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
[self.create_oauth_client(user_1) for _ in range(6)]
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
f'{self.route}?page=2',
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data['status'] == 'success'
|
||||||
|
assert len(data['data']['clients']) == 1
|
||||||
|
assert data['pagination'] == {
|
||||||
|
'has_next': False,
|
||||||
|
'has_prev': True,
|
||||||
|
'page': 2,
|
||||||
|
'pages': 2,
|
||||||
|
'total': 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_it_returns_clients_order_by_id_descending(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
clients = [self.create_oauth_client(user_1) for _ in range(7)]
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
self.route,
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data['status'] == 'success'
|
||||||
|
assert data['data']['clients'][0]['client_id'] == clients[6].client_id
|
||||||
|
assert data['data']['clients'][4]['client_id'] == clients[2].client_id
|
||||||
|
|
||||||
|
def test_it_does_not_returns_clients_from_another_user(
|
||||||
|
self, app: Flask, user_1: User, user_2: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
self.create_oauth_client(user_2)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
self.route,
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data['status'] == 'success'
|
||||||
|
assert data['data']['clients'] == []
|
||||||
|
|
||||||
|
|
||||||
|
class TestOAuthGetClient(ApiTestCaseMixin):
|
||||||
|
route = '/api/oauth/apps/{client_id}'
|
||||||
|
|
||||||
|
def test_it_returns_error_when_not_authenticated(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
self.route.format(client_id=self.random_int()),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_401(response)
|
||||||
|
|
||||||
|
def test_it_returns_error_when_client_not_found(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
self.route.format(client_id=self.random_int()),
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_404_with_message(response, 'OAuth client not found')
|
||||||
|
|
||||||
|
def test_it_returns_user_oauth_client(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
client_description = self.random_string()
|
||||||
|
oauth_client = self.create_oauth_client(
|
||||||
|
user_1,
|
||||||
|
metadata={
|
||||||
|
**TEST_OAUTH_CLIENT_METADATA,
|
||||||
|
'client_description': client_description,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
client_id = oauth_client.id
|
||||||
|
client_client_id = oauth_client.client_id
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
self.route.format(client_id=client_id),
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = json.loads(response.data.decode())
|
||||||
|
assert data['data']['client']['client_id'] == client_client_id
|
||||||
|
assert 'client_secret' not in data['data']['client']
|
||||||
|
assert (
|
||||||
|
data['data']['client']['client_description'] == client_description
|
||||||
|
)
|
||||||
|
assert data['data']['client']['id'] == client_id
|
||||||
|
assert (
|
||||||
|
data['data']['client']['name']
|
||||||
|
== TEST_OAUTH_CLIENT_METADATA['client_name']
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
data['data']['client']['redirect_uris']
|
||||||
|
== TEST_OAUTH_CLIENT_METADATA['redirect_uris']
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
data['data']['client']['website']
|
||||||
|
== TEST_OAUTH_CLIENT_METADATA['client_uri']
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_it_does_not_return_oauth_client_from_another_user(
|
||||||
|
self, app: Flask, user_1: User, user_2: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
oauth_client = self.create_oauth_client(user_2)
|
||||||
|
client_id = oauth_client.id
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
self.route.format(client_id=client_id),
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_404_with_message(response, 'OAuth client not found')
|
||||||
|
|
||||||
|
|
||||||
|
class TestOAuthDeleteClient(ApiTestCaseMixin):
|
||||||
|
route = '/api/oauth/apps/{client_id}'
|
||||||
|
|
||||||
|
def test_it_returns_error_when_not_authenticated(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
self.route.format(client_id=self.random_int()),
|
||||||
|
content_type='application/json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_401(response)
|
||||||
|
|
||||||
|
def test_it_returns_error_when_client_not_found(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
self.route.format(client_id=self.random_int()),
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_404_with_message(response, 'OAuth client not found')
|
||||||
|
|
||||||
|
def test_it_deletes_user_oauth_client(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
oauth_client = self.create_oauth_client(user_1)
|
||||||
|
client_id = oauth_client.id
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
self.route.format(client_id=client_id),
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
deleted_client = OAuth2Client.query.filter_by(id=client_id).first()
|
||||||
|
assert deleted_client is None
|
||||||
|
|
||||||
|
def test_it_deletes_user_authorized_oauth_client(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
oauth_client = self.create_oauth_client(user_1)
|
||||||
|
self.authorize_client(client, oauth_client, auth_token)
|
||||||
|
client_id = oauth_client.id
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
self.route.format(client_id=client_id),
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
deleted_client = OAuth2Client.query.filter_by(id=client_id).first()
|
||||||
|
assert deleted_client is None
|
||||||
|
|
||||||
|
def test_it_deletes_existing_code_associated_to_client(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
oauth_client = self.create_oauth_client(user_1)
|
||||||
|
code = self.authorize_client(client, oauth_client, auth_token)
|
||||||
|
client_id = oauth_client.id
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
self.route.format(client_id=client_id),
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
deleted_code = OAuth2AuthorizationCode.query.filter_by(
|
||||||
|
code=code[0]
|
||||||
|
).first()
|
||||||
|
assert deleted_code is None
|
||||||
|
|
||||||
|
def test_it_deletes_existing_token_associated_to_client(
|
||||||
|
self, app: Flask, user_1: User
|
||||||
|
) -> None:
|
||||||
|
(
|
||||||
|
client,
|
||||||
|
oauth_client,
|
||||||
|
access_token,
|
||||||
|
auth_token,
|
||||||
|
) = self.create_oauth_client_and_issue_token(app, user_1)
|
||||||
|
client_id = oauth_client.id
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
self.route.format(client_id=client_id),
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
||||||
|
token = OAuth2Token.query.filter_by(access_token=access_token).first()
|
||||||
|
assert token is None
|
||||||
|
|
||||||
|
def test_it_can_not_delete_oauth_client_from_another_user(
|
||||||
|
self, app: Flask, user_1: User, user_2: User
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
oauth_client = self.create_oauth_client(user_2)
|
||||||
|
client_id = oauth_client.id
|
||||||
|
|
||||||
|
response = client.delete(
|
||||||
|
self.route.format(client_id=client_id),
|
||||||
|
content_type='application/json',
|
||||||
|
headers=dict(Authorization=f'Bearer {auth_token}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assert_404_with_message(response, 'OAuth client not found')
|
||||||
|
client = OAuth2Client.query.filter_by(id=client_id).first()
|
||||||
|
assert client is not None
|
||||||
|
@ -49,6 +49,7 @@ class TestOAuth2ScopesWithReadAccess(OAuth2ScopesTestCase):
|
|||||||
client,
|
client,
|
||||||
oauth_client,
|
oauth_client,
|
||||||
access_token,
|
access_token,
|
||||||
|
_,
|
||||||
) = self.create_oauth_client_and_issue_token(
|
) = self.create_oauth_client_and_issue_token(
|
||||||
app, user_1, scope=self.scope
|
app, user_1, scope=self.scope
|
||||||
)
|
)
|
||||||
@ -74,6 +75,7 @@ class TestOAuth2ScopesWithReadAccess(OAuth2ScopesTestCase):
|
|||||||
client,
|
client,
|
||||||
oauth_client,
|
oauth_client,
|
||||||
access_token,
|
access_token,
|
||||||
|
_,
|
||||||
) = self.create_oauth_client_and_issue_token(
|
) = self.create_oauth_client_and_issue_token(
|
||||||
app, user_1_admin, scope=self.scope
|
app, user_1_admin, scope=self.scope
|
||||||
)
|
)
|
||||||
@ -106,6 +108,7 @@ class TestOAuth2ScopesWithReadAccess(OAuth2ScopesTestCase):
|
|||||||
client,
|
client,
|
||||||
oauth_client,
|
oauth_client,
|
||||||
access_token,
|
access_token,
|
||||||
|
_,
|
||||||
) = self.create_oauth_client_and_issue_token(
|
) = self.create_oauth_client_and_issue_token(
|
||||||
app, user_1, scope=self.scope
|
app, user_1, scope=self.scope
|
||||||
)
|
)
|
||||||
@ -135,6 +138,7 @@ class TestOAuth2ScopesWithReadAccess(OAuth2ScopesTestCase):
|
|||||||
client,
|
client,
|
||||||
oauth_client,
|
oauth_client,
|
||||||
access_token,
|
access_token,
|
||||||
|
_,
|
||||||
) = self.create_oauth_client_and_issue_token(
|
) = self.create_oauth_client_and_issue_token(
|
||||||
app, user_1, scope=self.scope
|
app, user_1, scope=self.scope
|
||||||
)
|
)
|
||||||
@ -165,6 +169,7 @@ class TestOAuth2ScopesWithReadAccess(OAuth2ScopesTestCase):
|
|||||||
client,
|
client,
|
||||||
oauth_client,
|
oauth_client,
|
||||||
access_token,
|
access_token,
|
||||||
|
_,
|
||||||
) = self.create_oauth_client_and_issue_token(
|
) = self.create_oauth_client_and_issue_token(
|
||||||
app, user_1_admin, scope=self.scope
|
app, user_1_admin, scope=self.scope
|
||||||
)
|
)
|
||||||
@ -196,6 +201,7 @@ class TestOAuth2ScopesWithReadAccess(OAuth2ScopesTestCase):
|
|||||||
client,
|
client,
|
||||||
oauth_client,
|
oauth_client,
|
||||||
access_token,
|
access_token,
|
||||||
|
_,
|
||||||
) = self.create_oauth_client_and_issue_token(
|
) = self.create_oauth_client_and_issue_token(
|
||||||
app, user_1, scope=self.scope
|
app, user_1, scope=self.scope
|
||||||
)
|
)
|
||||||
@ -226,6 +232,7 @@ class TestOAuth2ScopesWithReadAndWriteAccess(ApiTestCaseMixin):
|
|||||||
client,
|
client,
|
||||||
oauth_client,
|
oauth_client,
|
||||||
access_token,
|
access_token,
|
||||||
|
_,
|
||||||
) = self.create_oauth_client_and_issue_token(
|
) = self.create_oauth_client_and_issue_token(
|
||||||
app, user_1, scope=self.scope
|
app, user_1, scope=self.scope
|
||||||
)
|
)
|
||||||
@ -245,6 +252,7 @@ class TestOAuth2ScopesWithReadAndWriteAccess(ApiTestCaseMixin):
|
|||||||
client,
|
client,
|
||||||
oauth_client,
|
oauth_client,
|
||||||
access_token,
|
access_token,
|
||||||
|
_,
|
||||||
) = self.create_oauth_client_and_issue_token(
|
) = self.create_oauth_client_and_issue_token(
|
||||||
app, user_1, scope=self.scope
|
app, user_1, scope=self.scope
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user