More AP C2S support
This commit is contained in:
parent
5cf54c2782
commit
7b506f2519
3 changed files with 93 additions and 7 deletions
|
@ -13,6 +13,7 @@ from fastapi.responses import JSONResponse
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
from app import config
|
from app import config
|
||||||
from app import models
|
from app import models
|
||||||
|
@ -115,7 +116,7 @@ async def indieauth_authorization_endpoint(
|
||||||
"url": registered_client.client_uri,
|
"url": registered_client.client_uri,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
client = await indieauth.get_client_id_data(client_id)
|
client = await indieauth.get_client_id_data(client_id) # type: ignore
|
||||||
|
|
||||||
return await templates.render_template(
|
return await templates.render_template(
|
||||||
db_session,
|
db_session,
|
||||||
|
@ -321,8 +322,10 @@ async def _check_access_token(
|
||||||
) -> tuple[bool, models.IndieAuthAccessToken | None]:
|
) -> tuple[bool, models.IndieAuthAccessToken | None]:
|
||||||
access_token_info = (
|
access_token_info = (
|
||||||
await db_session.scalars(
|
await db_session.scalars(
|
||||||
select(models.IndieAuthAccessToken).where(
|
select(models.IndieAuthAccessToken)
|
||||||
models.IndieAuthAccessToken.access_token == token
|
.where(models.IndieAuthAccessToken.access_token == token)
|
||||||
|
.options(
|
||||||
|
joinedload(models.IndieAuthAccessToken.indieauth_authorization_request)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
).one_or_none()
|
).one_or_none()
|
||||||
|
@ -345,6 +348,7 @@ async def _check_access_token(
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class AccessTokenInfo:
|
class AccessTokenInfo:
|
||||||
scopes: list[str]
|
scopes: list[str]
|
||||||
|
client_id: str | None
|
||||||
|
|
||||||
|
|
||||||
async def verify_access_token(
|
async def verify_access_token(
|
||||||
|
@ -371,9 +375,57 @@ async def verify_access_token(
|
||||||
|
|
||||||
return AccessTokenInfo(
|
return AccessTokenInfo(
|
||||||
scopes=access_token.scope.split(),
|
scopes=access_token.scope.split(),
|
||||||
|
client_id=(
|
||||||
|
access_token.indieauth_authorization_request.client_id
|
||||||
|
if access_token.indieauth_authorization_request
|
||||||
|
else None
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def check_access_token(
|
||||||
|
request: Request,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
) -> AccessTokenInfo | None:
|
||||||
|
token = request.headers.get("Authorization", "").removeprefix("Bearer ")
|
||||||
|
if not token:
|
||||||
|
return None
|
||||||
|
|
||||||
|
is_token_valid, access_token = await _check_access_token(db_session, token)
|
||||||
|
if not is_token_valid:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not access_token or not access_token.scope:
|
||||||
|
raise ValueError("Should never happen")
|
||||||
|
|
||||||
|
access_token_info = AccessTokenInfo(
|
||||||
|
scopes=access_token.scope.split(),
|
||||||
|
client_id=(
|
||||||
|
access_token.indieauth_authorization_request.client_id
|
||||||
|
if access_token.indieauth_authorization_request
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Authenticated with access token from client_id="
|
||||||
|
f"{access_token_info.client_id} scopes={access_token.scope}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return access_token_info
|
||||||
|
|
||||||
|
|
||||||
|
async def enforce_access_token(
|
||||||
|
request: Request,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
) -> AccessTokenInfo:
|
||||||
|
maybe_access_token_info = await check_access_token(request, db_session)
|
||||||
|
if not maybe_access_token_info:
|
||||||
|
raise HTTPException(status_code=401, detail="access token required")
|
||||||
|
|
||||||
|
return maybe_access_token_info
|
||||||
|
|
||||||
|
|
||||||
@router.post("/revoke_token")
|
@router.post("/revoke_token")
|
||||||
async def indieauth_revocation_endpoint(
|
async def indieauth_revocation_endpoint(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
38
app/main.py
38
app/main.py
|
@ -464,7 +464,12 @@ async def followers(
|
||||||
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||||
) -> ActivityPubResponse | templates.TemplateResponse:
|
) -> ActivityPubResponse | templates.TemplateResponse:
|
||||||
if is_activitypub_requested(request):
|
if is_activitypub_requested(request):
|
||||||
if config.HIDES_FOLLOWERS:
|
maybe_access_token_info = await indieauth.check_access_token(
|
||||||
|
request,
|
||||||
|
db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
if config.HIDES_FOLLOWERS and not maybe_access_token_info:
|
||||||
return ActivityPubResponse(
|
return ActivityPubResponse(
|
||||||
await _empty_followx_collection(
|
await _empty_followx_collection(
|
||||||
db_session=db_session,
|
db_session=db_session,
|
||||||
|
@ -523,7 +528,12 @@ async def following(
|
||||||
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||||
) -> ActivityPubResponse | templates.TemplateResponse:
|
) -> ActivityPubResponse | templates.TemplateResponse:
|
||||||
if is_activitypub_requested(request):
|
if is_activitypub_requested(request):
|
||||||
if config.HIDES_FOLLOWING:
|
maybe_access_token_info = await indieauth.check_access_token(
|
||||||
|
request,
|
||||||
|
db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
if config.HIDES_FOLLOWING and not maybe_access_token_info:
|
||||||
return ActivityPubResponse(
|
return ActivityPubResponse(
|
||||||
await _empty_followx_collection(
|
await _empty_followx_collection(
|
||||||
db_session=db_session,
|
db_session=db_session,
|
||||||
|
@ -579,22 +589,34 @@ async def following(
|
||||||
|
|
||||||
@app.get("/outbox")
|
@app.get("/outbox")
|
||||||
async def outbox(
|
async def outbox(
|
||||||
|
request: Request,
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||||
) -> ActivityPubResponse:
|
) -> ActivityPubResponse:
|
||||||
|
maybe_access_token_info = await indieauth.check_access_token(
|
||||||
|
request,
|
||||||
|
db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Default restrictions unless the request is authenticated with an access token
|
||||||
|
restricted_where = [
|
||||||
|
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
|
||||||
|
models.OutboxObject.ap_type.in_(["Create", "Note", "Article", "Announce"]),
|
||||||
|
]
|
||||||
|
|
||||||
# By design, we only show the last 20 public activities in the oubox
|
# By design, we only show the last 20 public activities in the oubox
|
||||||
outbox_objects = (
|
outbox_objects = (
|
||||||
await db_session.scalars(
|
await db_session.scalars(
|
||||||
select(models.OutboxObject)
|
select(models.OutboxObject)
|
||||||
.where(
|
.where(
|
||||||
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
|
|
||||||
models.OutboxObject.is_deleted.is_(False),
|
models.OutboxObject.is_deleted.is_(False),
|
||||||
models.OutboxObject.ap_type.in_(["Create", "Announce"]),
|
*([] if maybe_access_token_info else restricted_where),
|
||||||
)
|
)
|
||||||
.order_by(models.OutboxObject.ap_published_at.desc())
|
.order_by(models.OutboxObject.ap_published_at.desc())
|
||||||
.limit(20)
|
.limit(20)
|
||||||
)
|
)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
return ActivityPubResponse(
|
return ActivityPubResponse(
|
||||||
{
|
{
|
||||||
"@context": ap.AS_EXTENDED_CTX,
|
"@context": ap.AS_EXTENDED_CTX,
|
||||||
|
@ -646,6 +668,14 @@ async def _check_outbox_object_acl(
|
||||||
if templates.is_current_user_admin(request):
|
if templates.is_current_user_admin(request):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
maybe_access_token_info = await indieauth.check_access_token(
|
||||||
|
request,
|
||||||
|
db_session,
|
||||||
|
)
|
||||||
|
if maybe_access_token_info:
|
||||||
|
# TODO: check scopes
|
||||||
|
return None
|
||||||
|
|
||||||
if ap_object.visibility in [
|
if ap_object.visibility in [
|
||||||
ap.VisibilityEnum.PUBLIC,
|
ap.VisibilityEnum.PUBLIC,
|
||||||
ap.VisibilityEnum.UNLISTED,
|
ap.VisibilityEnum.UNLISTED,
|
||||||
|
|
|
@ -465,6 +465,10 @@ class IndieAuthAccessToken(Base):
|
||||||
indieauth_authorization_request_id = Column(
|
indieauth_authorization_request_id = Column(
|
||||||
Integer, ForeignKey("indieauth_authorization_request.id"), nullable=True
|
Integer, ForeignKey("indieauth_authorization_request.id"), nullable=True
|
||||||
)
|
)
|
||||||
|
indieauth_authorization_request = relationship(
|
||||||
|
IndieAuthAuthorizationRequest,
|
||||||
|
uselist=False,
|
||||||
|
)
|
||||||
|
|
||||||
access_token = Column(String, nullable=False, unique=True, index=True)
|
access_token = Column(String, nullable=False, unique=True, index=True)
|
||||||
expires_in = Column(Integer, nullable=False)
|
expires_in = Column(Integer, nullable=False)
|
||||||
|
|
Loading…
Reference in a new issue