Skip to content

Commit

Permalink
feat: add MFA OTP support for IAM members (scaleway#836)
Browse files Browse the repository at this point in the history
Co-authored-by: Jonathan R. <[email protected]>
  • Loading branch information
scaleway-bot and jremy42 authored Jan 17, 2025
1 parent ee5878b commit f96b37e
Show file tree
Hide file tree
Showing 8 changed files with 394 additions and 24 deletions.
10 changes: 10 additions & 0 deletions scaleway-async/scaleway_async/iam/v1alpha1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@
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
from .types import DeleteGroupRequest
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
Expand Down Expand Up @@ -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
Expand All @@ -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__ = [
Expand Down Expand Up @@ -142,13 +147,15 @@
"CreateJWTRequest",
"CreatePolicyRequest",
"CreateSSHKeyRequest",
"CreateUserMFAOTPRequest",
"CreateUserRequest",
"DeleteAPIKeyRequest",
"DeleteApplicationRequest",
"DeleteGroupRequest",
"DeleteJWTRequest",
"DeletePolicyRequest",
"DeleteSSHKeyRequest",
"DeleteUserMFAOTPRequest",
"DeleteUserRequest",
"EncodedJWT",
"GetAPIKeyRequest",
Expand Down Expand Up @@ -186,6 +193,7 @@
"ListUsersRequest",
"ListUsersResponse",
"LockUserRequest",
"MFAOTP",
"OrganizationSecuritySettings",
"RemoveGroupMemberRequest",
"SetGroupMembersRequest",
Expand All @@ -201,5 +209,7 @@
"UpdateUserPasswordRequest",
"UpdateUserRequest",
"UpdateUserUsernameRequest",
"ValidateUserMFAOTPRequest",
"ValidateUserMFAOTPResponse",
"IamV1Alpha1API",
]
104 changes: 100 additions & 4 deletions scaleway-async/scaleway_async/iam/v1alpha1/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
ListSSHKeysResponse,
ListUsersResponse,
Log,
MFAOTP,
OrganizationSecuritySettings,
PermissionSet,
Policy,
Expand All @@ -75,6 +76,8 @@
UpdateUserRequest,
UpdateUserUsernameRequest,
User,
ValidateUserMFAOTPRequest,
ValidateUserMFAOTPResponse,
)
from .marshalling import (
unmarshal_JWT,
Expand All @@ -99,8 +102,10 @@
unmarshal_ListRulesResponse,
unmarshal_ListSSHKeysResponse,
unmarshal_ListUsersResponse,
unmarshal_MFAOTP,
unmarshal_OrganizationSecuritySettings,
unmarshal_SetRulesResponse,
unmarshal_ValidateUserMFAOTPResponse,
marshal_AddGroupMemberRequest,
marshal_AddGroupMembersRequest,
marshal_CreateAPIKeyRequest,
Expand All @@ -122,6 +127,7 @@
marshal_UpdateUserPasswordRequest,
marshal_UpdateUserRequest,
marshal_UpdateUserUsernameRequest,
marshal_ValidateUserMFAOTPRequest,
)


Expand Down Expand Up @@ -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 <User>`
Usage:
Expand All @@ -645,7 +649,6 @@ async def update_user_password(
result = await api.update_user_password(
user_id="example",
password="example",
send_email=False,
)
"""

Expand All @@ -658,7 +661,6 @@ async def update_user_password(
UpdateUserPasswordRequest(
user_id=user_id,
password=password,
send_email=send_email,
),
self.client,
),
Expand All @@ -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 <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 <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,
*,
Expand Down
48 changes: 45 additions & 3 deletions scaleway-async/scaleway_async/iam/v1alpha1/marshalling.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@
ListRulesResponse,
ListSSHKeysResponse,
ListUsersResponse,
MFAOTP,
OrganizationSecuritySettings,
SetRulesResponse,
ValidateUserMFAOTPResponse,
AddGroupMemberRequest,
AddGroupMembersRequest,
CreateAPIKeyRequest,
Expand All @@ -60,6 +62,7 @@
UpdateUserPasswordRequest,
UpdateUserRequest,
UpdateUserUsernameRequest,
ValidateUserMFAOTPRequest,
)


Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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
47 changes: 42 additions & 5 deletions scaleway-async/scaleway_async/iam/v1alpha1/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1757,6 +1773,11 @@ class LockUserRequest:
"""


@dataclass
class MFAOTP:
secret: str


@dataclass
class OrganizationSecuritySettings:
enforce_password_renewal: bool
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
"""
Loading

0 comments on commit f96b37e

Please sign in to comment.