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 pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from app import config
|
||||
from app import models
|
||||
|
@ -115,7 +116,7 @@ async def indieauth_authorization_endpoint(
|
|||
"url": registered_client.client_uri,
|
||||
}
|
||||
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(
|
||||
db_session,
|
||||
|
@ -321,8 +322,10 @@ async def _check_access_token(
|
|||
) -> tuple[bool, models.IndieAuthAccessToken | None]:
|
||||
access_token_info = (
|
||||
await db_session.scalars(
|
||||
select(models.IndieAuthAccessToken).where(
|
||||
models.IndieAuthAccessToken.access_token == token
|
||||
select(models.IndieAuthAccessToken)
|
||||
.where(models.IndieAuthAccessToken.access_token == token)
|
||||
.options(
|
||||
joinedload(models.IndieAuthAccessToken.indieauth_authorization_request)
|
||||
)
|
||||
)
|
||||
).one_or_none()
|
||||
|
@ -345,6 +348,7 @@ async def _check_access_token(
|
|||
@dataclass(frozen=True)
|
||||
class AccessTokenInfo:
|
||||
scopes: list[str]
|
||||
client_id: str | None
|
||||
|
||||
|
||||
async def verify_access_token(
|
||||
|
@ -371,9 +375,57 @@ async def verify_access_token(
|
|||
|
||||
return AccessTokenInfo(
|
||||
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")
|
||||
async def indieauth_revocation_endpoint(
|
||||
request: Request,
|
||||
|
|
38
app/main.py
38
app/main.py
|
@ -464,7 +464,12 @@ async def followers(
|
|||
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
) -> ActivityPubResponse | templates.TemplateResponse:
|
||||
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(
|
||||
await _empty_followx_collection(
|
||||
db_session=db_session,
|
||||
|
@ -523,7 +528,12 @@ async def following(
|
|||
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
) -> ActivityPubResponse | templates.TemplateResponse:
|
||||
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(
|
||||
await _empty_followx_collection(
|
||||
db_session=db_session,
|
||||
|
@ -579,22 +589,34 @@ async def following(
|
|||
|
||||
@app.get("/outbox")
|
||||
async def outbox(
|
||||
request: Request,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
) -> 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
|
||||
outbox_objects = (
|
||||
await db_session.scalars(
|
||||
select(models.OutboxObject)
|
||||
.where(
|
||||
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
|
||||
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())
|
||||
.limit(20)
|
||||
)
|
||||
).all()
|
||||
|
||||
return ActivityPubResponse(
|
||||
{
|
||||
"@context": ap.AS_EXTENDED_CTX,
|
||||
|
@ -646,6 +668,14 @@ async def _check_outbox_object_acl(
|
|||
if templates.is_current_user_admin(request):
|
||||
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 [
|
||||
ap.VisibilityEnum.PUBLIC,
|
||||
ap.VisibilityEnum.UNLISTED,
|
||||
|
|
|
@ -465,6 +465,10 @@ class IndieAuthAccessToken(Base):
|
|||
indieauth_authorization_request_id = Column(
|
||||
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)
|
||||
expires_in = Column(Integer, nullable=False)
|
||||
|
|
Loading…
Reference in a new issue