Skip to content

Commit

Permalink
feat: support the new authentification system (#184)
Browse files Browse the repository at this point in the history
Try to connect to the new authentication system first and then falls back to the old one
  • Loading branch information
renaudjester committed Oct 28, 2024
1 parent e9776b4 commit 615b421
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 5 deletions.
1 change: 1 addition & 0 deletions copernicusmarine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
OtherOptionsPassedWithCreateTemplate,
)
from copernicusmarine.core_functions.credentials_utils import (
CouldNotConnectToAuthenticationSystem,
CredentialsCannotBeNone,
InvalidUsernameOrPassword,
)
Expand Down
105 changes: 100 additions & 5 deletions copernicusmarine/core_functions/credentials_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@
"4444552-i-forgot-my-username-or-my-password-what-should-i-do"
)

COPERNICUS_MARINE_AUTH_SYSTEM_URL = "https://auth.marine.copernicus.eu/"
COPERNICUS_MARINE_AUTH_SYSTEM_TOKEN_ENDPOINT = (
COPERNICUS_MARINE_AUTH_SYSTEM_URL
+ "realms/MIS/protocol/openid-connect/token"
)
COPERNICUS_MARINE_AUTH_SYSTEM_USERINFO_ENDPOINT = (
COPERNICUS_MARINE_AUTH_SYSTEM_URL
+ "realms/MIS/protocol/openid-connect/userinfo"
)


