Skip to content

feat(integrations): add endpoint for rotating client secret of Internal/Public integrations #69015

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 19, 2024

Conversation

oioki
Copy link
Member

@oioki oioki commented Apr 16, 2024

Adding an endpoint that allows to rotate clientSecret of Internal/Public integrations.
Inspired by #53124

@oioki oioki requested review from mdtro and a team April 16, 2024 18:20
@oioki oioki requested review from a team as code owners April 16, 2024 18:20
@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Apr 16, 2024
@cathteng cathteng requested a review from ykamo001 April 16, 2024 18:57
Comment on lines 22 to 45
def has_object_permission(self, request: Request, view: object, sentry_app: SentryApp):
# organization that owns an integration
org_context = organization_service.get_organization_by_id(id=sentry_app.owner_id)
if org_context is None:
raise Http404

organization = org_context.organization
if organization is None:
raise Http404

# if user is not a member of an organization owning an integration,
# return 404 to avoid leaking integration slug
organizations = (
user_service.get_organizations(user_id=request.user.id)
if request.user.id is not None
else ()
)
if not any(organization.id == org.id for org in organizations):
raise Http404

# permission check inside an organization
self.determine_access(request, organization)
allowed_scopes = set(self.scope_map.get(request.method or "", []))
return any(request.access.has_scope(s) for s in allowed_scopes)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make more sense to just return false/boolean and have the parent/calling method decide what error code it should use? I think instead of 404, a failed permission is more of a 403?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think maybe even a 400 bad request status code might be more accurate? That being said, do we think this is plausible? Can a SentryApp (id or object) exist without a representational organization?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because technically a 404 would mean the sentry_app itself doesn't exist, which isn't really true in this case right?

Copy link
Member Author

@oioki oioki Apr 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically 403 means failed permission, but in this case we don't want to leak info if a specific integration slug exists with different status codes. Take a look from an attacker's perspective:

POST /integration-aaa/rotate-secret/  404 -- integration does not exist
POST /integration-bb/rotate-secret/   403 -- aha, so it exists but I don't have permissions. I can try to build some attack on top of this info

We're taking a similar approach in other places, for example:

if not any(sentry_app.owner_id == org.id for org in organizations):
raise Http404

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh, I see what you're saying, that makes sense!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I doubt we are, but hope we're that consistent across our other API endpoints in terms of behavior and expected status codes


def has_object_permission(self, request: Request, view: object, sentry_app: SentryApp):
# organization that owns an integration
org_context = organization_service.get_organization_by_id(id=sentry_app.owner_id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this is an RPC call, which could have network/side effect errors, would it be worth wrapping in a try/catch, and defaulting to return false for safety?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked other uses of organization_service.get_organization_by_id, and none of them are using this try-catch approach. I see only checks like if not org_context or if org_context is None, so I presume network failures are handled somewhere else (in RPC abstraction?).

"POST": ["org:write", "org:admin"],
}

def has_object_permission(self, request: Request, view: object, sentry_app: SentryApp):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something to help us understand our decisions and debug in the future: should we log when we make a decision in this function?

i.e.

if org_context is None:
   log.info("org does not exist for request", extra={"app_id": app_id, "user_id": user_id, ...etc})
   return False

Or, maybe the exception we raise has a better error message to help both the user and us (for showing in the UI and debugging when users come to us for support)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added logging for now.


def post(self, request: Request, sentry_app: SentryApp) -> Response:
if sentry_app.application is None:
return Response(status=404)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add some more details to the response?

return Response(status=404)

new_token = generate_token()
sentry_app.application.update(client_secret=new_token)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add a try/catch here and respond accordingly? And also log the error? Because this is a DB transaction, there could be errors

if sentry_app.application is None:
return Response(status=404)

new_token = generate_token()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mdtro I know we're making some changes to this method in another PR, is this still safe/intended to be used in this flow?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ykamo001 Yes, this is okay for now. :) When we come back through to improve the app API tokens we can adjust these calls then.

# if user is not a member of an organization owning an integration,
# return 404 to avoid leaking integration slug
organizations = (
user_service.get_organizations(user_id=request.user.id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question here regarding the RPC try/catch

from sentry.services.hybrid_cloud.user.service import user_service


class SentryAppRotateSecretPermission(SentryPermission):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cathteng should superusers be allowed to do this for other orgs on their behalf for support cases?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair. maybe we only let superusers with write privileges do this?

@ykamo001
Copy link
Contributor

Since you pulled the FE changes to another PR, you can probably remove the FE label on the PR, I am not sure it will auto update and remove for you haha

@oioki oioki removed the Scope: Frontend Automatically applied to PRs that change frontend components label Apr 18, 2024
@oioki oioki requested a review from ykamo001 April 18, 2024 12:28
@getsentry getsentry deleted a comment from github-actions bot Apr 18, 2024
@oioki oioki merged commit 1a732c3 into master Apr 19, 2024
52 checks passed
@oioki oioki deleted the feat/sentry-app-rotate-secret-endpoint branch April 19, 2024 07:29
oioki added a commit that referenced this pull request Apr 19, 2024
MichaelSun48 pushed a commit that referenced this pull request Apr 25, 2024
…al/Public integrations (#69015)

Adding an endpoint that allows to rotate `clientSecret` of
Internal/Public integrations.
Inspired by #53124
MichaelSun48 pushed a commit that referenced this pull request Apr 25, 2024
@github-actions github-actions bot locked and limited conversation to collaborators May 4, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Scope: Backend Automatically applied to PRs that change backend components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants