mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-07 08:38:23 +00:00
fix(ui): pick email and name based on claim values at time of login (#17626)
* fix(ui): pick email and name based on claim values at time of login * Update Error Message * Update code for better message * Typo * fix playwright failures * fix playwright tests * fix tests * fix login spec failing --------- Co-authored-by: mohitdeuex <mohit.y@deuexsolutions.com> (cherry picked from commit fa3529f0850ed801e11b752b963ab0cafb637e87)
This commit is contained in:
parent
29a29cde4d
commit
2a3ba61703
@ -24,7 +24,15 @@ public final class BadRequestException extends WebServiceException {
|
||||
super(Response.Status.BAD_REQUEST, ERROR_TYPE, DEFAULT_MESSAGE);
|
||||
}
|
||||
|
||||
private BadRequestException(String message) {
|
||||
super(Response.Status.BAD_REQUEST, ERROR_TYPE, message);
|
||||
}
|
||||
|
||||
public static BadRequestException of() {
|
||||
return new BadRequestException();
|
||||
}
|
||||
|
||||
public static BadRequestException of(String message) {
|
||||
return new BadRequestException(message);
|
||||
}
|
||||
}
|
||||
|
@ -3747,6 +3747,13 @@ public interface CollectionDAO {
|
||||
@SqlQuery("SELECT COUNT(*) FROM user_entity WHERE LOWER(email) = LOWER(:email)")
|
||||
int checkEmailExists(@Bind("email") String email);
|
||||
|
||||
@SqlQuery("SELECT COUNT(*) FROM user_entity WHERE LOWER(name) = LOWER(:name)")
|
||||
int checkUserNameExists(@Bind("name") String name);
|
||||
|
||||
@SqlQuery(
|
||||
"SELECT json FROM user_entity WHERE LOWER(name) = LOWER(:name) AND LOWER(email) = LOWER(:email)")
|
||||
String findUserByNameAndEmail(@Bind("name") String name, @Bind("email") String email);
|
||||
|
||||
@SqlQuery("SELECT json FROM user_entity WHERE LOWER(email) = LOWER(:email)")
|
||||
String findUserByEmail(@Bind("email") String email);
|
||||
|
||||
|
@ -60,6 +60,7 @@ import org.openmetadata.schema.type.csv.CsvImportResult;
|
||||
import org.openmetadata.schema.utils.EntityInterfaceUtil;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.OpenMetadataApplicationConfig;
|
||||
import org.openmetadata.service.exception.BadRequestException;
|
||||
import org.openmetadata.service.exception.CatalogExceptionMessage;
|
||||
import org.openmetadata.service.exception.EntityNotFoundException;
|
||||
import org.openmetadata.service.jdbi3.CollectionDAO.EntityRelationshipRecord;
|
||||
@ -146,7 +147,21 @@ public class UserRepository extends EntityRepository<User> {
|
||||
}
|
||||
|
||||
public User getByEmail(UriInfo uriInfo, String email, Fields fields) {
|
||||
String userString = ((CollectionDAO.UserDAO) dao).findUserByEmail(email);
|
||||
String userString = daoCollection.userDAO().findUserByEmail(email);
|
||||
if (userString == null) {
|
||||
throw EntityNotFoundException.byMessage(CatalogExceptionMessage.entityNotFound(USER, email));
|
||||
}
|
||||
User user = JsonUtils.readValue(userString, User.class);
|
||||
setFieldsInternal(user, fields);
|
||||
setInheritedFields(user, fields);
|
||||
// Clone the entity
|
||||
User entityClone = JsonUtils.deepCopy(user, User.class);
|
||||
clearFieldsInternal(entityClone, fields);
|
||||
return withHref(uriInfo, entityClone);
|
||||
}
|
||||
|
||||
public User getUserByNameAndEmail(UriInfo uriInfo, String name, String email, Fields fields) {
|
||||
String userString = daoCollection.userDAO().findUserByNameAndEmail(name, email);
|
||||
if (userString == null) {
|
||||
throw EntityNotFoundException.byMessage(CatalogExceptionMessage.entityNotFound(USER, email));
|
||||
}
|
||||
@ -314,6 +329,10 @@ public class UserRepository extends EntityRepository<User> {
|
||||
return daoCollection.userDAO().checkEmailExists(emailId) > 0;
|
||||
}
|
||||
|
||||
public boolean checkUserNameExists(String username) {
|
||||
return daoCollection.userDAO().checkUserNameExists(username) > 0;
|
||||
}
|
||||
|
||||
public void initializeUsers(OpenMetadataApplicationConfig config) {
|
||||
AuthProvider authProvider = config.getAuthenticationConfiguration().getProvider();
|
||||
// Create Admins
|
||||
@ -360,19 +379,45 @@ public class UserRepository extends EntityRepository<User> {
|
||||
public List<EntityReference> getGroupTeams(
|
||||
UriInfo uriInfo, SecurityContext context, String email) {
|
||||
// Cleanup
|
||||
User user = getByEmail(uriInfo, email, Fields.EMPTY_FIELDS);
|
||||
validateLoggedInUserNameAndEmailMatches(context.getUserPrincipal().getName(), email, user);
|
||||
User user =
|
||||
getLoggedInUserByNameAndEmail(
|
||||
uriInfo, context.getUserPrincipal().getName(), email, Fields.EMPTY_FIELDS);
|
||||
List<EntityReference> teams = getTeams(user);
|
||||
return getGroupTeams(teams);
|
||||
}
|
||||
|
||||
public void validateLoggedInUserNameAndEmailMatches(
|
||||
String username, String email, User storedUser) {
|
||||
String lowerCasedName = username.toLowerCase();
|
||||
String lowerCasedEmail = email.toLowerCase();
|
||||
if (!(lowerCasedName.equals(storedUser.getName().toLowerCase())
|
||||
&& lowerCasedEmail.equals(storedUser.getEmail().toLowerCase()))) {
|
||||
throw EntityNotFoundException.byMessage(CatalogExceptionMessage.entityNotFound(USER, email));
|
||||
public User getLoggedInUserByNameAndEmail(
|
||||
UriInfo uriInfo, String username, String email, Fields fields) {
|
||||
try {
|
||||
return getUserByNameAndEmail(uriInfo, username, email, fields);
|
||||
} catch (EntityNotFoundException e) {
|
||||
boolean existByName = checkUserNameExists(username);
|
||||
boolean existByEmail = checkEmailAlreadyExists(email);
|
||||
if (existByName && !existByEmail) {
|
||||
User userByName = getByName(uriInfo, username, Fields.EMPTY_FIELDS);
|
||||
throw BadRequestException.of(
|
||||
String.format(
|
||||
"User with given name exists but is not associated with the provided email. "
|
||||
+ "Matching User Found By Name [username:email] : [%s:%s], Provided User: [%s:%s]",
|
||||
userByName.getName().toLowerCase(),
|
||||
userByName.getEmail().toLowerCase(),
|
||||
username,
|
||||
email));
|
||||
} else if (!existByName && existByEmail) {
|
||||
User userByEmail = getByEmail(uriInfo, email, Fields.EMPTY_FIELDS);
|
||||
throw BadRequestException.of(
|
||||
String.format(
|
||||
"User with given email exists but is not associated with provider username. "
|
||||
+ "Matching User Found By Email [username:email] : [%s:%s], Provided User: [%s:%s]",
|
||||
userByEmail.getName().toLowerCase(),
|
||||
userByEmail.getEmail().toLowerCase(),
|
||||
username,
|
||||
email));
|
||||
} else {
|
||||
throw EntityNotFoundException.byMessage(
|
||||
String.format(
|
||||
"User with provider name : %s and email : %s not found", username, email));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -450,10 +450,9 @@ public class UserResource extends EntityResource<User, UserRepository> {
|
||||
(CatalogSecurityContext) containerRequestContext.getSecurityContext();
|
||||
Fields fields = getFields(fieldsParam);
|
||||
String currentEmail = ((CatalogPrincipal) catalogSecurityContext.getUserPrincipal()).getEmail();
|
||||
User user = repository.getByEmail(uriInfo, currentEmail, fields);
|
||||
|
||||
repository.validateLoggedInUserNameAndEmailMatches(
|
||||
securityContext.getUserPrincipal().getName(), currentEmail, user);
|
||||
User user =
|
||||
repository.getLoggedInUserByNameAndEmail(
|
||||
uriInfo, catalogSecurityContext.getUserPrincipal().getName(), currentEmail, fields);
|
||||
|
||||
// Sync the Roles from token to User
|
||||
if (Boolean.TRUE.equals(authorizerConfiguration.getUseRolesFromProvider())
|
||||
|
@ -66,7 +66,9 @@ test.describe('Glossary tests', () => {
|
||||
const glossary1 = new Glossary();
|
||||
glossary1.data.owners = [{ name: 'admin', type: 'user' }];
|
||||
glossary1.data.mutuallyExclusive = true;
|
||||
glossary1.data.reviewers = [{ name: user1.getUserName(), type: 'user' }];
|
||||
glossary1.data.reviewers = [
|
||||
{ name: `${user1.data.firstName}${user1.data.lastName}`, type: 'user' },
|
||||
];
|
||||
glossary1.data.terms = [new GlossaryTerm(glossary1)];
|
||||
|
||||
await test.step('Create Glossary', async () => {
|
||||
|
@ -67,6 +67,7 @@ import {
|
||||
getUrlPathnameExpiry,
|
||||
getUserManagerConfig,
|
||||
isProtectedRoute,
|
||||
prepareUserProfileFromClaims,
|
||||
} from '../../../utils/AuthProvider.util';
|
||||
import { escapeESReservedCharacters } from '../../../utils/StringsUtils';
|
||||
import { showErrorToast, showInfoToast } from '../../../utils/ToastUtils';
|
||||
@ -122,6 +123,9 @@ export const AuthProvider = ({
|
||||
setAuthConfig,
|
||||
setAuthorizerConfig,
|
||||
setIsSigningUp,
|
||||
authorizerConfig,
|
||||
jwtPrincipalClaims,
|
||||
jwtPrincipalClaimsMapping,
|
||||
setJwtPrincipalClaims,
|
||||
setJwtPrincipalClaimsMapping,
|
||||
removeRefreshToken,
|
||||
@ -376,10 +380,18 @@ export const AuthProvider = ({
|
||||
? userAPIQueryFields + ',' + isEmailVerifyField
|
||||
: userAPIQueryFields;
|
||||
try {
|
||||
const newUser = prepareUserProfileFromClaims({
|
||||
user,
|
||||
jwtPrincipalClaims,
|
||||
principalDomain: authorizerConfig?.principalDomain ?? '',
|
||||
jwtPrincipalClaimsMapping,
|
||||
clientType,
|
||||
});
|
||||
|
||||
const res = await getLoggedInUser({ fields });
|
||||
if (res) {
|
||||
const updatedUserData = getUserDataFromOidc(res, user);
|
||||
if (!matchUserDetails(res, updatedUserData, ['email'])) {
|
||||
const updatedUserData = getUserDataFromOidc(res, newUser);
|
||||
if (!matchUserDetails(res, updatedUserData, ['profile', 'email'])) {
|
||||
getUpdatedUser(updatedUserData, res);
|
||||
} else {
|
||||
setCurrentUser(res);
|
||||
@ -414,6 +426,10 @@ export const AuthProvider = ({
|
||||
},
|
||||
[
|
||||
authConfig?.enableSelfSignup,
|
||||
clientType,
|
||||
authorizerConfig?.principalDomain,
|
||||
jwtPrincipalClaims,
|
||||
jwtPrincipalClaimsMapping,
|
||||
setIsSigningUp,
|
||||
setIsAuthenticated,
|
||||
setApplicationLoading,
|
||||
|
@ -39,6 +39,7 @@ import {
|
||||
} from '../../../utils/ToastUtils';
|
||||
import { resetWebAnalyticSession } from '../../../utils/WebAnalyticsUtils';
|
||||
|
||||
import { toLower } from 'lodash';
|
||||
import { useApplicationStore } from '../../../hooks/useApplicationStore';
|
||||
import { OidcUser } from './AuthProvider.interface';
|
||||
|
||||
@ -115,7 +116,7 @@ const BasicAuthProvider = ({
|
||||
onLoginSuccess({
|
||||
id_token: response.accessToken,
|
||||
profile: {
|
||||
email: response.email,
|
||||
email: toLower(email),
|
||||
name: '',
|
||||
picture: '',
|
||||
sub: '',
|
||||
|
@ -22,6 +22,7 @@ import { first, get, isEmpty, isNil } from 'lodash';
|
||||
import { WebStorageStateStore } from 'oidc-client';
|
||||
import {
|
||||
AuthenticationConfigurationWithScope,
|
||||
OidcUser,
|
||||
UserProfile,
|
||||
} from '../components/Auth/AuthProviders/AuthProvider.interface';
|
||||
import { REDIRECT_PATHNAME, ROUTES } from '../constants/constants';
|
||||
@ -377,3 +378,46 @@ export const setUrlPathnameExpiryAfterRoute = (pathname: string) => {
|
||||
path: '/',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* We support Principle claim as: email,preferred_username,sub in any order
|
||||
* When Users are created from the initialAdmin we want to pick correct user details based on the principle claim
|
||||
* This method will ensure that name & email are correctly picked from the principle claim
|
||||
* @param user - User details extracted from Token
|
||||
* @param jwtPrincipalClaims - List of principle claims coming from auth API response
|
||||
* @param principalDomain - Principle Domain value coming from
|
||||
* @param jwtPrincipalClaimsMapping - Mapping of principle claims to user profile
|
||||
* @param clientType - Client Type Public or Confidential
|
||||
* @returns OidcUser with Profile info plucked based on the principle claim
|
||||
*/
|
||||
export const prepareUserProfileFromClaims = ({
|
||||
user,
|
||||
jwtPrincipalClaims,
|
||||
principalDomain,
|
||||
jwtPrincipalClaimsMapping,
|
||||
clientType,
|
||||
}: {
|
||||
user: OidcUser;
|
||||
jwtPrincipalClaims: string[];
|
||||
principalDomain: string;
|
||||
jwtPrincipalClaimsMapping: string[];
|
||||
clientType: ClientType;
|
||||
}): OidcUser => {
|
||||
const newUser = {
|
||||
...user,
|
||||
profile:
|
||||
clientType === ClientType.Public
|
||||
? getNameFromUserData(
|
||||
user.profile,
|
||||
jwtPrincipalClaims,
|
||||
principalDomain,
|
||||
jwtPrincipalClaimsMapping
|
||||
)
|
||||
: {
|
||||
name: user.profile?.name ?? '',
|
||||
email: user.profile?.email ?? '',
|
||||
},
|
||||
} as OidcUser;
|
||||
|
||||
return newUser;
|
||||
};
|
||||
|
@ -100,3 +100,69 @@ describe('Test Auth Provider utils', () => {
|
||||
expect(email).toEqual('i_am_preferred_username@test.com');
|
||||
});
|
||||
});
|
||||
|
||||
import { OidcUser } from '../components/Auth/AuthProviders/AuthProvider.interface';
|
||||
import { ClientType } from '../generated/configuration/authenticationConfiguration';
|
||||
import { prepareUserProfileFromClaims } from './AuthProvider.util';
|
||||
|
||||
describe('prepareUserProfileFromClaims', () => {
|
||||
const mockUser: OidcUser = {
|
||||
profile: {
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@example.com',
|
||||
},
|
||||
} as OidcUser;
|
||||
|
||||
const mockJwtPrincipalClaims = ['email'];
|
||||
const mockPrincipalDomain = 'example.com';
|
||||
const mockJwtPrincipalClaimsMapping = ['username:name', 'email:email'];
|
||||
|
||||
it('should prepare user profile for public client type', () => {
|
||||
const result = prepareUserProfileFromClaims({
|
||||
user: mockUser,
|
||||
jwtPrincipalClaims: mockJwtPrincipalClaims,
|
||||
principalDomain: mockPrincipalDomain,
|
||||
jwtPrincipalClaimsMapping: mockJwtPrincipalClaimsMapping,
|
||||
clientType: ClientType.Public,
|
||||
});
|
||||
|
||||
expect(result.profile).toEqual({
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should prepare user profile for non-public client type', () => {
|
||||
const result = prepareUserProfileFromClaims({
|
||||
user: mockUser,
|
||||
jwtPrincipalClaims: mockJwtPrincipalClaims,
|
||||
principalDomain: mockPrincipalDomain,
|
||||
jwtPrincipalClaimsMapping: mockJwtPrincipalClaimsMapping,
|
||||
clientType: ClientType.Confidential,
|
||||
});
|
||||
|
||||
expect(result.profile).toEqual({
|
||||
name: 'John Doe',
|
||||
email: 'john.doe@example.com',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing profile fields for non-public client type', () => {
|
||||
const mockUserWithMissingFields: OidcUser = {
|
||||
profile: {},
|
||||
} as OidcUser;
|
||||
|
||||
const result = prepareUserProfileFromClaims({
|
||||
user: mockUserWithMissingFields,
|
||||
jwtPrincipalClaims: mockJwtPrincipalClaims,
|
||||
principalDomain: mockPrincipalDomain,
|
||||
jwtPrincipalClaimsMapping: mockJwtPrincipalClaimsMapping,
|
||||
clientType: ClientType.Confidential,
|
||||
});
|
||||
|
||||
expect(result.profile).toEqual({
|
||||
name: '',
|
||||
email: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user