-
Notifications
You must be signed in to change notification settings - Fork 491
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
Add model last login to domain #17332
Conversation
743ffe6
to
f0298b0
Compare
@@ -34,7 +35,7 @@ type UserService interface { | |||
GetUserByName(ctx context.Context, name string) (coreuser.User, error) | |||
// UpdateLastLogin updates the last login time for the user with the | |||
// given name. | |||
UpdateLastLogin(ctx context.Context, name string) error | |||
UpdateLastLogin(ctx context.Context, modelUUID coremodel.UUID, name string) error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that it's the access (user) service, put the name first as the most relevant argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, changed for this and for LastModelLogin as well
domain/schema/controller.go
Outdated
SELECT time AS last_login, user_uuid | ||
FROM model_last_login | ||
GROUP BY user_uuid | ||
ORDER BY time DESC LIMIT 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This won't work. It limits the whole query to one row.
If you treat the max as a dummy value, you can still select time, because you're grouping by the user.
SELECT time AS last_login, user_uuid, MAX(time) as _
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah nice, changed to that
b38813e
to
f5afdb4
Compare
56d12ee
to
be5ff3a
Compare
@Aflynn50 please run the |
651f249
to
4ba4774
Compare
4222b9d
to
d2c97d8
Compare
Hey @Aflynn50, Thanks for the pickup on the bug. I have a had a look at the code and I think the bug has always been there I just manage to re-implement the bug in DQlite perfectly. Under the way it use to be for state we called
Take a look at the 3.6 branch and see if you can confirm that this was already a bug we had and I have just managed to replicate it in Dqlite? Assuming the above is correct I think we just need to decide what the logic should be and I am happy to throw in the change. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @Aflynn50, this is a very nice PR!
@@ -967,7 +968,7 @@ func (s *loginSuite) TestLoginUpdatesLastLoginAndConnection(c *gc.C) { | |||
model := s.ControllerModel(c) | |||
modelUser, err := model.State().UserAccess(names.NewUserTag("bobbrown"), model.ModelTag()) | |||
c.Assert(err, jc.ErrorIsNil) | |||
when, err := model.LastModelConnection(modelUser.UserTag) | |||
when, err := userService.LastModelLogin(context.Background(), modelUser.UserTag.Name(), coremodel.UUID(model.UUID())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small nit..... I am wondering if the arg order need to be swapped around here to be ctx, model id, user id
. To align with how we are asking the question. i.e For this model when did this user last login.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I checked with Joe about this, we decided that the user should go first because this is on the user service. I see your point though, I did have it the other way round originally
domain/access/errors/errors.go
Outdated
|
||
// UserNeverConnectedToModel describes an error that occurs if a user has | ||
// never connected to a model. | ||
UserNeverConnectedToModel = errors.ConstError("user never connected to model") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please disregard if you don't agree. I am not sure the "connected" part here make a lot of sense. Connected isn't a term we currently use for Juju to describe model access and could be confusing we we are trying to rationalise what connected means.
Maybe simply "accessed" is what we are after?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accessed sounds better, I'll change it. I copied this over from state where there were two confusingly similar concepts of a user logging in and a user connecting to a model. This PR replaces them with just the concept of login so this should go as well.
Could there be any problem with changing the wording of this error w.r.t. public APIs? I assume its something we can change with the move to 4.0 if there was but it would be good to know if changes to error messages like this could require changes in external clients.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No we will be fine changing wording. A lot of errors strings from deep in Juju end up a user errors unfortunately. Time and determination will fix this.
if err != nil { | ||
if !state.IsNeverConnectedError(err) { | ||
if !errors.Is(err, accesserrors.UserNeverConnectedToModel) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left a comment below around the term "connected" but I see why you chose it. To match what we already had.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep :)
@@ -404,9 +409,9 @@ func (c *ControllerAPI) AllModels(ctx context.Context) (params.UserModelList, er | |||
}, | |||
} | |||
|
|||
lastConn, err := model.LastModelConnection(c.apiUser) | |||
lastConn, err := c.accessService.LastModelLogin(ctx, c.apiUser.Name(), coremodel.UUID(model.UUID())) | |||
if err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit..... We can combine these to if checks together and remove a line of indentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Combining them would change the behavior. In the case where the error is UserNeverAccessedModel then combining them would cause it to be set to lastConn which it is not currently.
I'll change this clause to make it more explicit in setting lastConn to nil because right now its a bit obfuscated.
// given name. | ||
UpdateLastLogin(ctx context.Context, name string) error | ||
UpdateLastModelLogin(ctx context.Context, name string, modelUUID coremodel.UUID) error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comments as below for fetching. I think the argument order needs to be swapped here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See reply on LastModelLogin for why the user name is first.
// - accesserrors.UserNotFound: When the user cannot be found. | ||
// - accesserrors.UserNeverConnectedToModel: If there is no record of the user | ||
// connecting to the model. | ||
LastModelLogin(context.Context, string, coremodel.UUID) (time.Time, error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should probably exist a modelerrors.NotFound
in this list for LastModelLogin
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently the error returned in that case would be UserNeverAccessedModel
. I've just changed it now though to do a check for a missing model in the case that it gets an ErrorNoRows from the database so that we return the correct error in that case. I've added it now to this comment too.
creator.name AS &dbUser.created_by_name | ||
FROM v_user_auth u | ||
LEFT JOIN user AS creator | ||
ON u.created_by_uuid = creator.uuid | ||
LEFT JOIN v_user_last_login AS ull |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because this is a left join now what happens when last_login
is null or empty string. Do we default off to the zero value of time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do yea. I'm not changing the behavior from before here, though whether its right is a different matter.
IMO the field on user should really be a pointer to time.Time
. I'll check with the others if we should change this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just discussed this with the team. They're happy just leaving it as a non pointer and doing checkers for the 0 unix time rather than risking nil pointer errors.
// The foreign key constrain may be triggered if the user or the | ||
// model does not exist. However, the user must exist or the | ||
// uuidForName query would have failed, so it must be the model. | ||
return modelerrors.NotFound |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we please add more context to this error?
fmt.Errorf("updating last model login for user %q on model %q: %w")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added.
// - accesserrors.UserNotFound: When the user cannot be found. | ||
// - accesserrors.UserNeverConnectedToModel: If there is no record of the user | ||
// connecting to the model. | ||
func (st *UserState) LastModelLogin(ctx context.Context, name string, modelUUID coremodel.UUID) (time.Time, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Random question and thought. Currently we don't check to see if the model actually exists? Should we use a left join and do the work so we can be explicit and strict or no?
Happy to go either way. Just asking the question :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea this is a good point. I've added a check in the case that we get no rows from the query to model last login. This way we're not doing the extra work when we don't need to but we are ensuring that it exists when it should.
domain/schema/controller.go
Outdated
CREATE TABLE model_last_login ( | ||
model_uuid TEXT NOT NULL, | ||
user_uuid TEXT NOT NULL, | ||
time TIMESTAMP, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be NOT NULL?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, added a not null.
I had left it as nullable because ideally we would actually insert a row in here on user creation (with a null timestamp) and use a update to add last logins. As it is we use an upsert to add the row in. I discussed this with Joe and right now we don't have a centralised place where we add users so we're not really ready to switch to doing that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some comments.
gomock.Any(), | ||
"admin", | ||
coremodel.UUID(s.st.ModelUUID()), | ||
).Return(time.Time{}, accesserrors.UserNeverAccessedModel) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it doesn't seem like you're ever testing the scenario where a valid time is returned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, I've added an actual value to TestListModelSummaries
to test it there and reverted the change that removes it here. The others I have left as UserNeverAccessedModel
.
domain/access/state/user.go
Outdated
WHERE uuid = $M.uuid` | ||
SELECT (u.uuid, u.name, u.display_name, u.created_by_uuid, u.created_at, ull.last_login, u.disabled) AS (&dbUser.*) | ||
FROM v_user_auth u | ||
LEFT JOIN v_user_last_login ull ON u.uuid = ull.user_uuid |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indent the join so it's under v_user_last_login.
domain/access/state/user.go
Outdated
AND removed = false` | ||
SELECT (u.uuid, u.name, u.display_name, u.created_by_uuid, u.created_at, ull.last_login, u.disabled) AS (&dbUser.*) | ||
FROM v_user_auth u | ||
LEFT JOIN v_user_last_login ull ON u.uuid = ull.user_uuid |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indent here too.
domain/access/state/user.go
Outdated
return errors.Annotate(err, "preparing activation key deletion query") | ||
} | ||
|
||
deleteLastLoginStmt, err := st.Prepare("DELETE FROM model_last_login WHERE user_uuid = $M.uuid", m) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that deletion is a soft delete, and we haven't actually removed the record, there's no need to delete these entries. They might even prove useful for auditing purposes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'll remove this deletion
domain/access/state/user.go
Outdated
} | ||
|
||
getLastModelLoginTime := ` | ||
SELECT time AS &loginTime.time |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be hard to the left, with vertical justification after the keywords.
ed10424
to
e310c98
Compare
Stop updating the mongoDB LastModelConnection Add checks to ensure password and activation were deleted by RemoveUser. The fact that RemoveUser deleted these values was previously untested.
2568328
to
94c60ba
Compare
/build |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for seeing all the changes through.
I was able to create a new user/model and verify directly against the DB that last login times are written as appropriate.
/merge |
Juju stores the time each user last logged in to each model. It previously did this via a collection in mongo. This PR adds a table to the access service that tracks the users last logins.
The last login column is removed from the user table and the users last login is instead calculated by selecting the most recent login by that user to any model from the new table.
Currently we are double writing the last login to mongo and to DQLite but a subsequent PR will remove the need to do that by replacing all the remaining last login calls.
The tests for ListModels in the model-manager are currently commented out so this functionality is tests. I had a go at fixing the tests but realised that ListModels itself does not work as expected and fixing that is out of scope of this PR. I have coordinated with @tlm and he will land a patch for this soon.
Checklist
QA steps
The QA for this PR is a little difficult since the login and permissions stories are currently in flux. Specifically, when a is granted access to a model and logs in they do not see the model when they do
juju models
(this is likely because of the changes by @tlm that move ListModels to use state). There is also the issue that adding a model and accessing it does not trigger the expected login code and therefore, new models added do not have their last login updated.The extent of the QA possible right now is:
In the future, it should be possible to add a model and see that the last connection is just now. For the moment it says never connected.
It should also be possible to see the last logins of other users, however listing the models of other users is currently not working. You should be able to do:
Links
Jira card: JUJU-5835