Skip to content
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

[ElastiCache] Add support for AuthenticationMode for create_user #8494

Merged
merged 5 commits into from
Jan 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions moto/elasticache/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,26 @@ def __init__(self) -> None:
)


class InvalidParameterValueException(ElastiCacheException):
code = 404

def __init__(self, message: str) -> None:
super().__init__(
"InvalidParameterValue",
message=message,
)


class InvalidParameterCombinationException(ElastiCacheException):
code = 404

def __init__(self, message: str) -> None:
super().__init__(
"InvalidParameterCombination",
message=message,
)


class UserAlreadyExists(ElastiCacheException):
code = 404

Expand Down
33 changes: 32 additions & 1 deletion moto/elasticache/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
CacheClusterAlreadyExists,
CacheClusterNotFound,
InvalidARNFault,
InvalidParameterCombinationException,
InvalidParameterValueException,
UserAlreadyExists,
UserNotFound,
)
from .utils import PAGINATION_MODEL
from .utils import PAGINATION_MODEL, AuthenticationTypes


class User(BaseModel):
Expand All @@ -29,10 +31,12 @@ def __init__(
engine: str,
no_password_required: bool,
passwords: Optional[List[str]] = None,
authentication_type: Optional[str] = None,
):
self.id = user_id
self.name = user_name
self.engine = engine

self.passwords = passwords or []
self.access_string = access_string
self.no_password_required = no_password_required
Expand All @@ -41,6 +45,7 @@ def __init__(
self.usergroupids: List[str] = []
self.region = region
self.arn = f"arn:{get_partition(self.region)}:elasticache:{self.region}:{account_id}:user:{self.id}"
self.authentication_type = authentication_type


class CacheCluster(BaseModel):
Expand Down Expand Up @@ -161,9 +166,34 @@ def create_user(
passwords: List[str],
access_string: str,
no_password_required: bool,
authentication_type: str, # contain it to the str in the enums TODO
) -> User:
if user_id in self.users:
raise UserAlreadyExists

if authentication_type not in AuthenticationTypes._value2member_map_:
raise InvalidParameterValueException(
f"Input Authentication type: {authentication_type} is not in the allowed list: [password,no-password-required,iam]"
)

if (
no_password_required
and authentication_type != AuthenticationTypes.NOPASSWORD
):
raise InvalidParameterCombinationException(
f"No password required flag is true but provided authentication type is {authentication_type}"
)

if passwords and authentication_type != AuthenticationTypes.PASSWORD:
raise InvalidParameterCombinationException(
f"Password field is not allowed with authentication type: {authentication_type}"
)

if not passwords and authentication_type == AuthenticationTypes.PASSWORD:
raise InvalidParameterCombinationException(
"A user with Authentication Mode: password, must have at least one password"
)

user = User(
account_id=self.account_id,
region=self.region_name,
Expand All @@ -173,6 +203,7 @@ def create_user(
passwords=passwords,
access_string=access_string,
no_password_required=no_password_required,
authentication_type=authentication_type,
)
self.users[user_id] = user
return user
Expand Down
49 changes: 43 additions & 6 deletions moto/elasticache/responses.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from moto.core.responses import BaseResponse

from .exceptions import PasswordRequired, PasswordTooShort
from .exceptions import (
InvalidParameterCombinationException,
InvalidParameterValueException,
PasswordTooShort,
)
from .models import ElastiCacheBackend, elasticache_backends
from .utils import AuthenticationTypes


class ElastiCacheResponse(BaseResponse):
Expand All @@ -21,12 +26,41 @@
user_name = params.get("UserName")
engine = params.get("Engine")
passwords = params.get("Passwords", [])
no_password_required = self._get_bool_param("NoPasswordRequired", False)
password_required = not no_password_required
if password_required and not passwords:
raise PasswordRequired
no_password_required = self._get_bool_param("NoPasswordRequired")
authentication_mode = params.get("AuthenticationMode")
authentication_type = "null"

if no_password_required is not None:
authentication_type = (
AuthenticationTypes.NOPASSWORD.value
if no_password_required
else AuthenticationTypes.PASSWORD.value
)

if passwords:
authentication_type = AuthenticationTypes.PASSWORD.value

if authentication_mode:
for key in authentication_mode.keys():
if key not in ["Type", "Passwords"]:
raise InvalidParameterValueException(

Check warning on line 46 in moto/elasticache/responses.py

View check run for this annotation

Codecov / codecov/patch

moto/elasticache/responses.py#L46

Added line #L46 was not covered by tests
f'Unknown parameter in AuthenticationMode: "{key}", must be one of: Type, Passwords'
)

authentication_type = authentication_mode.get("Type")
authentication_passwords = authentication_mode.get("Passwords", [])

if passwords and authentication_passwords:
raise InvalidParameterCombinationException(
"Passwords provided via multiple arguments. Use only one argument"
)

# if passwords is empty, then we can use the authentication_passwords
passwords = passwords if passwords else authentication_passwords

if any([len(p) < 16 for p in passwords]):
raise PasswordTooShort

access_string = params.get("AccessString")
user = self.elasticache_backend.create_user(
user_id=user_id, # type: ignore[arg-type]
Expand All @@ -35,6 +69,7 @@
passwords=passwords,
access_string=access_string, # type: ignore[arg-type]
no_password_required=no_password_required,
authentication_type=authentication_type,
)
template = self.response_template(CREATE_USER_TEMPLATE)
return template.render(user=user)
Expand Down Expand Up @@ -167,7 +202,9 @@
{% if user.no_password_required %}
<Type>no-password</Type>
{% else %}
<Type>password</Type>
<Type>{{ user.authentication_type }}</Type>
{% endif %}
{% if user.passwords %}
<PasswordCount>{{ user.passwords|length }}</PasswordCount>
{% endif %}
</Authentication>
Expand Down
8 changes: 8 additions & 0 deletions moto/elasticache/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from enum import Enum

PAGINATION_MODEL = {
"describe_cache_clusters": {
"input_token": "marker",
Expand All @@ -6,3 +8,9 @@
"unique_attribute": "cache_cluster_id",
},
}


class AuthenticationTypes(str, Enum):
NOPASSWORD = "no-password-required"
PASSWORD = "password"
IAM = "iam"
Loading
Loading