From f96b37e602c6341a941f87a3b3fc73c7c4bf5129 Mon Sep 17 00:00:00 2001 From: Scaleway Bot Date: Fri, 17 Jan 2025 15:41:52 +0100 Subject: [PATCH] feat: add MFA OTP support for IAM members (#836) Co-authored-by: Jonathan R. --- .../scaleway_async/iam/v1alpha1/__init__.py | 10 ++ .../scaleway_async/iam/v1alpha1/api.py | 104 +++++++++++++++++- .../iam/v1alpha1/marshalling.py | 48 +++++++- .../scaleway_async/iam/v1alpha1/types.py | 47 +++++++- scaleway/scaleway/iam/v1alpha1/__init__.py | 10 ++ scaleway/scaleway/iam/v1alpha1/api.py | 104 +++++++++++++++++- scaleway/scaleway/iam/v1alpha1/marshalling.py | 48 +++++++- scaleway/scaleway/iam/v1alpha1/types.py | 47 +++++++- 8 files changed, 394 insertions(+), 24 deletions(-) diff --git a/scaleway-async/scaleway_async/iam/v1alpha1/__init__.py b/scaleway-async/scaleway_async/iam/v1alpha1/__init__.py index 1c30e259..3837eaf0 100644 --- a/scaleway-async/scaleway_async/iam/v1alpha1/__init__.py +++ b/scaleway-async/scaleway_async/iam/v1alpha1/__init__.py @@ -40,6 +40,7 @@ from .types import CreateJWTRequest from .types import CreatePolicyRequest from .types import CreateSSHKeyRequest +from .types import CreateUserMFAOTPRequest from .types import CreateUserRequest from .types import DeleteAPIKeyRequest from .types import DeleteApplicationRequest @@ -47,6 +48,7 @@ from .types import DeleteJWTRequest from .types import DeletePolicyRequest from .types import DeleteSSHKeyRequest +from .types import DeleteUserMFAOTPRequest from .types import DeleteUserRequest from .types import EncodedJWT from .types import GetAPIKeyRequest @@ -84,6 +86,7 @@ from .types import ListUsersRequest from .types import ListUsersResponse from .types import LockUserRequest +from .types import MFAOTP from .types import OrganizationSecuritySettings from .types import RemoveGroupMemberRequest from .types import SetGroupMembersRequest @@ -99,6 +102,8 @@ from .types import UpdateUserPasswordRequest from .types import UpdateUserRequest from .types import UpdateUserUsernameRequest +from .types import ValidateUserMFAOTPRequest +from .types import ValidateUserMFAOTPResponse from .api import IamV1Alpha1API __all__ = [ @@ -142,6 +147,7 @@ "CreateJWTRequest", "CreatePolicyRequest", "CreateSSHKeyRequest", + "CreateUserMFAOTPRequest", "CreateUserRequest", "DeleteAPIKeyRequest", "DeleteApplicationRequest", @@ -149,6 +155,7 @@ "DeleteJWTRequest", "DeletePolicyRequest", "DeleteSSHKeyRequest", + "DeleteUserMFAOTPRequest", "DeleteUserRequest", "EncodedJWT", "GetAPIKeyRequest", @@ -186,6 +193,7 @@ "ListUsersRequest", "ListUsersResponse", "LockUserRequest", + "MFAOTP", "OrganizationSecuritySettings", "RemoveGroupMemberRequest", "SetGroupMembersRequest", @@ -201,5 +209,7 @@ "UpdateUserPasswordRequest", "UpdateUserRequest", "UpdateUserUsernameRequest", + "ValidateUserMFAOTPRequest", + "ValidateUserMFAOTPResponse", "IamV1Alpha1API", ] diff --git a/scaleway-async/scaleway_async/iam/v1alpha1/api.py b/scaleway-async/scaleway_async/iam/v1alpha1/api.py index a780d75d..24fc6a49 100644 --- a/scaleway-async/scaleway_async/iam/v1alpha1/api.py +++ b/scaleway-async/scaleway_async/iam/v1alpha1/api.py @@ -54,6 +54,7 @@ ListSSHKeysResponse, ListUsersResponse, Log, + MFAOTP, OrganizationSecuritySettings, PermissionSet, Policy, @@ -75,6 +76,8 @@ UpdateUserRequest, UpdateUserUsernameRequest, User, + ValidateUserMFAOTPRequest, + ValidateUserMFAOTPResponse, ) from .marshalling import ( unmarshal_JWT, @@ -99,8 +102,10 @@ unmarshal_ListRulesResponse, unmarshal_ListSSHKeysResponse, unmarshal_ListUsersResponse, + unmarshal_MFAOTP, unmarshal_OrganizationSecuritySettings, unmarshal_SetRulesResponse, + unmarshal_ValidateUserMFAOTPResponse, marshal_AddGroupMemberRequest, marshal_AddGroupMembersRequest, marshal_CreateAPIKeyRequest, @@ -122,6 +127,7 @@ marshal_UpdateUserPasswordRequest, marshal_UpdateUserRequest, marshal_UpdateUserUsernameRequest, + marshal_ValidateUserMFAOTPRequest, ) @@ -630,13 +636,11 @@ async def update_user_password( *, user_id: str, password: str, - send_email: bool, ) -> User: """ Update an user's password. Private Beta feature. :param user_id: ID of the user to update. :param password: The new password. - :param send_email: Whether or not to send an email alerting the user their password has changed. :return: :class:`User ` Usage: @@ -645,7 +649,6 @@ async def update_user_password( result = await api.update_user_password( user_id="example", password="example", - send_email=False, ) """ @@ -658,7 +661,6 @@ async def update_user_password( UpdateUserPasswordRequest( user_id=user_id, password=password, - send_email=send_email, ), self.client, ), @@ -667,6 +669,100 @@ async def update_user_password( self._throw_on_error(res) return unmarshal_User(res.json()) + async def create_user_mfaotp( + self, + *, + user_id: str, + ) -> MFAOTP: + """ + Create a MFA OTP. Private Beta feature. + :param user_id: User ID of the MFA OTP. + :return: :class:`MFAOTP ` + + Usage: + :: + + result = await api.create_user_mfaotp( + user_id="example", + ) + """ + + param_user_id = validate_path_param("user_id", user_id) + + res = self._request( + "POST", + f"/iam/v1alpha1/users/{param_user_id}/mfa-otp", + body={}, + ) + + self._throw_on_error(res) + return unmarshal_MFAOTP(res.json()) + + async def validate_user_mfaotp( + self, + *, + user_id: str, + one_time_password: str, + ) -> ValidateUserMFAOTPResponse: + """ + Validate a MFA OTP. Private Beta feature. + :param user_id: User ID of the MFA OTP. + :param one_time_password: A password generated using the OTP. + :return: :class:`ValidateUserMFAOTPResponse ` + + Usage: + :: + + result = await api.validate_user_mfaotp( + user_id="example", + one_time_password="example", + ) + """ + + param_user_id = validate_path_param("user_id", user_id) + + res = self._request( + "POST", + f"/iam/v1alpha1/users/{param_user_id}/validate-mfa-otp", + body=marshal_ValidateUserMFAOTPRequest( + ValidateUserMFAOTPRequest( + user_id=user_id, + one_time_password=one_time_password, + ), + self.client, + ), + ) + + self._throw_on_error(res) + return unmarshal_ValidateUserMFAOTPResponse(res.json()) + + async def delete_user_mfaotp( + self, + *, + user_id: str, + ) -> None: + """ + Delete a MFA OTP. Private Beta feature. + :param user_id: User ID of the MFA OTP. + + Usage: + :: + + result = await api.delete_user_mfaotp( + user_id="example", + ) + """ + + param_user_id = validate_path_param("user_id", user_id) + + res = self._request( + "DELETE", + f"/iam/v1alpha1/users/{param_user_id}/mfa-otp", + body={}, + ) + + self._throw_on_error(res) + async def lock_user( self, *, diff --git a/scaleway-async/scaleway_async/iam/v1alpha1/marshalling.py b/scaleway-async/scaleway_async/iam/v1alpha1/marshalling.py index af585326..09ce69f0 100644 --- a/scaleway-async/scaleway_async/iam/v1alpha1/marshalling.py +++ b/scaleway-async/scaleway_async/iam/v1alpha1/marshalling.py @@ -35,8 +35,10 @@ ListRulesResponse, ListSSHKeysResponse, ListUsersResponse, + MFAOTP, OrganizationSecuritySettings, SetRulesResponse, + ValidateUserMFAOTPResponse, AddGroupMemberRequest, AddGroupMembersRequest, CreateAPIKeyRequest, @@ -60,6 +62,7 @@ UpdateUserPasswordRequest, UpdateUserRequest, UpdateUserUsernameRequest, + ValidateUserMFAOTPRequest, ) @@ -996,6 +999,21 @@ def unmarshal_ListUsersResponse(data: Any) -> ListUsersResponse: return ListUsersResponse(**args) +def unmarshal_MFAOTP(data: Any) -> MFAOTP: + if not isinstance(data, dict): + raise TypeError( + "Unmarshalling the type 'MFAOTP' failed as data isn't a dictionary." + ) + + args: Dict[str, Any] = {} + + field = data.get("secret", None) + if field is not None: + args["secret"] = field + + return MFAOTP(**args) + + def unmarshal_OrganizationSecuritySettings(data: Any) -> OrganizationSecuritySettings: if not isinstance(data, dict): raise TypeError( @@ -1038,6 +1056,21 @@ def unmarshal_SetRulesResponse(data: Any) -> SetRulesResponse: return SetRulesResponse(**args) +def unmarshal_ValidateUserMFAOTPResponse(data: Any) -> ValidateUserMFAOTPResponse: + if not isinstance(data, dict): + raise TypeError( + "Unmarshalling the type 'ValidateUserMFAOTPResponse' failed as data isn't a dictionary." + ) + + args: Dict[str, Any] = {} + + field = data.get("recovery_codes", None) + if field is not None: + args["recovery_codes"] = field + + return ValidateUserMFAOTPResponse(**args) + + def marshal_AddGroupMemberRequest( request: AddGroupMemberRequest, defaults: ProfileDefaults, @@ -1455,9 +1488,6 @@ def marshal_UpdateUserPasswordRequest( if request.password is not None: output["password"] = request.password - if request.send_email is not None: - output["send_email"] = request.send_email - return output @@ -1486,3 +1516,15 @@ def marshal_UpdateUserUsernameRequest( output["username"] = request.username return output + + +def marshal_ValidateUserMFAOTPRequest( + request: ValidateUserMFAOTPRequest, + defaults: ProfileDefaults, +) -> Dict[str, Any]: + output: Dict[str, Any] = {} + + if request.one_time_password is not None: + output["one_time_password"] = request.one_time_password + + return output diff --git a/scaleway-async/scaleway_async/iam/v1alpha1/types.py b/scaleway-async/scaleway_async/iam/v1alpha1/types.py index 9dcc6473..6f2adc3f 100644 --- a/scaleway-async/scaleway_async/iam/v1alpha1/types.py +++ b/scaleway-async/scaleway_async/iam/v1alpha1/types.py @@ -986,6 +986,14 @@ class CreateSSHKeyRequest: """ +@dataclass +class CreateUserMFAOTPRequest: + user_id: str + """ + User ID of the MFA OTP. + """ + + @dataclass class CreateUserRequest: organization_id: Optional[str] @@ -1048,6 +1056,14 @@ class DeleteSSHKeyRequest: ssh_key_id: str +@dataclass +class DeleteUserMFAOTPRequest: + user_id: str + """ + User ID of the MFA OTP. + """ + + @dataclass class DeleteUserRequest: user_id: str @@ -1757,6 +1773,11 @@ class LockUserRequest: """ +@dataclass +class MFAOTP: + secret: str + + @dataclass class OrganizationSecuritySettings: enforce_password_renewal: bool @@ -1970,11 +1991,6 @@ class UpdateUserPasswordRequest: The new password. """ - send_email: bool - """ - Whether or not to send an email alerting the user their password has changed. - """ - @dataclass class UpdateUserRequest: @@ -2005,3 +2021,24 @@ class UpdateUserUsernameRequest: """ The new username. """ + + +@dataclass +class ValidateUserMFAOTPRequest: + user_id: str + """ + User ID of the MFA OTP. + """ + + one_time_password: str + """ + A password generated using the OTP. + """ + + +@dataclass +class ValidateUserMFAOTPResponse: + recovery_codes: List[str] + """ + List of recovery codes usable for this OTP method. + """ diff --git a/scaleway/scaleway/iam/v1alpha1/__init__.py b/scaleway/scaleway/iam/v1alpha1/__init__.py index 1c30e259..3837eaf0 100644 --- a/scaleway/scaleway/iam/v1alpha1/__init__.py +++ b/scaleway/scaleway/iam/v1alpha1/__init__.py @@ -40,6 +40,7 @@ from .types import CreateJWTRequest from .types import CreatePolicyRequest from .types import CreateSSHKeyRequest +from .types import CreateUserMFAOTPRequest from .types import CreateUserRequest from .types import DeleteAPIKeyRequest from .types import DeleteApplicationRequest @@ -47,6 +48,7 @@ from .types import DeleteJWTRequest from .types import DeletePolicyRequest from .types import DeleteSSHKeyRequest +from .types import DeleteUserMFAOTPRequest from .types import DeleteUserRequest from .types import EncodedJWT from .types import GetAPIKeyRequest @@ -84,6 +86,7 @@ from .types import ListUsersRequest from .types import ListUsersResponse from .types import LockUserRequest +from .types import MFAOTP from .types import OrganizationSecuritySettings from .types import RemoveGroupMemberRequest from .types import SetGroupMembersRequest @@ -99,6 +102,8 @@ from .types import UpdateUserPasswordRequest from .types import UpdateUserRequest from .types import UpdateUserUsernameRequest +from .types import ValidateUserMFAOTPRequest +from .types import ValidateUserMFAOTPResponse from .api import IamV1Alpha1API __all__ = [ @@ -142,6 +147,7 @@ "CreateJWTRequest", "CreatePolicyRequest", "CreateSSHKeyRequest", + "CreateUserMFAOTPRequest", "CreateUserRequest", "DeleteAPIKeyRequest", "DeleteApplicationRequest", @@ -149,6 +155,7 @@ "DeleteJWTRequest", "DeletePolicyRequest", "DeleteSSHKeyRequest", + "DeleteUserMFAOTPRequest", "DeleteUserRequest", "EncodedJWT", "GetAPIKeyRequest", @@ -186,6 +193,7 @@ "ListUsersRequest", "ListUsersResponse", "LockUserRequest", + "MFAOTP", "OrganizationSecuritySettings", "RemoveGroupMemberRequest", "SetGroupMembersRequest", @@ -201,5 +209,7 @@ "UpdateUserPasswordRequest", "UpdateUserRequest", "UpdateUserUsernameRequest", + "ValidateUserMFAOTPRequest", + "ValidateUserMFAOTPResponse", "IamV1Alpha1API", ] diff --git a/scaleway/scaleway/iam/v1alpha1/api.py b/scaleway/scaleway/iam/v1alpha1/api.py index fc4e0b26..11364cf4 100644 --- a/scaleway/scaleway/iam/v1alpha1/api.py +++ b/scaleway/scaleway/iam/v1alpha1/api.py @@ -54,6 +54,7 @@ ListSSHKeysResponse, ListUsersResponse, Log, + MFAOTP, OrganizationSecuritySettings, PermissionSet, Policy, @@ -75,6 +76,8 @@ UpdateUserRequest, UpdateUserUsernameRequest, User, + ValidateUserMFAOTPRequest, + ValidateUserMFAOTPResponse, ) from .marshalling import ( unmarshal_JWT, @@ -99,8 +102,10 @@ unmarshal_ListRulesResponse, unmarshal_ListSSHKeysResponse, unmarshal_ListUsersResponse, + unmarshal_MFAOTP, unmarshal_OrganizationSecuritySettings, unmarshal_SetRulesResponse, + unmarshal_ValidateUserMFAOTPResponse, marshal_AddGroupMemberRequest, marshal_AddGroupMembersRequest, marshal_CreateAPIKeyRequest, @@ -122,6 +127,7 @@ marshal_UpdateUserPasswordRequest, marshal_UpdateUserRequest, marshal_UpdateUserUsernameRequest, + marshal_ValidateUserMFAOTPRequest, ) @@ -630,13 +636,11 @@ def update_user_password( *, user_id: str, password: str, - send_email: bool, ) -> User: """ Update an user's password. Private Beta feature. :param user_id: ID of the user to update. :param password: The new password. - :param send_email: Whether or not to send an email alerting the user their password has changed. :return: :class:`User ` Usage: @@ -645,7 +649,6 @@ def update_user_password( result = api.update_user_password( user_id="example", password="example", - send_email=False, ) """ @@ -658,7 +661,6 @@ def update_user_password( UpdateUserPasswordRequest( user_id=user_id, password=password, - send_email=send_email, ), self.client, ), @@ -667,6 +669,100 @@ def update_user_password( self._throw_on_error(res) return unmarshal_User(res.json()) + def create_user_mfaotp( + self, + *, + user_id: str, + ) -> MFAOTP: + """ + Create a MFA OTP. Private Beta feature. + :param user_id: User ID of the MFA OTP. + :return: :class:`MFAOTP ` + + Usage: + :: + + result = api.create_user_mfaotp( + user_id="example", + ) + """ + + param_user_id = validate_path_param("user_id", user_id) + + res = self._request( + "POST", + f"/iam/v1alpha1/users/{param_user_id}/mfa-otp", + body={}, + ) + + self._throw_on_error(res) + return unmarshal_MFAOTP(res.json()) + + def validate_user_mfaotp( + self, + *, + user_id: str, + one_time_password: str, + ) -> ValidateUserMFAOTPResponse: + """ + Validate a MFA OTP. Private Beta feature. + :param user_id: User ID of the MFA OTP. + :param one_time_password: A password generated using the OTP. + :return: :class:`ValidateUserMFAOTPResponse ` + + Usage: + :: + + result = api.validate_user_mfaotp( + user_id="example", + one_time_password="example", + ) + """ + + param_user_id = validate_path_param("user_id", user_id) + + res = self._request( + "POST", + f"/iam/v1alpha1/users/{param_user_id}/validate-mfa-otp", + body=marshal_ValidateUserMFAOTPRequest( + ValidateUserMFAOTPRequest( + user_id=user_id, + one_time_password=one_time_password, + ), + self.client, + ), + ) + + self._throw_on_error(res) + return unmarshal_ValidateUserMFAOTPResponse(res.json()) + + def delete_user_mfaotp( + self, + *, + user_id: str, + ) -> None: + """ + Delete a MFA OTP. Private Beta feature. + :param user_id: User ID of the MFA OTP. + + Usage: + :: + + result = api.delete_user_mfaotp( + user_id="example", + ) + """ + + param_user_id = validate_path_param("user_id", user_id) + + res = self._request( + "DELETE", + f"/iam/v1alpha1/users/{param_user_id}/mfa-otp", + body={}, + ) + + self._throw_on_error(res) + def lock_user( self, *, diff --git a/scaleway/scaleway/iam/v1alpha1/marshalling.py b/scaleway/scaleway/iam/v1alpha1/marshalling.py index af585326..09ce69f0 100644 --- a/scaleway/scaleway/iam/v1alpha1/marshalling.py +++ b/scaleway/scaleway/iam/v1alpha1/marshalling.py @@ -35,8 +35,10 @@ ListRulesResponse, ListSSHKeysResponse, ListUsersResponse, + MFAOTP, OrganizationSecuritySettings, SetRulesResponse, + ValidateUserMFAOTPResponse, AddGroupMemberRequest, AddGroupMembersRequest, CreateAPIKeyRequest, @@ -60,6 +62,7 @@ UpdateUserPasswordRequest, UpdateUserRequest, UpdateUserUsernameRequest, + ValidateUserMFAOTPRequest, ) @@ -996,6 +999,21 @@ def unmarshal_ListUsersResponse(data: Any) -> ListUsersResponse: return ListUsersResponse(**args) +def unmarshal_MFAOTP(data: Any) -> MFAOTP: + if not isinstance(data, dict): + raise TypeError( + "Unmarshalling the type 'MFAOTP' failed as data isn't a dictionary." + ) + + args: Dict[str, Any] = {} + + field = data.get("secret", None) + if field is not None: + args["secret"] = field + + return MFAOTP(**args) + + def unmarshal_OrganizationSecuritySettings(data: Any) -> OrganizationSecuritySettings: if not isinstance(data, dict): raise TypeError( @@ -1038,6 +1056,21 @@ def unmarshal_SetRulesResponse(data: Any) -> SetRulesResponse: return SetRulesResponse(**args) +def unmarshal_ValidateUserMFAOTPResponse(data: Any) -> ValidateUserMFAOTPResponse: + if not isinstance(data, dict): + raise TypeError( + "Unmarshalling the type 'ValidateUserMFAOTPResponse' failed as data isn't a dictionary." + ) + + args: Dict[str, Any] = {} + + field = data.get("recovery_codes", None) + if field is not None: + args["recovery_codes"] = field + + return ValidateUserMFAOTPResponse(**args) + + def marshal_AddGroupMemberRequest( request: AddGroupMemberRequest, defaults: ProfileDefaults, @@ -1455,9 +1488,6 @@ def marshal_UpdateUserPasswordRequest( if request.password is not None: output["password"] = request.password - if request.send_email is not None: - output["send_email"] = request.send_email - return output @@ -1486,3 +1516,15 @@ def marshal_UpdateUserUsernameRequest( output["username"] = request.username return output + + +def marshal_ValidateUserMFAOTPRequest( + request: ValidateUserMFAOTPRequest, + defaults: ProfileDefaults, +) -> Dict[str, Any]: + output: Dict[str, Any] = {} + + if request.one_time_password is not None: + output["one_time_password"] = request.one_time_password + + return output diff --git a/scaleway/scaleway/iam/v1alpha1/types.py b/scaleway/scaleway/iam/v1alpha1/types.py index 9dcc6473..6f2adc3f 100644 --- a/scaleway/scaleway/iam/v1alpha1/types.py +++ b/scaleway/scaleway/iam/v1alpha1/types.py @@ -986,6 +986,14 @@ class CreateSSHKeyRequest: """ +@dataclass +class CreateUserMFAOTPRequest: + user_id: str + """ + User ID of the MFA OTP. + """ + + @dataclass class CreateUserRequest: organization_id: Optional[str] @@ -1048,6 +1056,14 @@ class DeleteSSHKeyRequest: ssh_key_id: str +@dataclass +class DeleteUserMFAOTPRequest: + user_id: str + """ + User ID of the MFA OTP. + """ + + @dataclass class DeleteUserRequest: user_id: str @@ -1757,6 +1773,11 @@ class LockUserRequest: """ +@dataclass +class MFAOTP: + secret: str + + @dataclass class OrganizationSecuritySettings: enforce_password_renewal: bool @@ -1970,11 +1991,6 @@ class UpdateUserPasswordRequest: The new password. """ - send_email: bool - """ - Whether or not to send an email alerting the user their password has changed. - """ - @dataclass class UpdateUserRequest: @@ -2005,3 +2021,24 @@ class UpdateUserUsernameRequest: """ The new username. """ + + +@dataclass +class ValidateUserMFAOTPRequest: + user_id: str + """ + User ID of the MFA OTP. + """ + + one_time_password: str + """ + A password generated using the OTP. + """ + + +@dataclass +class ValidateUserMFAOTPResponse: + recovery_codes: List[str] + """ + List of recovery codes usable for this OTP method. + """