UI : Mask the JWT token in Metadata service (#8842)

* Mask the Jwt token in Metadata service

* minor fix

* fix the icon alignment in Add Ingestion button

* disable the test connection for metadata service OpenMetadata type

* change the css name

* fix unit test issue

* Fix Auth Provider

* Fix add ingestion dropdown icon alignment

* Fix formatting

* Do not encryt JWT auth mechanism with secrets manager

Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
Co-authored-by: ulixius9 <mayursingal9@gmail.com>
Co-authored-by: Sachin Chaurasiya <sachinchaurasiyachotey87@gmail.com>
Co-authored-by: mohitdeuex <mohit.y@deuexsolutions.com>
Co-authored-by: Nahuel Verdugo Revigliono <nahuel@getcollate.io>
This commit is contained in:
Ashish Gupta 2022-11-22 19:47:40 +05:30 committed by GitHub
parent b2c1e42f9b
commit 36f27e947d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 79 additions and 39 deletions

View File

@ -440,11 +440,15 @@ class OpenMetadataAuthenticationProvider(AuthenticationProvider):
def auth_token(self) -> None:
if not self.jwt_token:
if os.path.isfile(self.security_config.jwtToken):
with open(self.security_config.jwtToken, "r", encoding="utf-8") as file:
if os.path.isfile(self.security_config.jwtToken.get_secret_value()):
with open(
self.security_config.jwtToken.get_secret_value(),
"r",
encoding="utf-8",
) as file:
self.jwt_token = file.read().rstrip()
else:
self.jwt_token = self.security_config.jwtToken
self.jwt_token = self.security_config.jwtToken.get_secret_value()
def get_access_token(self):
self.auth_token()

View File

@ -20,11 +20,13 @@ import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Locale;
import java.util.Set;
import lombok.Getter;
import org.openmetadata.annotations.PasswordField;
import org.openmetadata.schema.entity.services.ServiceType;
import org.openmetadata.schema.entity.services.ingestionPipelines.IngestionPipeline;
import org.openmetadata.schema.entity.teams.AuthenticationMechanism;
import org.openmetadata.schema.security.client.OpenMetadataJWTClientConfig;
import org.openmetadata.schema.security.secrets.SecretsManagerProvider;
import org.openmetadata.service.exception.InvalidServiceConnectionException;
import org.openmetadata.service.exception.SecretsManagerException;
@ -41,6 +43,8 @@ public abstract class SecretsManager {
private Fernet fernet;
private static final Set<Class<?>> DO_NOT_ENCRYPT_CLASSES = Set.of(OpenMetadataJWTClientConfig.class);
protected SecretsManager(SecretsManagerProvider secretsManagerProvider, String clusterPrefix) {
this.secretsManagerProvider = secretsManagerProvider;
this.clusterPrefix = clusterPrefix;
@ -97,30 +101,32 @@ public abstract class SecretsManager {
}
private void encryptPasswordFields(Object toEncryptObject, String secretId) {
// for each get method
Arrays.stream(toEncryptObject.getClass().getMethods())
.filter(this::isGetMethodOfObject)
.forEach(
method -> {
Object obj = getObjectFromMethod(method, toEncryptObject);
String fieldName = method.getName().replaceFirst("get", "");
// if the object matches the package of openmetadata
if (obj != null && obj.getClass().getPackageName().startsWith("org.openmetadata")) {
// encryptPasswordFields
encryptPasswordFields(obj, buildSecretId(false, secretId, fieldName.toLowerCase(Locale.ROOT)));
// check if it has annotation
} else if (obj != null && method.getAnnotation(PasswordField.class) != null) {
// store value if proceed
String newFieldValue = storeValue(fieldName, (String) obj, secretId);
// get setMethod
Method toSet = getToSetMethod(toEncryptObject, obj, fieldName);
// set new value
setValueInMethod(
toEncryptObject,
Fernet.isTokenized(newFieldValue) ? newFieldValue : fernet.encrypt(newFieldValue),
toSet);
}
});
if (!DO_NOT_ENCRYPT_CLASSES.contains(toEncryptObject.getClass())) {
// for each get method
Arrays.stream(toEncryptObject.getClass().getMethods())
.filter(this::isGetMethodOfObject)
.forEach(
method -> {
Object obj = getObjectFromMethod(method, toEncryptObject);
String fieldName = method.getName().replaceFirst("get", "");
// if the object matches the package of openmetadata
if (obj != null && obj.getClass().getPackageName().startsWith("org.openmetadata")) {
// encryptPasswordFields
encryptPasswordFields(obj, buildSecretId(false, secretId, fieldName.toLowerCase(Locale.ROOT)));
// check if it has annotation
} else if (obj != null && method.getAnnotation(PasswordField.class) != null) {
// store value if proceed
String newFieldValue = storeValue(fieldName, (String) obj, secretId);
// get setMethod
Method toSet = getToSetMethod(toEncryptObject, obj, fieldName);
// set new value
setValueInMethod(
toEncryptObject,
Fernet.isTokenized(newFieldValue) ? newFieldValue : fernet.encrypt(newFieldValue),
toSet);
}
});
}
}
private void decryptPasswordFields(Object toDecryptObject) {

View File

@ -8,7 +8,8 @@
"properties": {
"jwtToken": {
"description": "OpenMetadata generated JWT token.",
"type": "string"
"type": "string",
"format": "password"
}
},
"additionalProperties": false,

View File

@ -221,7 +221,7 @@ const Ingestion: React.FC<IngestionProps> = ({
const getAddIngestionButton = (type: PipelineType) => {
return (
<Button
className={classNames('tw-h-8 tw-rounded tw-mb-2')}
className={classNames('h-8 rounded-4 m-b-xs')}
data-testid="add-new-ingestion-button"
size="small"
type="primary"
@ -235,7 +235,7 @@ const Ingestion: React.FC<IngestionProps> = ({
return (
<Fragment>
<Button
className={classNames('tw-h-8 tw-rounded tw-mb-2')}
className={classNames('h-8 rounded-4 m-b-xs flex items-center')}
data-testid="add-new-ingestion-button"
size="small"
type="primary"
@ -245,15 +245,15 @@ const Ingestion: React.FC<IngestionProps> = ({
<DropdownIcon
style={{
transform: 'rotate(180deg)',
marginTop: '2px',
verticalAlign: 'middle',
color: '#fff',
}}
/>
) : (
<DropdownIcon
style={{
marginTop: '2px',
color: '#fff',
verticalAlign: 'middle',
}}
/>
)}

View File

@ -55,6 +55,7 @@ interface Props {
status: LoadingState;
onCancel?: () => void;
onSave: (data: ISubmitEvent<ConfigData>) => void;
disableTestConnection?: boolean;
}
const ConnectionConfigForm: FunctionComponent<Props> = ({
@ -66,6 +67,7 @@ const ConnectionConfigForm: FunctionComponent<Props> = ({
status,
onCancel,
onSave,
disableTestConnection = false,
}: Props) => {
const [isAirflowAvailable, setIsAirflowAvailable] = useState<boolean>(false);
@ -154,6 +156,7 @@ const ConnectionConfigForm: FunctionComponent<Props> = ({
return (
<FormBuilder
cancelText={cancelText}
disableTestConnection={disableTestConnection}
formData={validConfig}
isAirflowAvailable={isAirflowAvailable}
okText={okText}

View File

@ -33,6 +33,7 @@ interface ServiceConfigProps {
data: ConfigData,
serviceCategory: ServiceCategory
) => Promise<void>;
disableTestConnection: boolean;
}
export const Field = ({ children }: { children: React.ReactNode }) => {
@ -45,6 +46,7 @@ const ServiceConfig = ({
serviceType,
data,
handleUpdate,
disableTestConnection,
}: ServiceConfigProps) => {
const history = useHistory();
const [status, setStatus] = useState<LoadingState>('initial');
@ -80,6 +82,7 @@ const ServiceConfig = ({
| DashboardService
| PipelineService
}
disableTestConnection={disableTestConnection}
serviceCategory={serviceCategory}
serviceType={serviceType}
status={status}

View File

@ -17,7 +17,7 @@ import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { isEmpty, isNull, isObject } from 'lodash';
import React, { ReactNode, useEffect, useState } from 'react';
import { DEF_UI_SCHEMA } from '../../constants/services.const';
import { DEF_UI_SCHEMA, JWT_CONFIG } from '../../constants/services.const';
import { EntityType } from '../../enums/entity.enum';
import { DashboardServiceType } from '../../generated/entity/services/dashboardService';
import { DatabaseServiceType } from '../../generated/entity/services/databaseService';
@ -135,6 +135,15 @@ const ServiceConnectionDetails = ({
}
}
}
} else if (
serviceCategory.slice(0, -1) === EntityType.METADATA_SERVICE &&
key === 'securityConfig'
) {
const newSchemaPropertyObject = schemaPropertyObject[
key
].oneOf.filter((item) => item.title === JWT_CONFIG)[0].properties;
return getKeyValues(value, newSchemaPropertyObject);
} else {
return getKeyValues(
value,

View File

@ -37,6 +37,7 @@ interface Props extends FormProps<ConfigData> {
status?: LoadingState;
onCancel?: () => void;
onTestConnection?: (formData: ConfigData) => Promise<void>;
disableTestConnection: boolean;
}
const FormBuilder: FunctionComponent<Props> = ({
@ -51,6 +52,7 @@ const FormBuilder: FunctionComponent<Props> = ({
onTestConnection,
uiSchema,
isAirflowAvailable,
disableTestConnection,
...props
}: Props) => {
const formRef = useRef<CoreForm<ConfigData>>();
@ -188,7 +190,7 @@ const FormBuilder: FunctionComponent<Props> = ({
'tw-opacity-40': connectionTesting,
})}
data-testid="test-connection-btn"
disabled={connectionTesting}
disabled={connectionTesting || disableTestConnection}
size="small"
theme="primary"
variant="outlined"

View File

@ -221,4 +221,5 @@ export const COMMON_UI_SCHEMA = {
},
};
export const OPENMETADATA = 'Openmetadata';
export const OPENMETADATA = 'OpenMetadata';
export const JWT_CONFIG = 'openMetadataJWTClientConfig';

View File

@ -26,6 +26,7 @@ import Loader from '../../components/Loader/Loader';
import ServiceConfig from '../../components/ServiceConfig/ServiceConfig';
import { GlobalSettingsMenuCategory } from '../../constants/globalSettings.constants';
import { addServiceGuide } from '../../constants/service-guide.constant';
import { OPENMETADATA } from '../../constants/services.const';
import { PageLayoutType } from '../../enums/layout.enum';
import { ServiceCategory } from '../../enums/service.enum';
import { ConfigData, ServicesType } from '../../interface/service.interface';
@ -159,6 +160,10 @@ function EditConnectionFormPage() {
</h6>
<ServiceConfig
data={serviceDetails as ServicesData}
disableTestConnection={
ServiceCategory.METADATA_SERVICES === serviceCategory &&
OPENMETADATA === serviceFQN
}
handleUpdate={handleConfigUpdate}
serviceCategory={serviceCategory as ServiceCategory}
serviceFQN={serviceFQN}

View File

@ -79,6 +79,16 @@
.text-underline {
text-decoration: underline;
}
// Radius
.rounded-4 {
border-radius: 4px;
}
.rounded-full {
border-radius: 9999px;
}
// Width
.w-4 {
width: 16px;
@ -249,10 +259,6 @@
border-color: @gray;
}
.rounded-full {
border-radius: 9999px;
}
.bg-primary-lite {
background: @primary-light;
}