mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2026-01-06 12:36:56 +00:00
Add last login time in User when logging in (#20390)
* Add last login time * Add change to entitySpecificUpdate * remove last login time from createOrUpdate * moved UpdateLastLogin to user repository * Added last login Time on login (#20419) * updated last login time * removed condition * added unit tests --------- Co-authored-by: Dhruv Parmar <83108871+dhruvjsx@users.noreply.github.com> Co-authored-by: Karan Hotchandani <33024356+karanh37@users.noreply.github.com>
This commit is contained in:
parent
321b8f7f65
commit
4aa8a0c0a1
@ -72,6 +72,7 @@ import org.openmetadata.service.jdbi3.SystemRepository;
|
||||
import org.openmetadata.service.jdbi3.TokenRepository;
|
||||
import org.openmetadata.service.jdbi3.TypeRepository;
|
||||
import org.openmetadata.service.jdbi3.UsageRepository;
|
||||
import org.openmetadata.service.jdbi3.UserRepository;
|
||||
import org.openmetadata.service.jobs.JobDAO;
|
||||
import org.openmetadata.service.resources.feeds.MessageParser.EntityLink;
|
||||
import org.openmetadata.service.search.SearchRepository;
|
||||
@ -740,4 +741,8 @@ public final class Entity {
|
||||
|
||||
return allServices;
|
||||
}
|
||||
|
||||
public static UserRepository getUserRepository() {
|
||||
return (UserRepository) Entity.getEntityRepository(Entity.USER);
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ import static org.openmetadata.service.Entity.FIELD_DOMAINS;
|
||||
import static org.openmetadata.service.Entity.ROLE;
|
||||
import static org.openmetadata.service.Entity.TEAM;
|
||||
import static org.openmetadata.service.Entity.USER;
|
||||
import static org.openmetadata.service.util.EntityUtil.objectMatch;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
@ -37,6 +38,7 @@ import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javax.json.JsonPatch;
|
||||
import javax.ws.rs.core.SecurityContext;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -221,6 +223,14 @@ public class UserRepository extends EntityRepository<User> {
|
||||
user.withRoles(roles).withTeams(teams);
|
||||
}
|
||||
|
||||
public void updateUserLastLoginTime(User orginalUser, long lastLoginTime) {
|
||||
User updatedUser = JsonUtils.deepCopy(orginalUser, User.class);
|
||||
JsonPatch patch =
|
||||
JsonUtils.getJsonPatch(orginalUser, updatedUser.withLastLoginTime(lastLoginTime));
|
||||
UserRepository userRepository = (UserRepository) Entity.getEntityRepository(Entity.USER);
|
||||
userRepository.patch(null, orginalUser.getId(), orginalUser.getUpdatedBy(), patch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeRelationships(User user) {
|
||||
assignRoles(user, user.getRoles());
|
||||
@ -632,6 +642,13 @@ public class UserRepository extends EntityRepository<User> {
|
||||
public void entitySpecificUpdate(boolean consolidatingChanges) {
|
||||
// LowerCase Email
|
||||
updated.setEmail(original.getEmail().toLowerCase());
|
||||
recordChange(
|
||||
"lastLoginTime",
|
||||
original.getLastLoginTime(),
|
||||
updated.getLastLoginTime(),
|
||||
false,
|
||||
objectMatch,
|
||||
true);
|
||||
|
||||
// Updates
|
||||
updateRoles(original, updated);
|
||||
|
||||
@ -600,7 +600,10 @@ public class UserResource extends EntityResource<User, UserRepository> {
|
||||
} catch (EntityNotFoundException ex) {
|
||||
if (isSelfSignUpEnabled) {
|
||||
if (securityContext.getUserPrincipal().getName().equals(user.getName())) {
|
||||
User created = addHref(uriInfo, repository.create(uriInfo, user));
|
||||
User created =
|
||||
addHref(
|
||||
uriInfo,
|
||||
repository.create(uriInfo, user.withLastLoginTime(System.currentTimeMillis())));
|
||||
createdUserRes = Response.created(created.getHref()).entity(created).build();
|
||||
} else {
|
||||
throw new CustomExceptionMessage(
|
||||
|
||||
@ -663,6 +663,12 @@ public class AuthenticationCodeFlowHandler {
|
||||
|
||||
String redirectUri = (String) request.getSession().getAttribute(SESSION_REDIRECT_URI);
|
||||
|
||||
String storedUserStr =
|
||||
Entity.getCollectionDAO().userDAO().findUserByNameAndEmail(userName, email);
|
||||
if (storedUserStr != null) {
|
||||
User user = JsonUtils.readValue(storedUserStr, User.class);
|
||||
Entity.getUserRepository().updateUserLastLoginTime(user, System.currentTimeMillis());
|
||||
}
|
||||
String url =
|
||||
String.format(
|
||||
"%s?id_token=%s&email=%s&name=%s",
|
||||
|
||||
@ -469,6 +469,7 @@ public class BasicAuthenticator implements AuthenticatorHandler {
|
||||
checkIfLoginBlocked(email);
|
||||
User storedUser = lookUserInProvider(email, loginRequest.getPassword());
|
||||
validatePassword(email, loginRequest.getPassword(), storedUser);
|
||||
Entity.getUserRepository().updateUserLastLoginTime(storedUser, System.currentTimeMillis());
|
||||
return getJwtResponse(storedUser, SecurityUtil.getLoginConfiguration().getJwtTokenExpiryTime());
|
||||
}
|
||||
|
||||
|
||||
@ -143,6 +143,7 @@ public class LdapAuthenticator implements AuthenticatorHandler {
|
||||
String email = loginRequest.getEmail();
|
||||
checkIfLoginBlocked(email);
|
||||
User omUser = lookUserInProvider(email, loginRequest.getPassword());
|
||||
Entity.getUserRepository().updateUserLastLoginTime(omUser, System.currentTimeMillis());
|
||||
return getJwtResponse(omUser, SecurityUtil.getLoginConfiguration().getJwtTokenExpiryTime());
|
||||
}
|
||||
|
||||
|
||||
@ -136,6 +136,7 @@ public class SamlAssertionConsumerServlet extends HttpServlet {
|
||||
jwtAuthMechanism.getJWTToken(),
|
||||
nameId,
|
||||
username);
|
||||
Entity.getUserRepository().updateUserLastLoginTime(user, System.currentTimeMillis());
|
||||
resp.sendRedirect(url);
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,6 +143,10 @@
|
||||
"domains" : {
|
||||
"description": "Domain the User belongs to. This is inherited by the team the user belongs to.",
|
||||
"$ref": "../../type/entityReferenceList.json"
|
||||
},
|
||||
"lastLoginTime": {
|
||||
"description": "Last time the user logged in.",
|
||||
"$ref": "../../type/basic.json#/definitions/timestamp"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
@ -82,6 +82,10 @@ export interface User {
|
||||
* If the User has verified the mail
|
||||
*/
|
||||
isEmailVerified?: boolean;
|
||||
/**
|
||||
* Last time the user logged in.
|
||||
*/
|
||||
lastLoginTime?: number;
|
||||
/**
|
||||
* A unique name of the user, typically the user ID from an identity provider. Example - uid
|
||||
* from LDAP.
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
*/
|
||||
import { OidcUser } from '../components/Auth/AuthProviders/AuthProvider.interface';
|
||||
import { User } from '../generated/entity/teams/user';
|
||||
import * as userAPI from '../rest/userAPI';
|
||||
import { checkIfUpdateRequired, getUserWithImage } from './UserDataUtils';
|
||||
|
||||
describe('getUserWithImage', () => {
|
||||
@ -91,6 +92,16 @@ describe('getUserWithImage', () => {
|
||||
});
|
||||
|
||||
describe('checkIfUpdateRequired', () => {
|
||||
const mockTimestamp = 1642512000000;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Date, 'now').mockImplementation(() => mockTimestamp);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return the updated user details if update is required', async () => {
|
||||
const existingUserDetails: User = {
|
||||
email: 'a@a.com',
|
||||
@ -149,4 +160,41 @@ describe('checkIfUpdateRequired', () => {
|
||||
|
||||
expect(updatedUserDetails).toEqual(existingUserDetails);
|
||||
});
|
||||
|
||||
it('should call updateUserDetail with correct payload', async () => {
|
||||
// Import the module containing the function
|
||||
|
||||
const existingUserDetails: User = {
|
||||
email: 'a@a.com',
|
||||
id: '1',
|
||||
name: 'user',
|
||||
isBot: false,
|
||||
};
|
||||
// Spy on the function within the module
|
||||
const mockUpdateUserDetail = jest
|
||||
.spyOn(userAPI, 'updateUserDetail')
|
||||
.mockResolvedValue(existingUserDetails);
|
||||
|
||||
const newUser: OidcUser = {
|
||||
id_token: 'idToken',
|
||||
scope: 'scope',
|
||||
profile: {
|
||||
email: 'a@a.com',
|
||||
name: 'user',
|
||||
picture: '',
|
||||
preferred_username: 'preferred_username',
|
||||
sub: 'sub',
|
||||
},
|
||||
};
|
||||
|
||||
await checkIfUpdateRequired(existingUserDetails, newUser);
|
||||
|
||||
expect(mockUpdateUserDetail).toHaveBeenCalledWith(existingUserDetails.id, [
|
||||
{
|
||||
op: 'add',
|
||||
path: '/lastLoginTime',
|
||||
value: mockTimestamp,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -90,6 +90,7 @@ export const checkIfUpdateRequired = async (
|
||||
newUser: OidcUser
|
||||
): Promise<User> => {
|
||||
const updatedUserData = getUserDataFromOidc(existingUserDetails, newUser);
|
||||
let finalData = { ...existingUserDetails, lastLoginTime: Date.now() };
|
||||
|
||||
if (existingUserDetails.email !== updatedUserData.email) {
|
||||
return existingUserDetails;
|
||||
@ -99,28 +100,28 @@ export const checkIfUpdateRequired = async (
|
||||
updatedUserData.profile?.images &&
|
||||
!matchUserDetails(existingUserDetails, updatedUserData, ['profile'])
|
||||
) {
|
||||
const finalData = {
|
||||
...existingUserDetails,
|
||||
finalData = {
|
||||
...finalData,
|
||||
// We want to override any profile information that is coming from the OIDC provider
|
||||
profile: {
|
||||
...existingUserDetails.profile,
|
||||
...updatedUserData.profile,
|
||||
},
|
||||
};
|
||||
const jsonPatch = compare(existingUserDetails, finalData);
|
||||
}
|
||||
const jsonPatch = compare(existingUserDetails, finalData);
|
||||
|
||||
try {
|
||||
const res = await updateUserDetail(existingUserDetails.id, jsonPatch);
|
||||
try {
|
||||
const res = await updateUserDetail(existingUserDetails.id, jsonPatch);
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
showErrorToast(
|
||||
error as AxiosError,
|
||||
i18n.t('server.entity-updating-error', {
|
||||
entity: i18n.t('label.admin-profile'),
|
||||
})
|
||||
);
|
||||
}
|
||||
return res;
|
||||
} catch (error) {
|
||||
showErrorToast(
|
||||
error as AxiosError,
|
||||
i18n.t('server.entity-updating-error', {
|
||||
entity: i18n.t('label.admin-profile'),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return existingUserDetails;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user