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

Mocking password_last_used date #5927

Closed
WesleyHindle opened this issue Feb 14, 2023 · 21 comments
Closed

Mocking password_last_used date #5927

WesleyHindle opened this issue Feb 14, 2023 · 21 comments

Comments

@WesleyHindle
Copy link

Is there a way to mock a specific date that an IAM user last used their password?

I think the answer is no, because you actually have to log into the console to have used the password, but just wondered if I had missed anything.

Thanks.

@bblommers
Copy link
Collaborator

Hi @WesleyHindle, there is no way to mock this, no.

You could reach directly in the internal API and change it there:

from moto.backends import get_backend
iam_backend = get_backend("iam")[ACCOUNT_ID]["global"]
iam_backend.users[username].password_last_used = datetime.utcnow()

Just note that, as it is an internal API, this may change without notice.

We use the same approach in our tests:

iam_backend = get_backend("iam")[ACCOUNT_ID]["global"]

@WesleyHindle
Copy link
Author

Thank you for the reply. I'm having trouble implementing this.

def test_password_last_used(iam):
    current_time = pytz.timezone("UTC").localize(datetime.utcnow())
    password_last_used_date = current_time - timedelta(days=100)

    username = "test.user"
    iam[0].create_user(Path='/staff/', UserName=username)
    iam[0].create_login_profile(
        UserName = username,
        Password = "Password1",
        PasswordResetRequired = False)
    

    iam_backend = get_backend("iam")[ACCOUNT_ID]["global"]
    iam_backend.users[username].password_last_used = password_last_used_date


    x = iam[1].CurrentUser().user_name
    print(x)

So this returns default_user, not the test.user created, soI'm unable to validate what date the password last used was.

Is there a way to change to current user from default_user to the created user, so I can access the password last used information?

@bblommers
Copy link
Collaborator

The proper way would be to create an AccessKey for that user, and then login as that user:

access_key = client.create_access_key(UserName=username)["AccessKey"]

as_new_user = boto3.resource(
    "iam",
    region_name="us-east-1",
    aws_access_key_id=access_key["AccessKeyId"],
    aws_secret_access_key=access_key["SecretAccessKey"],
)

assert as_new_user.CurrentUser().user_name == username

Would that work?

Note that does not give you the password at the moment - Moto's implementation doesn't return that field yet. I'll raise a PR for that shortly to get that fixed.

@WesleyHindle
Copy link
Author

Okay, so that has solved the issue about being the default-user, thank you for that.

However I'm still having trouble setting a date for password_last_used. This is my config:

def test_password_last_used(iam):
    current_time = pytz.timezone("UTC").localize(datetime.utcnow())
    password_last_used_date = current_time - timedelta(days=100)

    username = "test.user"
    iam[0].create_user(Path='/staff/', UserName=username)
    iam[0].create_login_profile(
        UserName = username,
        Password = "Password1",
        PasswordResetRequired = False)

    iam_backend = get_backend("iam")[ACCOUNT_ID]["global"]
    iam_backend.users[username].password_last_used = password_last_used_date

    access_key = iam[0].create_access_key(UserName=username)["AccessKey"]

    as_new_user = boto3.resource(
        "iam",
        region_name="us-east-1",
        aws_access_key_id=access_key["AccessKeyId"],
        aws_secret_access_key=access_key["SecretAccessKey"],
    )

    print(as_new_user.CurrentUser().password_last_used)

Which returns None

@bblommers
Copy link
Collaborator

Can you try the latest version @WesleyHindle, moto >= 4.1.3.dev34? That should fix that

@WesleyHindle
Copy link
Author

Thanks for the reply, I'm still getting None returned after updating

moto 4.1.3.dev34

@bblommers
Copy link
Collaborator

That's odd - it does work for me, and as far as I can tell, the script I use is the same.
My console output:
Screenshot from 2023-02-19 10-45-16

@WesleyHindle
Copy link
Author

Thank you for the reply, I'm still having issues though, even when replicating your code:

pip show moto                 
Name: moto
Version: 4.1.3.dev34
from moto import mock_iam
from moto.backends import get_backend
from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID

@pytest.fixture
@mock_iam
def aws_credentials():
    """Mocked AWS Credentials for moto."""
    os.environ["AWS_ACCESS_KEY_ID"] = "testing"
    os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
    os.environ["AWS_SECURITY_TOKEN"] = "testing"
    os.environ["AWS_SESSION_TOKEN"] = "testing"


#Here for completeness
@pytest.fixture
def iam(aws_credentials):
    with mock_iam():
        #client will be [0], resource [1]
        yield (boto3.client("iam"), boto3.resource("iam"))

mock = mock_iam()
mock.start()
client = boto3.client("iam", "us-east-1")
username = "test.user"
client.create_user(Path='/staff/', UserName=username)
client.create_login_profile(
    UserName = username,
    Password = "Password1",
    PasswordResetRequired = False )

