oncall-engine/engine/apps/social_auth/backends.py
Ildar Iskhakov c01034c90e
Remove deprecated identify slack scope (#5192)
# What this PR does
Tested on dev (by manually changing the sign in URL in oauth flow). All
works correctly


## Which issue(s) this PR closes

Related to [issue link here]

<!--
*Note*: If you want the issue to be auto-closed once the PR is merged,
change "Related to" to "Closes" in the line above.
If you have more than one GitHub issue that this PR closes, be sure to
preface
each issue link with a [closing
keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue).
This ensures that the issue(s) are auto-closed once the PR has been
merged.
-->

## Checklist

- [ ] Unit, integration, and e2e (if applicable) tests updated
- [ ] Documentation added (or `pr:no public docs` PR label added if not
required)
- [ ] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will
    show up in the autogenerated release notes.
2024-10-18 06:05:41 +00:00

203 lines
7.3 KiB
Python

from urllib.parse import urljoin
from social_core.backends.google import GoogleOAuth2 as BaseGoogleOAuth2
from social_core.backends.slack import SlackOAuth2
from social_core.utils import handle_http_errors
from apps.auth_token.constants import SLACK_AUTH_TOKEN_NAME
from apps.auth_token.models import GoogleOAuth2Token, SlackAuthToken
# Scopes for slack user token.
# It is main purpose - retrieve user data in SlackOAuth2V2 but we are using it in legacy code or weird Slack api cases.
USER_SCOPE = ["channels:read", "users.profile:read", "users:read", "users:read.email"]
# Scopes for slack bot token.
# It is prime token we are using for most requests to Slack api.
# Changing these scopes requires confirmation in Slack app settings.
BOT_SCOPE = [
"app_mentions:read",
"channels:history",
"channels:join",
"channels:read",
"chat:write",
"chat:write.customize",
"chat:write.public",
"commands",
"files:write",
"groups:history",
"groups:read",
"im:history",
"im:read",
"im:write",
"mpim:history",
"reactions:write",
"team:read",
"usergroups:read",
"usergroups:write",
"users.profile:read",
"users:read",
"users:read.email",
"users:write",
]
class GoogleOAuth2(BaseGoogleOAuth2):
REDIRECT_STATE = False
"""
Remove redirect state because we lose session during redirects
"""
STATE_PARAMETER = False
"""
keep `False` to avoid having `BaseGoogleOAuth2` check the `state` query param against a session value
"""
def auth_params(self, state=None):
"""
Override to generate `GoogleOAuth2Token` token to include as `state` query parameter.
https://developers.google.com/identity/protocols/oauth2/web-server#:~:text=Specifies%20any%20string%20value%20that%20your%20application%20uses%20to%20maintain%20state%20between%20your%20authorization%20request%20and%20the%20authorization%20server%27s%20response
"""
params = super().auth_params(state)
_, token_string = GoogleOAuth2Token.create_auth_token(
self.strategy.request.user, self.strategy.request.auth.organization
)
params["state"] = token_string
return params
class SlackOAuth2V2(SlackOAuth2):
"""
Reference to Slack tokens: https://api.slack.com/authentication/token-types
Slack app with granular permissions require using SlackOauth2.0 V2.
SlackOAuth2V2 and its inheritors tune SlackOAuth2 implementation from social core to fit new endpoints
and response shapes.
Read more https://api.slack.com/authentication/oauth-v2
"""
AUTHORIZATION_URL = "https://slack.com/oauth/v2/authorize"
ACCESS_TOKEN_URL = "https://slack.com/api/oauth.v2.access"
AUTH_TOKEN_NAME = SLACK_AUTH_TOKEN_NAME
REDIRECT_STATE = False
"""
Remove redirect state because we lose session during redirects
"""
STATE_PARAMETER = False
EXTRA_DATA = [("id", "id"), ("name", "name"), ("real_name", "real_name"), ("team", "team")]
@handle_http_errors
def auth_complete(self, *args, **kwargs):
"""
Override original method to include auth token in redirect uri and adjust response shape to slack Oauth2.0 V2.
Access token is in the ["authed_user"]["access_token"] field, not in the root of the response.
"""
self.process_error(self.data)
state = self.validate_state()
# add auth token to redirect uri, because it must be the same in all slack auth requests
token_string = self.data.get(self.AUTH_TOKEN_NAME)
if token_string:
self._update_redirect_uri_with_auth_token(token_string)
data, params = None, None
if self.ACCESS_TOKEN_METHOD == "GET":
params = self.auth_complete_params(state)
else:
data = self.auth_complete_params(state)
response = self.request_access_token(
self.access_token_url(),
data=data,
params=params,
headers=self.auth_headers(),
auth=self.auth_complete_credentials(),
method=self.ACCESS_TOKEN_METHOD,
)
self.process_error(response)
# Take access token from the authed_user field, not from the root
access_token = response["authed_user"]["access_token"]
kwargs.update(response=response)
return self.do_auth(access_token, *args, **kwargs)
@handle_http_errors
def do_auth(self, access_token, *args, **kwargs):
"""Finish the auth process once the access_token was retrieved"""
data = self.user_data(access_token, *args, **kwargs)
data.pop("team", None) # we don't want to override team from token by team from user_data request
response = kwargs.get("response") or {}
response.update(data or {})
if "access_token" not in response:
response["access_token"] = access_token
kwargs.update({"response": response, "backend": self})
return self.strategy.authenticate(*args, **kwargs)
def get_scope_argument(self):
param = {}
scopes = self.get_scope()
for k, v in scopes.items():
param[k] = self.SCOPE_SEPARATOR.join(v)
return param
def user_data(self, access_token, *args, **kwargs):
"""
Override original method to load user data using method users.profile.get with users.profile:read scope
"""
r = self.get_json("https://slack.com/api/users.profile.get", params={"token": access_token})
if r["ok"] is False:
r = self.get_json(
"https://slack.com/api/users.profile.get",
headers={"Authorization": f"Bearer {access_token}"},
)
r = r["profile"]
# Emulate shape of return value from original method to not to brake smth inside social_core
response = {}
response["user"] = {}
response["user"]["name"] = r["real_name_normalized"]
response["user"]["email"] = r["email"]
response["team"] = r.get("team", None)
return response
def start(self):
"""Add slack auth token to redirect uri and continue authentication"""
token_string = self._generate_auth_token_string()
self._update_redirect_uri_with_auth_token(token_string)
return super().start()
def _generate_auth_token_string(self) -> str:
_, token_string = SlackAuthToken.create_auth_token(
self.strategy.request.user, self.strategy.request.auth.organization
)
return token_string
def _update_redirect_uri_with_auth_token(self, token_string: str) -> None:
auth_token_param = f"?{self.AUTH_TOKEN_NAME}={token_string}"
self.redirect_uri = urljoin(self.redirect_uri, auth_token_param)
class LoginSlackOAuth2V2(SlackOAuth2V2):
name = "slack-login"
SCOPE_PARAMETER_NAME = "user_scope"
EXTRA_DATA = [
("id", "id"),
("name", "name"),
("real_name", "real_name"),
("team", "team"),
]
def get_scope(self):
return {"user_scope": USER_SCOPE}
# it's named slack-install-free because it was used to install free version of Slack App.
# There is no free/paid version of Slack App anymore, so it's just a name.
SLACK_INSTALLATION_BACKEND = "slack-install-free"
class InstallSlackOAuth2V2(SlackOAuth2V2):
name = SLACK_INSTALLATION_BACKEND
def get_scope(self):
return {"user_scope": USER_SCOPE, "scope": BOT_SCOPE}