- Config Changes for Logo and Jwt Expiry (#10287)

* - Changes for Logo
- Make Jwt Token Expiry Configurable

* change config time

* fix tests

* review comments

* fix failing python

* moved login to application config as well

* fix login Config

* Added Nav Bar and Login Page Config for Logos

* Removed UI Api calls

* fix: unit test

---------

Co-authored-by: Sachin Chaurasiya <sachinchaurasiyachotey87@gmail.com>
This commit is contained in:
Mohit Yadav 2023-02-23 22:57:57 +05:30 committed by GitHub
parent 8191f92438
commit 960ae2918e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 134 additions and 209 deletions

View File

@ -265,10 +265,15 @@ email:
password: ${SMTP_SERVER_PWD:-""}
transportationStrategy: ${SMTP_SERVER_STRATEGY:-"SMTP_TLS"}
sandboxModeEnabled: ${SANDBOX_MODE_ENABLED:-false}
slackChat:
slackUrl: ${SLACK_CHAT_SLACK_URL:-"https://slack.open-metadata.org/"}
applicationConfig:
logoConfig:
logoLocationType: ${OM_LOGO_LOCATION_TYPE:-openmetadata} #either "openmetadata' or { "url" or "filePath" , based on this specify either '*AbsoluteFilePath' or '*LogoUrlPath' }
loginPageLogoAbsoluteFilePath: ${OM_LOGO_LOGIN_LOCATION_FILE_PATH:-""} #login page logo , work in "filePath" mode
loginPageLogoUrlPath: ${OM_LOGO_LOGIN_LOCATION_URL_PATH:-""} #login page logo , work in "url" mode
navBarLogoAbsoluteFilePath: ${OM_LOGO_NAVBAR_LOCATION_FILE_PATH:-""} #nav bar logo , work in "filePath" mode
navBarLogoUrlPath: ${OM_LOGO_NAVBAR_LOCATION_URL_PATH:-""} #nav bar logo , work in "url" mode
loginConfig:
maxLoginFailAttempts: ${OM_MAX_FAILED_LOGIN_ATTEMPTS:-3}
accessBlockTime: ${OM_LOGIN_ACCESS_BLOCKTIME:-600}
jwtTokenExpiryTime: ${OM_JWT_EXPIRY_TIME:-3600}
login:
maxLoginFailAttempts: ${OM_MAX_FAILED_LOGIN_ATTEMPTS:-3}
accessBlockTime: ${OM_LOGIN_ACCESS_BLOCKTIME:-600}

View File

@ -22,14 +22,13 @@ import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import org.openmetadata.schema.api.configuration.LoginConfiguration;
import org.openmetadata.api.configuration.ApplicationConfiguration;
import org.openmetadata.schema.api.configuration.events.EventHandlerConfiguration;
import org.openmetadata.schema.api.configuration.pipelineServiceClient.PipelineServiceClientConfiguration;
import org.openmetadata.schema.api.fernet.FernetConfiguration;
import org.openmetadata.schema.api.security.AuthenticationConfiguration;
import org.openmetadata.schema.api.security.AuthorizerConfiguration;
import org.openmetadata.schema.api.security.jwt.JWTTokenConfiguration;
import org.openmetadata.schema.api.slackChat.SlackChatConfiguration;
import org.openmetadata.schema.email.SmtpSettings;
import org.openmetadata.schema.service.configuration.elasticsearch.ElasticSearchConfiguration;
import org.openmetadata.service.migration.MigrationConfiguration;
@ -77,11 +76,8 @@ public class OpenMetadataApplicationConfig extends Configuration {
@Valid
private HealthConfiguration healthConfiguration = new HealthConfiguration();
@JsonProperty("sandboxModeEnabled")
private boolean sandboxModeEnabled;
@JsonProperty("slackChat")
private SlackChatConfiguration slackChatConfiguration = new SlackChatConfiguration();
@JsonProperty("applicationConfig")
private ApplicationConfiguration applicationConfiguration = new ApplicationConfiguration();
@JsonProperty("secretsManagerConfiguration")
private SecretsManagerConfiguration secretsManagerConfiguration;
@ -95,9 +91,6 @@ public class OpenMetadataApplicationConfig extends Configuration {
@JsonProperty("email")
private SmtpSettings smtpSettings;
@JsonProperty("login")
private LoginConfiguration loginSettings;
@Override
public String toString() {
return "catalogConfig{"

View File

@ -22,13 +22,12 @@ import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.openmetadata.api.configuration.ApplicationConfiguration;
import org.openmetadata.schema.api.security.AuthenticationConfiguration;
import org.openmetadata.schema.api.security.AuthorizerConfiguration;
import org.openmetadata.schema.api.slackChat.SlackChatConfiguration;
import org.openmetadata.service.OpenMetadataApplicationConfig;
import org.openmetadata.service.clients.pipeline.PipelineServiceAPIClientConfig;
import org.openmetadata.service.resources.Collection;
import org.openmetadata.service.sandbox.SandboxConfiguration;
import org.openmetadata.service.security.jwt.JWKSResponse;
import org.openmetadata.service.security.jwt.JWTTokenGenerator;
@ -95,47 +94,22 @@ public class ConfigResource {
}
@GET
@Path(("/sandbox"))
@Path(("/applicationConfig"))
@Operation(
operationId = "getSandboxConfiguration",
summary = "Get sandbox mode",
operationId = "getApplicationConfiguration",
summary = "Get application configuration",
tags = "system",
responses = {
@ApiResponse(
responseCode = "200",
description = "Sandbox mode",
content =
@Content(mediaType = "application/json", schema = @Schema(implementation = SandboxConfiguration.class)))
})
public SandboxConfiguration getSandboxMode() {
SandboxConfiguration sandboxConfiguration = new SandboxConfiguration();
if (openMetadataApplicationConfig.isSandboxModeEnabled()) {
sandboxConfiguration.setSandboxModeEnabled(true);
}
return sandboxConfiguration;
}
@GET
@Path(("/slackChat"))
@Operation(
operationId = "getSlackChatConfiguration",
summary = "Get slack chat configuration",
tags = "system",
responses = {
@ApiResponse(
responseCode = "200",
description = "Get slack chat configuration",
description = "Get application configuration",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = SlackChatConfiguration.class)))
schema = @Schema(implementation = ApplicationConfiguration.class)))
})
public SlackChatConfiguration getSlackChatConfiguration() {
SlackChatConfiguration slackChatConfiguration = new SlackChatConfiguration();
if (openMetadataApplicationConfig.getSlackChatConfiguration() != null) {
slackChatConfiguration = openMetadataApplicationConfig.getSlackChatConfiguration();
}
return slackChatConfiguration;
public ApplicationConfiguration getApplicationConfiguration() {
return openMetadataApplicationConfig.getApplicationConfiguration();
}
@GET

View File

@ -12,7 +12,6 @@ import org.jdbi.v3.core.Jdbi;
import org.openmetadata.schema.api.teams.CreateUser;
import org.openmetadata.schema.auth.ChangePasswordRequest;
import org.openmetadata.schema.auth.JWTAuthMechanism;
import org.openmetadata.schema.auth.JWTTokenExpiry;
import org.openmetadata.schema.auth.LoginRequest;
import org.openmetadata.schema.auth.PasswordResetRequest;
import org.openmetadata.schema.auth.RefreshToken;
@ -80,11 +79,11 @@ public interface AuthenticatorHandler {
throw new CustomExceptionMessage(Response.Status.NOT_IMPLEMENTED, NOT_IMPLEMENTED_METHOD);
}
default JwtResponse getJwtResponse(User storedUser) throws JsonProcessingException {
default JwtResponse getJwtResponse(User storedUser, long expireInSeconds) throws JsonProcessingException {
RefreshToken refreshToken = createRefreshTokenForLogin(storedUser.getId());
JWTAuthMechanism jwtAuthMechanism =
JWTTokenGenerator.getInstance()
.generateJWTToken(storedUser.getName(), storedUser.getEmail(), JWTTokenExpiry.OneHour, false);
.generateJWTToken(storedUser.getName(), storedUser.getEmail(), expireInSeconds, false);
JwtResponse response = new JwtResponse();
response.setTokenType("Bearer");

View File

@ -40,7 +40,6 @@ import org.openmetadata.schema.auth.BasicAuthMechanism;
import org.openmetadata.schema.auth.ChangePasswordRequest;
import org.openmetadata.schema.auth.EmailVerificationToken;
import org.openmetadata.schema.auth.JWTAuthMechanism;
import org.openmetadata.schema.auth.JWTTokenExpiry;
import org.openmetadata.schema.auth.LoginRequest;
import org.openmetadata.schema.auth.PasswordResetRequest;
import org.openmetadata.schema.auth.PasswordResetToken;
@ -87,7 +86,7 @@ public class BasicAuthenticator implements AuthenticatorHandler {
SmtpSettings smtpSettings = config.getSmtpSettings();
this.isEmailServiceEnabled = smtpSettings != null && smtpSettings.getEnableSmtpServer();
this.isSelfSignUpAvailable = config.getAuthenticationConfiguration().getEnableSelfSignup();
this.loginConfiguration = config.getLoginSettings();
this.loginConfiguration = config.getApplicationConfiguration().getLoginConfig();
}
@Override
@ -336,7 +335,8 @@ public class BasicAuthenticator implements AuthenticatorHandler {
RefreshToken refreshToken = validateAndReturnNewRefresh(storedUser.getId(), request);
JWTAuthMechanism jwtAuthMechanism =
JWTTokenGenerator.getInstance()
.generateJWTToken(storedUser.getName(), storedUser.getEmail(), JWTTokenExpiry.OneHour, false);
.generateJWTToken(
storedUser.getName(), storedUser.getEmail(), loginConfiguration.getJwtTokenExpiryTime(), false);
JwtResponse response = new JwtResponse();
response.setTokenType("Bearer");
response.setAccessToken(jwtAuthMechanism.getJWTToken());
@ -413,7 +413,7 @@ public class BasicAuthenticator implements AuthenticatorHandler {
checkIfLoginBlocked(userName);
User storedUser = lookUserInProvider(userName);
validatePassword(storedUser, loginRequest.getPassword());
return getJwtResponse(storedUser);
return getJwtResponse(storedUser, loginConfiguration.getJwtTokenExpiryTime());
}
@Override

View File

@ -69,7 +69,7 @@ public class LdapAuthenticator implements AuthenticatorHandler {
this.tokenRepository = new TokenRepository(jdbi.onDemand(CollectionDAO.class));
this.ldapConfiguration = config.getAuthenticationConfiguration().getLdapConfiguration();
this.loginAttemptCache = new LoginAttemptCache(config);
this.loginConfiguration = config.getLoginSettings();
this.loginConfiguration = config.getApplicationConfiguration().getLoginConfig();
}
private LDAPConnectionPool getLdapConnectionPool(LdapConfiguration ldapConfiguration) {
@ -115,7 +115,7 @@ public class LdapAuthenticator implements AuthenticatorHandler {
User storedUser = lookUserInProvider(loginRequest.getEmail());
validatePassword(storedUser, loginRequest.getPassword());
User omUser = checkAndCreateUser(loginRequest.getEmail());
return getJwtResponse(omUser);
return getJwtResponse(omUser, loginConfiguration.getJwtTokenExpiryTime());
}
private User checkAndCreateUser(String email) throws IOException {

View File

@ -15,7 +15,7 @@ public class LoginAttemptCache {
public LoginAttemptCache(OpenMetadataApplicationConfig config) {
super();
LoginConfiguration loginConfiguration = config.getLoginSettings();
LoginConfiguration loginConfiguration = config.getApplicationConfiguration().getLoginConfig();
long accessBlockTime = 600;
if (loginConfiguration != null) {
MAX_ATTEMPT = loginConfiguration.getMaxLoginFailAttempts();

View File

@ -88,11 +88,11 @@ public class JWTTokenGenerator {
}
}
public JWTAuthMechanism generateJWTToken(String userName, String email, JWTTokenExpiry expiry, boolean isBot) {
public JWTAuthMechanism generateJWTToken(String userName, String email, long expiryInSeconds, boolean isBot) {
try {
JWTAuthMechanism jwtAuthMechanism = new JWTAuthMechanism().withJWTTokenExpiry(expiry);
JWTAuthMechanism jwtAuthMechanism = new JWTAuthMechanism();
Algorithm algorithm = Algorithm.RSA256(null, privateKey);
Date expires = getExpiryDate(expiry);
Date expires = getCustomExpiryDate(expiryInSeconds);
String token =
JWT.create()
.withIssuer(issuer)
@ -139,6 +139,11 @@ public class JWTTokenGenerator {
return expiryDate != null ? Date.from(expiryDate.atZone(ZoneId.systemDefault()).toInstant()) : null;
}
public Date getCustomExpiryDate(long seconds) {
LocalDateTime expiryDate = LocalDateTime.now().plusSeconds(seconds);
return Date.from(expiryDate.atZone(ZoneId.systemDefault()).toInstant());
}
public JWKSResponse getJWKSResponse() {
JWKSResponse jwksResponse = new JWKSResponse();
JWKSKey jwksKey = new JWKSKey();

View File

@ -29,9 +29,9 @@ import javax.ws.rs.client.WebTarget;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.openmetadata.api.configuration.ApplicationConfiguration;
import org.openmetadata.schema.api.security.AuthenticationConfiguration;
import org.openmetadata.schema.api.security.AuthorizerConfiguration;
import org.openmetadata.schema.api.slackChat.SlackChatConfiguration;
import org.openmetadata.service.OpenMetadataApplicationConfig;
import org.openmetadata.service.OpenMetadataApplicationTest;
import org.openmetadata.service.clients.pipeline.PipelineServiceAPIClientConfig;
@ -88,11 +88,12 @@ class ConfigResourceTest extends OpenMetadataApplicationTest {
}
@Test
void get_slack_chat_configs_200_OK() throws IOException {
WebTarget target = getConfigResource("slackChat");
SlackChatConfiguration slackChatConfiguration =
TestUtils.get(target, SlackChatConfiguration.class, TEST_AUTH_HEADERS);
assertEquals(config.getSlackChatConfiguration().getSlackUrl(), slackChatConfiguration.getSlackUrl());
void get_application_configs_200_OK() throws IOException {
WebTarget target = getConfigResource("applicationConfig");
ApplicationConfiguration applicationConfiguration =
TestUtils.get(target, ApplicationConfiguration.class, TEST_AUTH_HEADERS);
assertEquals(config.getApplicationConfiguration().getLogoConfig(), applicationConfiguration.getLogoConfig());
assertEquals(config.getApplicationConfiguration().getLoginConfig(), applicationConfiguration.getLoginConfig());
}
@Test

View File

@ -191,9 +191,15 @@ email:
password: ""
transportationStrategy: "SMTP_TLS"
slackChat:
slackUrl: "http://localhost:8080"
login:
maxLoginFailAttempts: 3
accessBlockTime: 600
applicationConfig:
logoConfig:
logoLocationType: "openmetadata"
loginPageLogoAbsoluteFilePath: ""
loginPageLogoUrlPath: ""
navBarLogoAbsoluteFilePath: ""
navBarLogoUrlPath: ""
loginConfig:
maxLoginFailAttempts: 3
accessBlockTime: 600
jwtTokenExpiryTime: 3600

View File

@ -0,0 +1,73 @@
{
"$id": "https://open-metadata.org/schema/entity/configuration/applicationConfiguration.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ApplicationConfiguration",
"description": "This schema defines the Application Configuration.",
"type": "object",
"javaType": "org.openmetadata.api.configuration.ApplicationConfiguration",
"definitions": {
"logoConfiguration": {
"description": "This schema defines the Logo Configuration",
"type": "object",
"javaType": "org.openmetadata.api.configuration.LogoConfiguration",
"properties": {
"logoLocationType": {
"description": "Type of Logo Location",
"type": "string",
"enum": ["openmetadata","filePath","url"]
},
"loginPageLogoAbsoluteFilePath": {
"description": "Login Page Absolute File Path For Logo Image",
"type": "string"
},
"loginPageLogoUrlPath": {
"description": "Login Page Logo Image Url",
"type": "string"
},
"navBarLogoAbsoluteFilePath": {
"description": "Navigation Bar Absolute File Path For Logo Image",
"type": "string"
},
"navBarLogoUrlPath": {
"description": "Navigation Bar Logo Image Url",
"type": "string"
}
},
"required": ["logoLocationType"],
"additionalProperties": false
},
"loginConfiguration": {
"description": "This schema defines the Login Configuration",
"type": "object",
"javaType": "org.openmetadata.schema.api.configuration.LoginConfiguration",
"properties": {
"maxLoginFailAttempts": {
"description": "Failed Login Attempts allowed for user.",
"type": "integer",
"default": 3
},
"accessBlockTime": {
"description": "Access Block time for user on exceeding failed attempts(in seconds)",
"type": "integer",
"default": 600
},
"jwtTokenExpiryTime": {
"description": "Jwt Token Expiry time for login in seconds",
"type": "integer",
"default": 3600
}
},
"additionalProperties": false
}
},
"properties": {
"logoConfig" : {
"$ref": "#/definitions/logoConfiguration"
},
"loginConfig": {
"$ref": "#/definitions/loginConfiguration"
}
},
"additionalProperties": false
}

View File

@ -1,21 +0,0 @@
{
"$id": "https://open-metadata.org/schema/entity/configuration/loginConfiguration.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "LoginConfiguration",
"description": "This schema defines the Login Configuration",
"type": "object",
"javaType": "org.openmetadata.schema.api.configuration.LoginConfiguration",
"properties": {
"maxLoginFailAttempts": {
"description": "Failed Login Attempts allowed for user.",
"type": "integer",
"default": 3
},
"accessBlockTime": {
"description": "Access Block time for user on exceeding failed attempts(in seconds)",
"type": "integer",
"default": 600
}
},
"additionalProperties": false
}

View File

@ -1,16 +0,0 @@
{
"$id": "https://open-metadata.org/schema/entity/configuration/slackChatConfiguration.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SlackChatConfiguration",
"description": "This schema defines the Slack Chat Configuration.",
"type": "object",
"javaType": "org.openmetadata.schema.api.slackChat.SlackChatConfiguration",
"properties": {
"slackUrl": {
"description": "The URL of the slack channel to redirect the users to on click of the chat icon",
"type": "string"
}
},
"required": ["slackUrl"],
"additionalProperties": false
}

View File

@ -1,36 +0,0 @@
{
"$id": "https://open-metadata.org/schema/entity/configuration/slackEventPubConfiguration.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SlackPublisherConfiguration",
"description": "This schema defines the Authentication Configuration.",
"type": "object",
"javaType": "org.openmetadata.schema.api.slack.SlackPublisherConfiguration",
"properties": {
"name": {
"description": "Publisher Name",
"type": "string"
},
"webhookUrl": {
"description": "Webhook URL",
"type": "string"
},
"openMetadataUrl": {
"description": "OpenMetadata URL",
"type": "string"
},
"filters": {
"description": "Filters",
"type": "array",
"items": {
"$ref": "../type/changeEvent.json#/definitions/eventFilter"
}
},
"batchSize": {
"description": "Batch Size",
"type": "integer",
"default": 10
}
},
"required": ["name", "filters"],
"additionalProperties": false
}

View File

@ -55,12 +55,6 @@
{
"$ref": "../configuration/jwtTokenConfiguration.json"
},
{
"$ref": "../configuration/slackChatConfiguration.json"
},
{
"$ref": "../configuration/slackEventPubConfiguration.json"
},
{
"$ref": "../configuration/taskNotificationConfiguration.json"
}

View File

@ -13,13 +13,12 @@
import { AxiosError } from 'axios';
import PageContainerV1 from 'components/containers/PageContainerV1';
import GithubStarButton from 'components/GithubStarButton/GithubStarButton';
import Loader from 'components/Loader/Loader';
import MyData from 'components/MyData/MyData.component';
import { MyDataState } from 'components/MyData/MyData.interface';
import { useWebSocketConnector } from 'components/web-scoket/web-scoket.provider';
import { Operation } from 'fast-json-patch';
import { isEmpty, isNil, isUndefined } from 'lodash';
import { isEmpty, isNil } from 'lodash';
import { observer } from 'mobx-react';
import React, {
Fragment,
@ -32,7 +31,7 @@ import React, {
} from 'react';
import { useLocation } from 'react-router-dom';
import { getFeedsWithFilter, postFeedById } from 'rest/feedsAPI';
import { fetchSandboxConfig, getAllEntityCount } from 'rest/miscAPI';
import { getAllEntityCount } from 'rest/miscAPI';
import { getUserById } from 'rest/userAPI';
import AppState from '../../AppState';
import { SOCKET_EVENTS } from '../../constants/constants';
@ -86,7 +85,6 @@ const MyDataPage = () => {
const [entityThread, setEntityThread] = useState<Thread[]>([]);
const [isFeedLoading, setIsFeedLoading] = useState<boolean>(false);
const [isLoadingOwnedData, setIsLoadingOwnedData] = useState<boolean>(false);
const [isSandbox, setIsSandbox] = useState<boolean>(false);
const [activityFeeds, setActivityFeeds] = useState<Thread[]>([]);
@ -246,24 +244,6 @@ const MyDataPage = () => {
updateThreadData(threadId, postId, isThread, data, setEntityThread);
};
const fetchSandboxMode = () => {
fetchSandboxConfig()
.then((res) => {
if (!isUndefined(res.sandboxModeEnabled)) {
setIsSandbox(Boolean(res.sandboxModeEnabled));
} else {
throw '';
}
})
.catch((err: AxiosError) => {
showErrorToast(
err,
jsonData['api-error-messages']['unexpected-server-response']
);
setIsSandbox(false);
});
};
// Fetch tasks list to show count for Pending tasks
const fetchMyTaskData = useCallback(() => {
if (!currentUser || !currentUser.id) {
@ -281,7 +261,6 @@ const MyDataPage = () => {
}, [currentUser]);
useEffect(() => {
fetchSandboxMode();
fetchData();
fetchMyTaskData();
}, []);
@ -354,7 +333,6 @@ const MyDataPage = () => {
updateThreadHandler={updateThreadHandler}
onRefreshFeeds={onRefreshFeeds}
/>
{isSandbox ? <GithubStarButton /> : null}
</Fragment>
) : (
<Loader />

View File

@ -13,17 +13,13 @@
import { findByText, queryByText, render } from '@testing-library/react';
import React, { ReactNode } from 'react';
import { fetchSandboxConfig, getAllEntityCount } from 'rest/miscAPI';
import { getAllEntityCount } from 'rest/miscAPI';
import MyDataPageComponent from './MyDataPage.component';
const mockAuth = {
isAuthDisabled: true,
};
const mockErrors = {
sandboxMode: 'SandboxModeError',
};
jest.mock('react', () => {
const originalReact = jest.requireActual('react');
@ -127,32 +123,14 @@ describe('Test MyData page component', () => {
});
it('Component should render in sandbox mode', async () => {
(fetchSandboxConfig as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({
sandboxModeEnabled: true,
})
);
const { container } = render(<MyDataPageComponent />);
const myData = await findByText(container, /MyData.component/i);
const githubStarButton = await findByText(
container,
/GithubStarButton.component/i
);
expect(myData).toBeInTheDocument();
expect(githubStarButton).toBeInTheDocument();
});
describe('render Sad Paths', () => {
it('show error message on failing of config/sandbox api', async () => {
(fetchSandboxConfig as jest.Mock).mockImplementationOnce(() =>
Promise.reject({
response: { data: { message: mockErrors.sandboxMode } },
})
);
const { container } = render(<MyDataPageComponent />);
const myData = await findByText(container, /MyData.component/i);
@ -166,10 +144,6 @@ describe('Test MyData page component', () => {
});
it('show error message on no data from config/sandbox api', async () => {
(fetchSandboxConfig as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({})
);
const { container } = render(<MyDataPageComponent />);
const myData = await findByText(container, /MyData.component/i);

View File

@ -88,10 +88,6 @@ export const fetchSandboxConfig = async () => {
return response.data;
};
export const fetchSlackConfig = (): Promise<AxiosResponse> => {
return APIClient.get('/system/config/slackChat');
};
export const fetchAirflowConfig = async () => {
const response = await APIClient.get<PipelineServiceClientConfiguration>(
'/system/config/pipeline-service-client'