iam_backend = get_backend("iam")[ACCOUNT_ID]["global"]
iam_backend.users[username].password_last_used = datetime.utcnow()
a_k = access_key = client.create_access_key(UserName=username)["AccessKey"]
print(a_k)
as_new_user = boto3.resource("iam", region_name="us-east-1", aws_access_key_id=access_key["AccessKeyId"], aws_secret_access_key=access_key["SecretAccessKey"] )
print("---")
print(as_new_user.CurrentUser().password_last_used)
mock.stop()

Which still returns None

@bblommers
Copy link
Collaborator

FYI, I've released Moto 4.1.3 just now. It contains the same functionality as 4.1.3.dev34, but maybe it's easier to install/test.

@WesleyHindle
Copy link
Author

I'be updated to 4.1.3 and ran this code in a new file

import boto3
from datetime import datetime, timedelta
from moto import mock_iam
from moto.backends import get_backend
from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
import os
import pytest

mock = mock_iam()
mock.start()
client = boto3.client("iam", "us-east-1")
username = "test.user"
client.create_user(Path='/staff/', UserName=username)
client.create_login_profile(
    UserName = username,
    Password = "Password1",
    PasswordResetRequired = False )

iam_backend = get_backend("iam")[ACCOUNT_ID]["global"]
iam_backend.users[username].password_last_used = datetime.utcnow()
a_k = access_key = client.create_access_key(UserName=username)["AccessKey"]
print(a_k)
as_new_user = boto3.resource("iam", region_name="us-east-1", aws_access_key_id=access_key["AccessKeyId"], aws_secret_access_key=access_key["SecretAccessKey"] )
print("---")
print(as_new_user.CurrentUser().password_last_used)
mock.stop()

And I'm still getting None returned, is there anything else I can do to help with debugging?

@bblommers
Copy link
Collaborator

Can you add some print-statements to get_user() in moto/iam/responses.py?
Right around here:

template = self.response_template(USER_TEMPLATE)

Just curious to see what's going wrong, if it's the model or the view.

print(user.__dict__)
print(user.password_last_used_iso_8601)
print(template.render(action="Get", user=user, tags=tags))

And maybe a print(moto.__version__), just to double check that this is not executed against an older version of Moto somehow. Happens to me a little too often, that I'm playing around in the wrong virtual env or something like that...

@WesleyHindle
Copy link
Author

Sorry, this is quickly becoming way out of my depth! So in moto/iam/responses.py I have:

def get_user(self):
        user_name = self._get_param("UserName")
        if not user_name:
            access_key_id = self.get_access_key()
            user = self.backend.get_user_from_access_key_id(access_key_id)
            if user is None:
                user = User(self.current_account, "default_user")
        else:
            user = self.backend.get_user(user_name)
        tags = self.backend.tagger.list_tags_for_resource(user.arn).get("Tags", [])
        template = self.response_template(USER_TEMPLATE)
        print(user.__dict__)
        print(user.password_last_used_iso_8601)
        print(template.render(action="Get", user=user, tags=tags))
        return template.render(action="Get", user=user, tags=tags)

Which when I run the previous code doesn't output anything, additional.

I was also unsure where to add print(moto.__version__) as in responses.py this highlighted a moto is not defined error.

@bblommers
Copy link
Collaborator

If nothing is printed, then we're definitely looking at different Moto versions/installations. It is the right method, since that's where the default_user comes from that we saw earlier. But if nothing is printed, then the Moto-library used by your script is not the same as the Moto-code that you're editing.

I would suggest to start from scratch. Are you using virtual environments? If so, just create a new one, install moto in that env and execute the script again. Feel free to share the steps that you take if you're unsure about anything.

@WesleyHindle
Copy link
Author

WesleyHindle commented Feb 22, 2023

Okay, so good news! You're right. Your update was working and it was an issue on my side.

The only bad news is the horrible state of my pip, which was causing the issue.

Thank you for your patience, help and new feature.

@bblommers
Copy link
Collaborator

Happy to hear this is now solved!

@WesleyHindle WesleyHindle reopened this Feb 22, 2023
@WesleyHindle
Copy link
Author

Okay. I think I was slightly eager!

I want to test this code:

def password_last_used():
    iam_resource = boto3.resource("iam")
    iam_client = boto3.client("iam")
    pass_last_used = iam_resource.CurrentUser().password_last_used
    username = iam_resource.CurrentUser().user_name

    if pass_last_used < cutoff_date:
        print("not fine")
    else:
        print("fine")

Using this test (I'll add the actual test once it's working):

@pytest.fixture
@mock_iam
def aws_credentials():
    """Mocked AWS Credentials for moto."""
    os.environ["AWS_ACCESS_KEY_ID"] = "testing"
    os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
    os.environ["AWS_SECURITY_TOKEN"] = "testing"
    os.environ["AWS_SESSION_TOKEN"] = "testing"

@pytest.fixture
def iam(aws_credentials):
    with mock_iam():
        #client will be [0], resource [1]
        yield (boto3.client("iam"), boto3.resource("iam"))


