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: def auth_token(self) -> None:
if not self.jwt_token: if not self.jwt_token:
if os.path.isfile(self.security_config.jwtToken): if os.path.isfile(self.security_config.jwtToken.get_secret_value()):
with open(self.security_config.jwtToken, "r", encoding="utf-8") as file: with open(
self.security_config.jwtToken.get_secret_value(),
"r",
encoding="utf-8",
) as file:
self.jwt_token = file.read().rstrip() self.jwt_token = file.read().rstrip()
else: else:
self.jwt_token = self.security_config.jwtToken self.jwt_token = self.security_config.jwtToken.get_secret_value()
def get_access_token(self): def get_access_token(self):
self.auth_token() self.auth_token()

View File

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

View File

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

View File

@ -221,7 +221,7 @@ const Ingestion: React.FC<IngestionProps> = ({
const getAddIngestionButton = (type: PipelineType) => { const getAddIngestionButton = (type: PipelineType) => {
return ( return (
<Button <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" data-testid="add-new-ingestion-button"
size="small" size="small"
type="primary" type="primary"
@ -235,7 +235,7 @@ const Ingestion: React.FC<IngestionProps> = ({
return ( return (
<Fragment> <Fragment>
<Button <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" data-testid="add-new-ingestion-button"
size="small" size="small"
type="primary" type="primary"
@ -245,15 +245,15 @@ const Ingestion: React.FC<IngestionProps> = ({
<DropdownIcon <DropdownIcon
style={{ style={{
transform: 'rotate(180deg)', transform: 'rotate(180deg)',
marginTop: '2px', verticalAlign: 'middle',
color: '#fff', color: '#fff',
}} }}
/> />
) : ( ) : (
<DropdownIcon <DropdownIcon
style={{ style={{
marginTop: '2px',
color: '#fff', color: '#fff',
verticalAlign: 'middle',
}} }}
/> />
)} )}

View File

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

View File

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

View File

@ -17,7 +17,7 @@ import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { isEmpty, isNull, isObject } from 'lodash'; import { isEmpty, isNull, isObject } from 'lodash';
import React, { ReactNode, useEffect, useState } from 'react'; 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 { EntityType } from '../../enums/entity.enum';
import { DashboardServiceType } from '../../generated/entity/services/dashboardService'; import { DashboardServiceType } from '../../generated/entity/services/dashboardService';
import { DatabaseServiceType } from '../../generated/entity/services/databaseService'; 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 { } else {
return getKeyValues( return getKeyValues(
value, value,

View File

@ -37,6 +37,7 @@ interface Props extends FormProps<ConfigData> {
status?: LoadingState; status?: LoadingState;
onCancel?: () => void; onCancel?: () => void;
onTestConnection?: (formData: ConfigData) => Promise<void>; onTestConnection?: (formData: ConfigData) => Promise<void>;
disableTestConnection: boolean;
} }
const FormBuilder: FunctionComponent<Props> = ({ const FormBuilder: FunctionComponent<Props> = ({
@ -51,6 +52,7 @@ const FormBuilder: FunctionComponent<Props> = ({
onTestConnection, onTestConnection,
uiSchema, uiSchema,
isAirflowAvailable, isAirflowAvailable,
disableTestConnection,
...props ...props
}: Props) => { }: Props) => {
const formRef = useRef<CoreForm<ConfigData>>(); const formRef = useRef<CoreForm<ConfigData>>();
@ -188,7 +190,7 @@ const FormBuilder: FunctionComponent<Props> = ({
'tw-opacity-40': connectionTesting, 'tw-opacity-40': connectionTesting,
})} })}
data-testid="test-connection-btn" data-testid="test-connection-btn"
disabled={connectionTesting} disabled={connectionTesting || disableTestConnection}
size="small" size="small"
theme="primary" theme="primary"
variant="outlined" 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 ServiceConfig from '../../components/ServiceConfig/ServiceConfig';
import { GlobalSettingsMenuCategory } from '../../constants/globalSettings.constants'; import { GlobalSettingsMenuCategory } from '../../constants/globalSettings.constants';
import { addServiceGuide } from '../../constants/service-guide.constant'; import { addServiceGuide } from '../../constants/service-guide.constant';
import { OPENMETADATA } from '../../constants/services.const';
import { PageLayoutType } from '../../enums/layout.enum'; import { PageLayoutType } from '../../enums/layout.enum';
import { ServiceCategory } from '../../enums/service.enum'; import { ServiceCategory } from '../../enums/service.enum';
import { ConfigData, ServicesType } from '../../interface/service.interface'; import { ConfigData, ServicesType } from '../../interface/service.interface';
@ -159,6 +160,10 @@ function EditConnectionFormPage() {
</h6> </h6>
<ServiceConfig <ServiceConfig
data={serviceDetails as ServicesData} data={serviceDetails as ServicesData}
disableTestConnection={
ServiceCategory.METADATA_SERVICES === serviceCategory &&
OPENMETADATA === serviceFQN
}
handleUpdate={handleConfigUpdate} handleUpdate={handleConfigUpdate}
serviceCategory={serviceCategory as ServiceCategory} serviceCategory={serviceCategory as ServiceCategory}
serviceFQN={serviceFQN} serviceFQN={serviceFQN}

View File

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