class CredentialsCannotBeNone(Exception):
"""
Expand All @@ -65,6 +75,21 @@ class InvalidUsernameOrPassword(Exception):
pass


class CouldNotConnectToAuthenticationSystem(Exception):
"""
Exception raised when the client could not connect to the authentication system.
Please check the following common problems:
- Check your internet connection
- make sure to authorize ``cmems-cas.cls.fr`` and/or ``auth.marine.copernicus.eu`` domains
If none of this worked, maybe the authentication system is down, please try again later.
""" # noqa

pass


def _load_credential_from_copernicus_marine_configuration_file(
credential_type: Literal["username", "password"],
configuration_filename: pathlib.Path,
Expand Down Expand Up @@ -217,7 +242,7 @@ def copernicusmarine_credentials_are_valid(
password: Optional[str],
):
if username and password:
if _check_credentials_with_cas(username, password):
if _are_copernicus_marine_credentials_valid(username, password):
logger.info("Valid credentials from input username and password.")
return True
else:
Expand All @@ -229,7 +254,7 @@ def copernicusmarine_credentials_are_valid(
elif (
COPERNICUSMARINE_SERVICE_USERNAME and COPERNICUSMARINE_SERVICE_PASSWORD
):
if _check_credentials_with_cas(
if _are_copernicus_marine_credentials_valid(
COPERNICUSMARINE_SERVICE_USERNAME,
COPERNICUSMARINE_SERVICE_PASSWORD,
):
Expand Down Expand Up @@ -277,7 +302,7 @@ def copernicusmarine_configuration_file_is_valid(
return (
username is not None
and password is not None
and _check_credentials_with_cas(username, password)
and _are_copernicus_marine_credentials_valid(username, password)
)


Expand Down Expand Up @@ -310,7 +335,7 @@ def create_copernicusmarine_configuration_file(
return configuration_filename


def _check_credentials_with_cas(username: str, password: str) -> bool:
def _check_credentials_with_old_cas(username: str, password: str) -> bool:
logger.debug("Checking user credentials...")
service = "copernicus-marine-client"
cmems_cas_login_url = (
Expand Down Expand Up @@ -339,9 +364,77 @@ def _check_credentials_with_cas(username: str, password: str) -> bool:
return login_success


def _check_credentials_with_cas(username: str, password: str) -> bool:
keycloak_url = COPERNICUS_MARINE_AUTH_SYSTEM_TOKEN_ENDPOINT
client_id = "toolbox"
scope = "openid profile email"

data = {
"client_id": client_id,
"grant_type": "password",
"username": username,
"password": password,
"scope": scope,
}
conn_session = get_configured_requests_session()
response = conn_session.post(keycloak_url, data=data)
response.raise_for_status()
if response.status_code == 200:
token_response = response.json()
access_token = token_response["access_token"]
userinfo_url = COPERNICUS_MARINE_AUTH_SYSTEM_USERINFO_ENDPOINT
headers = {"Authorization": f"Bearer {access_token}"}
response = conn_session.get(userinfo_url, headers=headers)
response.raise_for_status()
if response.status_code == 200:
return True
else:
return False
else:
return False


def _are_copernicus_marine_credentials_valid_old_system(
username: str, password: str
) -> bool:
number_of_retry = 3
user_is_active = None
while (user_is_active not in [True, False]) and number_of_retry > 0:
try:
user_is_active = _check_credentials_with_old_cas(
username=username, password=password
)
except requests.exceptions.ConnectTimeout:
number_of_retry -= 1
except requests.exceptions.ConnectionError:
number_of_retry -= 1
if user_is_active is None:
raise CouldNotConnectToAuthenticationSystem()
return user_is_active


def _are_copernicus_marine_credentials_valid(
username: str, password: str
) -> Optional[bool]:
) -> bool:
try:
result = _are_copernicus_marine_credentials_valid_new_system(
username, password
)
return result

except Exception as e:
logger.debug(
f"Could not connect with new authentication system because of: {e}"
)
logger.debug("Trying with old authentication system...")
return _are_copernicus_marine_credentials_valid_old_system(
username, password
)


def _are_copernicus_marine_credentials_valid_new_system(
username: str, password: str
) -> bool:
number_of_retry = 3
user_is_active = None
while (user_is_active not in [True, False]) and number_of_retry > 0:
Expand All @@ -353,6 +446,8 @@ def _are_copernicus_marine_credentials_valid(
number_of_retry -= 1
except requests.exceptions.ConnectionError:
number_of_retry -= 1
if user_is_active is None:
raise CouldNotConnectToAuthenticationSystem()
return user_is_active


Expand Down
1 change: 1 addition & 0 deletions copernicusmarine/core_functions/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def login_function(
check_credentials_valid: bool,
) -> bool:
if check_credentials_valid:
logger.info("Checking if credentials are valid.")
if copernicusmarine_credentials_are_valid(
configuration_file_directory, username, password
):
Expand Down
2 changes: 2 additions & 0 deletions doc/usage/errors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Custom errors

.. autoclass:: copernicusmarine.InvalidUsernameOrPassword()

.. autoclass:: copernicusmarine.CouldNotConnectToAuthenticationSystem()

.. autoclass:: copernicusmarine.MinimumLongitudeGreaterThanMaximumLongitude()

.. autoclass:: copernicusmarine.VariableDoesNotExistInTheDataset()
Expand Down
11 changes: 11 additions & 0 deletions doc/usage/login-usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ If the ``.copernicusmarine-credentials`` file already exists, the system will as

You can also use the ``--skip-if-user-logged-in`` option to prevent overwriting the configuration file if the user is already logged in.

New Copernius Marine authentication system
-------------------------------------------

A new Copernius Marine authentication system will be released in the following months after the release of the Copernicus Marine toolbox version 2.0.0.
From 2.0.0, the toolbox should be able to handle both the old and the new authentication systems.

If you are blocking some domains, you will need to authorize the domain ``auth.marine.copernicus.eu`` to be able to connect when the old system is decomissioned.

.. note::
One of limitation of the old system is that it goes through HTTP (through redirections) and not HTTPS. The new system will use HTTPS only.

Access points migration and evolution
-------------------------------------

Expand Down
28 changes: 28 additions & 0 deletions tests/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,34 @@ def check_credentials_file_is_valid(self, tmp_path):
b"Valid credentials from configuration file" in self.output.stderr
)

def test_login_falls_back_to_old_system(self):
environment_without_crendentials = (
get_environment_without_crendentials()
)
command = [
"copernicusmarine",
"login",
"--check-credentials-valid",
"--username",
"toto",
"--password",
"tutu",
"--log-level",
"DEBUG",
]

self.output = execute_in_terminal(
command=command, env=environment_without_crendentials
)
assert self.output.returncode == 1
assert (
b"Could not connect with new authentication system"
in self.output.stderr
)
assert (
b" Trying with old authentication system..." in self.output.stderr
)

def test_login_python_interface(self, tmp_path):
folder = Path(tmp_path, "lololo12")
assert not login(
Expand Down

0 comments on commit 615b421

Please sign in to comment.