mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-12 09:18:20 +00:00
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:
parent
b2c1e42f9b
commit
36f27e947d
@ -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()
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
@ -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',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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,
|
||||||
|
@ -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"
|
||||||
|
@ -221,4 +221,5 @@ export const COMMON_UI_SCHEMA = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OPENMETADATA = 'Openmetadata';
|
export const OPENMETADATA = 'OpenMetadata';
|
||||||
|
export const JWT_CONFIG = 'openMetadataJWTClientConfig';
|
||||||
|
@ -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}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user