def test_password_last_used(iam):
    current_time = pytz.timezone("UTC").localize(datetime.utcnow())
    password_last_used_date = current_time - timedelta(days=100)

    username = "test.user"
    iam[0].create_user(Path='/staff/', UserName=username)
    iam[0].create_login_profile(
        UserName = username,
        Password = "Password1",
        PasswordResetRequired = False
    )

    access_key = iam[0].create_access_key(UserName=username)["AccessKey"]

    as_new_user = boto3.resource(
        "iam",
        region_name="us-east-1",
        aws_access_key_id=access_key["AccessKeyId"],
        aws_secret_access_key=access_key["SecretAccessKey"],
    )

    iam_backend = get_backend("iam")[ACCOUNT_ID]["global"]
    iam_backend.users[username].password_last_used = password_last_used_date

    print(password_last_used())

But when I run the test I get an error saying that there's None type, so can't comapre for dates. Makes sense, I'm comparing a different user to the test user who's password_last_used attribute I've manipulated.

However, I think that the test user only exists as a var within the test and not an env var as the actual current user, and because no param is passed into the test, there's no way for the test to know to use the test user if it isn't assigned as the CurrentUser as an env var.

Am I on the right track with this so and if so, is there a way for me to be able to configure the test so that I can use the test user's password last used object for testing the app code, even though the app doesn't take in a param.

I think realistically though this app code would have to take in a param, else you're editiing your own AWS user

@bblommers
Copy link
Collaborator

Is the iam_resource actually defined inside the password_last_used()-method?
If it is created as part of a class, you could maybe override it as part of the test:

logic = LogicClass()  # whatever this is..
logic.iam_resource = as_new_user
logic.password_last_used()

A second theoretical option is to set environment variables just before calling password_last_used()

access_key = iam[0].create_access_key(UserName=username)["AccessKey"]
os.environ["AWS_ACCESS_KEY_ID"] = access_key["AccessKeyId"]
os.environ["AWS_SECRET_ACCESS_KEY"] = access_key["SecretAccessKey"]
password_last_used()

This should configure the boto3.resource to use the environment variables, and make the call as if it is the new user.
(Note the theoretical and should! I'm not able to test this at the moment, but I might just work...)

@WesleyHindle
Copy link
Author

Okay, so I had think about what I was trying to do and I was overcomplicating it. There was no need to use the CurrentUser class, when the User Class has the same methods. You can easily change the user with User class too, unlike using CurrentUser.

This was my finished test / app code with a manipulated password_last_used date.

Thank you again.

# App

import boto3
import pytz
from datetime import datetime, timedelta

# Password can only be 30 days old
# Convert current time date to be time zone aware needed for comparison with an AWS user date
current_time = pytz.timezone("UTC").localize(datetime.utcnow())
cutoff_date = current_time - timedelta(days=30)

def password_last_used(username):
    iam_resource = boto3.resource("iam")
    iam_client = boto3.client("iam")
    aws_user = iam_resource.User(username)

    if aws_user.password_last_used < cutoff_date:
        print("not fine")
        iam_client.delete_login_profile(UserName=username)
from app_5 import password_last_used
import boto3
from datetime import datetime, timedelta
from moto import mock_iam
from moto.backends import get_backend
from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
import os
import pytest
import pytz

@pytest.fixture
@mock_iam
def aws_credentials():
    """Mocked AWS Credentials for moto."""
    os.environ["AWS_ACCESS_KEY_ID"] = "testing"
    os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
    os.environ["AWS_SECURITY_TOKEN"] = "testing"
    os.environ["AWS_SESSION_TOKEN"] = "testing"

@pytest.fixture
def iam(aws_credentials):
    with mock_iam():
        #client will be [0], resource [1]
        yield (boto3.client("iam"), boto3.resource("iam"))


def test_password_last_used(iam):
    current_time = pytz.timezone("UTC").localize(datetime.utcnow())
    password_last_used_date = current_time - timedelta(days=100)

    username = "test.user"
    iam[0].create_user(Path='/staff/', UserName=username)
    iam[0].create_login_profile(
        UserName = username,
        Password = "Password1",
        PasswordResetRequired = False
    )

    iam_backend = get_backend("iam")[ACCOUNT_ID]["global"]
    iam_backend.users[username].password_last_used = password_last_used_date

    assert iam[0].get_login_profile(UserName=username) is not None

    password_last_used(username)

    with pytest.raises(Exception):
        iam[0].get_login_profile(UserName=username)

@WesleyHindle
Copy link
Author

As you mentioned changing the last used date requires you amending the backend, are there plans to implement functionality where you can manipulate the 'last used date' without amending the backend, or is this the only feasible way to do this?

If this is the case are changes to the internal API listed on change notes?

@bblommers
Copy link
Collaborator

From what I can tell, this functionality isn't used by enough people to warrant a dedicated/public/fixed API.

There are honestly too many internal API changes to list them all in the change notes. But for the last used dates, I'll make a note to call it out in the changelog if it ever changes.

@WesleyHindle
Copy link
Author

Thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants