+ )
+ }
+ const getText = (isNarrow) => {
+ return (
+ !isNarrow && (
+ <>
+ AWS
-
- SaaS Boost
- >
- )
+
+ SaaS Boost
+ >
+ )
+ )
+ }
+ return (
+
+
+
+ {getText(sidebarNarrow)}
+
+
+
+
+
+
+
+ {
+ setSidebarNarrow(!sidebarNarrow)
+ }}
+ />
+
)
- }
- return (
-
-
-
- {getText(sidebarNarrow)}
-
-
-
-
-
-
- {
- setSidebarNarrow(!sidebarNarrow)
- }}
- />
-
- )
}
AppSidebar.propTypes = {
- navigation: PropTypes.array,
+ navigation: PropTypes.array,
}
-export default React.memo(AppSidebar)
+export default React.memo(AppSidebar)
\ No newline at end of file
diff --git a/client/web/src/dashboard/DashboardComponent.js b/client/web/src/dashboard/DashboardComponent.js
index 0c2d8a2e..18bd632f 100644
--- a/client/web/src/dashboard/DashboardComponent.js
+++ b/client/web/src/dashboard/DashboardComponent.js
@@ -87,7 +87,7 @@ export const DashboardComponent = (props) => {
)
const saasBoostEnvironment = useSelector(
- (state) => selectSettingsById(state, 'SAAS_BOOST_ENVIRONMENT')?.value
+ (state) => selectSettingsById(state, 'ENVIRONMENT')?.value
)
const countActiveTenants = useSelector((state) => {
diff --git a/client/web/src/identity/ProviderCreateContainer.js b/client/web/src/identity/ProviderCreateContainer.js
new file mode 100644
index 00000000..d023491a
--- /dev/null
+++ b/client/web/src/identity/ProviderCreateContainer.js
@@ -0,0 +1,109 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {PropTypes} from 'prop-types'
+import React, {Component} from 'react'
+import ProviderForm from './ProviderFormComponent'
+import {connect} from 'react-redux'
+import {withRouter} from 'react-router'
+import identityAPI from './api'
+import {dismissError, createProviderThunk} from './ducks'
+
+const mapDispatchToProps = {
+ createProviderThunk,
+ dismissError,
+}
+
+const mapStateToProps = (state, props) => {
+ const {providerId} = props.match.params;
+ const {providers} = state;
+ const provider = !!providerId ? providers.entities[providerId] : undefined;
+ //console.log('mapStateToProps: ', provider);
+
+ return {
+ providers: providers,
+ provider: provider
+ }
+}
+
+class ProviderCreateContainer extends Component {
+ constructor(props) {
+ super(props)
+ this.state = {}
+
+ this.saveProvider = this.saveProvider.bind(this)
+ this.handleError = this.handleError.bind(this)
+ }
+
+ async saveProvider(values, {setSubmitting, resetForm}) {
+ const provider = this.props.provider;
+ const saveProvider = {
+ type: provider.type,
+ metadata: values.metadata
+ }
+ console.log('saveProvider: ', saveProvider);
+ try {
+ //const createdResponse = await createProviderThunk(saveProvider);
+ const createdResponse = await identityAPI.create(saveProvider);
+ if (!createdResponse.error) {
+ const {history} = this.props
+ history.goBack()
+ //history.push(`/providers/${createdResponse.payload.id}`)
+ } else {
+ setSubmitting(false)
+ resetForm({values})
+ }
+ } catch (e) {
+ setSubmitting(false)
+ resetForm({values})
+ }
+ }
+
+ handleCancel = () => {
+ const {history} = this.props
+ history.goBack()
+ }
+
+ handleError = () => {
+ const {dismissError} = this.props
+ dismissError()
+ }
+
+ render() {
+ const {error} = this.props.providers
+ return (
+
+ )
+ }
+}
+
+ProviderCreateContainer.propTypes = {
+ createProviderThunk: PropTypes.func,
+ history: PropTypes.object,
+ providers: PropTypes.object,
+ dismissError: PropTypes.func,
+}
+
+export const ProviderCreateContainerWithRouter = connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(withRouter(ProviderCreateContainer))
+
+export default ProviderCreateContainerWithRouter
diff --git a/client/web/src/identity/ProviderFormComponent.js b/client/web/src/identity/ProviderFormComponent.js
new file mode 100644
index 00000000..1748f844
--- /dev/null
+++ b/client/web/src/identity/ProviderFormComponent.js
@@ -0,0 +1,132 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {PropTypes} from 'prop-types'
+import React from 'react'
+import {Formik, Form} from 'formik'
+import * as Yup from 'yup'
+import {Row, Col, Card, Button, Alert} from 'react-bootstrap'
+import {SaasBoostInput} from '../components/FormComponents'
+
+const initialProvider = {
+ id: null,
+ name: '',
+ metadata: {
+ assumedRole: '',
+ userPoolId: '',
+ }
+}
+
+const ProviderForm = (props) => {
+ const {
+ handleSubmit,
+ handleCancel,
+ provider = initialProvider,
+ error,
+ dismissError,
+ } = props
+
+ const showError = (error, dismissError) => {
+ if (!!error) {
+ return (
+
+
+ dismissError()}
+ >
+
Error
+
{error}
+
+
+
+ )
+ }
+ return undefined
+ }
+
+ return (
+
+ {(formik) => (
+
+ )}
+
+ )
+}
+
+ProviderForm.propTypes = {
+ handleSubmit: PropTypes.func,
+ handleCancel: PropTypes.func,
+ dismissError: PropTypes.func,
+ provider: PropTypes.object,
+ error: PropTypes.string,
+ config: PropTypes.object,
+}
+
+export default ProviderForm
diff --git a/client/web/src/identity/ProviderListComponent.js b/client/web/src/identity/ProviderListComponent.js
new file mode 100644
index 00000000..98a0db0a
--- /dev/null
+++ b/client/web/src/identity/ProviderListComponent.js
@@ -0,0 +1,148 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {PropTypes} from 'prop-types'
+import React from 'react'
+import {
+ Card, CardBody, CardHeader, CardGroup, CardFooter,
+ Input,
+ Label,
+ Form, FormGroup,
+ Button,
+ Row,
+ Col,
+ Alert,
+} from 'reactstrap'
+import {ReactComponent as Auth0Logo} from './svg/auth0.svg';
+import {ReactComponent as CognitoLogo} from './svg/cognito.svg';
+import {ReactComponent as KeyCloakLogo} from './svg/keycloak.svg';
+
+ProviderListItem.propTypes = {
+ provider: PropTypes.object,
+ handleProviderClick: PropTypes.func,
+}
+
+function ProviderListItem({provider, handleProviderClick}) {
+ return (
+
+
+
+
+ )
+}
+
+export default ProviderList
diff --git a/client/web/src/identity/ProviderListContainer.js b/client/web/src/identity/ProviderListContainer.js
new file mode 100644
index 00000000..f4e3ada4
--- /dev/null
+++ b/client/web/src/identity/ProviderListContainer.js
@@ -0,0 +1,80 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, {useEffect, Fragment} from 'react'
+import {useDispatch, useSelector} from 'react-redux'
+
+import {fetchProvidersThunk, selectAllProviders, dismissError} from './ducks'
+import ProviderListComponent from "./ProviderListComponent";
+import {useHistory} from 'react-router-dom'
+import LoadingOverlay from '@ronchalant/react-loading-overlay'
+
+
+export default function ProviderListContainer() {
+ const dispatch = useDispatch()
+ const history = useHistory()
+ const providers = useSelector(selectAllProviders)
+ const loading = useSelector((state) => state.tiers.loading)
+ const error = useSelector((state) => state.tiers.error)
+ let selectedProvider = null;
+ const handleProviderClick = (id) => {
+ selectedProvider = id;
+ //console.log('selectedProvider: ', selectedProvider);
+ }
+ const handleCreateProvider = () => {
+ history.push(`/providers/${selectedProvider}`);
+ }
+
+ const handleRefresh = () => {
+ dispatch(fetchProvidersThunk())
+ }
+
+ const handleError = () => {
+ dispatch(dismissError())
+ }
+
+ useEffect(() => {
+ const fetchProviders = dispatch(fetchProvidersThunk());
+ //console.log('fetchProviders: ', fetchProviders);
+ return () => {
+ if (fetchProviders.PromiseStatus === 'pending') {
+ console.log('pending....');
+ fetchProviders.abort()
+ }
+ dispatch(dismissError())
+ }
+ }, [dispatch]) //TODO: Follow up on the use of this dispatch function.
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/client/web/src/identity/api/index.js b/client/web/src/identity/api/index.js
new file mode 100644
index 00000000..24664a9a
--- /dev/null
+++ b/client/web/src/identity/api/index.js
@@ -0,0 +1,165 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import axios from 'axios'
+import { fetchAccessToken, handleErrorResponse, handleErrorNoResponse } from '../../api'
+import appConfig from '../../config/appConfig'
+const { apiUri } = appConfig
+
+const apiServer = axios.create({
+ baseURL: `${apiUri}/identity`,
+ headers: {
+ common: {
+ 'Content-Type': 'application/json',
+ },
+ },
+ mode: 'cors',
+})
+const CancelToken = axios.CancelToken
+const source = CancelToken.source()
+
+apiServer.interceptors.request.use(async (r) => {
+ //Obtain and pass along Authorization token
+ const authorizationToken = await fetchAccessToken()
+ r.headers.Authorization = "Bearer " + authorizationToken
+
+ //Configure the AbortSignal
+ if (r.signal) {
+ r.signal.onabort = () => {
+ source.cancel()
+ }
+ }
+ r.cancelToken = source.token
+
+ return r
+})
+
+//API Aborted class
+class Aborted extends Error {
+ constructor(message, cause) {
+ super(message)
+ this.aborted = true
+ this.cause = cause
+ }
+}
+const identityAPI = {
+ fetchAll: async (ops) => {
+ const { signal } = ops
+ try {
+ const response = await apiServer.get('providers', { signal })
+ return response.data
+ } catch (err) {
+ if (axios.isCancel(err)) {
+ console.log('API call cancelled')
+ throw new Aborted('Call aborted', err)
+ } else {
+ console.error(err)
+ throw Error('Unable to fetch providers')
+ }
+ }
+ },
+ create: async (providerData) => {
+ //const { signal } = ops
+ try {
+ const authorizationToken = await fetchAccessToken();
+ const response = await fetch(`${apiUri}/identity/`, {
+ method: 'POST',
+ mode: 'cors',
+ body: JSON.stringify({
+ ...providerData,
+ }),
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: "Bearer " + authorizationToken,
+ },
+ })
+ return Promise.resolve(response);
+ //return await handleErrorResponse(response)
+ } catch (err) {
+ console.error(err)
+ throw Error('Unable to create provider')
+ }
+ },
+ update: async (providerData, ops) => {
+ const { signal } = ops
+
+ try {
+ const authorizationToken = await fetchAccessToken()
+ const response = await fetch(`${apiUri}/providers/${providerData.id}`, {
+ method: 'PUT',
+ mode: 'cors',
+ body: JSON.stringify({
+ ...providerData,
+ }),
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: "Bearer " + authorizationToken,
+ },
+ })
+ console.log('provider api update response', response)
+ return await handleErrorResponse(response)
+ } catch (err) {
+ console.error(err)
+ throw Error('Unable to edit provider.')
+ }
+ },
+ fetchProvider: async (providerId, ops) => {
+ const { signal } = ops
+
+ try {
+ const authorizationToken = await fetchAccessToken()
+ const response = await fetch(`${apiUri}/providers/${providerId}`, {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: "Bearer " + authorizationToken,
+ },
+ })
+ return await handleErrorResponse(response)
+ } catch (err) {
+ console.error(err)
+ throw Error(`Unable to fetch provider: ${providerId}`)
+ }
+ },
+ delete: async (values, ops) => {
+ const { signal } = ops
+ const { providerId, history } = values
+
+ try {
+ const response = await apiServer.delete(`/${providerId}`, { signal })
+ history.push('/providers')
+ return response.data
+ } catch (err) {
+ if (axios.isCancel(err)) {
+ throw new Aborted('Call aborted', err)
+ } else {
+ console.error(err)
+ throw Error(`Unable to delete provider ${providerId}`)
+ }
+ }
+ },
+ /**
+ * Determines if err is from a Cancelled or Aborted request
+ * @param err
+ */
+ isCancel: (err) => {
+ if (err.aborted && err.aborted === true) {
+ return true
+ }
+ return false
+ },
+}
+
+export default identityAPI
diff --git a/client/web/src/identity/ducks/index.js b/client/web/src/identity/ducks/index.js
new file mode 100644
index 00000000..f317058d
--- /dev/null
+++ b/client/web/src/identity/ducks/index.js
@@ -0,0 +1,162 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ createAsyncThunk,
+ createSlice,
+ createEntityAdapter,
+ createSelector,
+} from '@reduxjs/toolkit'
+
+import {normalize, schema} from 'normalizr'
+import identityAPI from '../api'
+import React from "react";
+
+// Define normalizr entity schemas
+const providerEntity = new schema.Entity('providers')
+const providerListSchema = [providerEntity]
+const providersAdapter = createEntityAdapter()
+
+const getName = (type) => {
+ if (type === 'AUTH0') {
+ return 'Auth0';
+ } else if (type === 'KEYCLOAK') {
+ return 'Keycloak';
+ } else {
+ return 'Amazon Cognito';
+ }
+};
+
+// Thunks
+export const fetchProvidersThunk = createAsyncThunk('/providers/fetchAll', async (...[, thunkAPI]) => {
+ const {signal} = thunkAPI
+ try {
+ const response = await identityAPI.fetchAll({signal});
+ console.log('fetchProvidersThunk: ', response);
+ let idFixed = response.map((provider) => {
+ const name = getName(provider.type);
+ return {
+ ...provider,
+ id: provider.type,
+ name: name
+ }
+ });
+ const normalized = normalize(idFixed, providerListSchema);
+ return normalized.entities
+ } catch (err) {
+ if (identityAPI.isCancel(err)) {
+ return
+ } else {
+ console.error(err)
+ return thunkAPI.rejectWithValue(err.message)
+ }
+ }
+})
+
+export const createProviderThunk = createAsyncThunk('providers/create',
+ async (providerData, thunkAPI) => {
+ const {signal} = thunkAPI
+ console.log('createProviderThunk.....');
+ try {
+ return await identityAPI.create(providerData, {signal})
+ } catch (err) {
+ console.error(err)
+ return thunkAPI.rejectWithValue(err.message)
+ }
+ },
+)
+
+const rejectedReducer = (state, action) => {
+ //Handle when thunk was aborted
+ console.log('rejectedReducer: ', state, action);
+ if (action.meta.aborted) {
+ state.error = {}
+ }
+ if (action.error) {
+ state.error = action.payload
+ }
+ if (state.loading === 'pending') {
+ state.loading = 'idle'
+ }
+ return state
+}
+
+const pendingReducer = (state, action) => {
+ state.error = null
+ if (state.loading === 'idle') {
+ state.loading = 'pending'
+ }
+ return state
+}
+const initialState = providersAdapter.getInitialState({
+ loading: 'idle',
+ error: null,
+ detail: null,
+})
+// Slices
+const providersSlice = createSlice({
+ name: 'providers',
+ initialState,
+ reducers: {
+ dismissError(state, error) {
+ //console.log('fetchProvidersThunk.dismissError: ', state);
+ state.error = null
+ return state
+ },
+ },
+ extraReducers: {
+ RESET: (state) => {
+ return initialState
+ },
+ [fetchProvidersThunk.fulfilled]: (state, action) => {
+ //console.log('fetchProvidersThunk.fulfilled: ', state, action);
+ if (action.payload === undefined) {
+ state.loading = 'idle'
+ state.error = null
+ return state
+ }
+ // Add identities to state object
+
+ providersAdapter.setAll(state, action.payload.providers ?? [])
+
+ state.loading = 'idle'
+ state.error = null
+ return state
+ },
+ [fetchProvidersThunk.pending]: pendingReducer,
+ [fetchProvidersThunk.rejected]: rejectedReducer,
+
+ [createProviderThunk.fulfilled]: (state, action) => {
+ console.log('createProviderThunk.fulfilled: ', state);
+ //tiersAdapter.upsertOne(state, action.payload)
+ state.loading = 'idle'
+ state.error = null
+ return state
+ },
+ [createProviderThunk.pending]: pendingReducer,
+ [createProviderThunk.rejected]: rejectedReducer,
+
+ },
+})
+
+const {actions, reducer} = providersSlice;
+
+export const {fetchAll, dismissError} = actions
+export const {selectAll: selectAllProviders, selectById: selectProviderById} =
+ providersAdapter.getSelectors((state) => state.providers)
+
+export default reducer
+
\ No newline at end of file
diff --git a/client/web/src/identity/index.js b/client/web/src/identity/index.js
new file mode 100644
index 00000000..a9d6f204
--- /dev/null
+++ b/client/web/src/identity/index.js
@@ -0,0 +1,35 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react'
+
+const ProviderListContainer = React.lazy(() => import('./ProviderListContainer'))
+const ProviderCreateContainer = React.lazy(() => import('./ProviderCreateContainer'))
+
+export const IdentityRoutes = [
+ {
+ path: '/providers',
+ exact: true,
+ name: 'Providers',
+ component: ProviderListContainer
+ },
+ {
+ path: `/providers/:providerId`,
+ exact: true,
+ name: 'Create Provider',
+ component: ProviderCreateContainer
+ }
+]
diff --git a/client/web/src/identity/svg/auth0.svg b/client/web/src/identity/svg/auth0.svg
new file mode 100644
index 00000000..ebeae3e0
--- /dev/null
+++ b/client/web/src/identity/svg/auth0.svg
@@ -0,0 +1,14 @@
+
+
+
diff --git a/client/web/src/identity/svg/cognito.svg b/client/web/src/identity/svg/cognito.svg
new file mode 100644
index 00000000..8bc8b546
--- /dev/null
+++ b/client/web/src/identity/svg/cognito.svg
@@ -0,0 +1,31 @@
+
+
+
diff --git a/client/web/src/identity/svg/keycloak.svg b/client/web/src/identity/svg/keycloak.svg
new file mode 100644
index 00000000..0e23f216
--- /dev/null
+++ b/client/web/src/identity/svg/keycloak.svg
@@ -0,0 +1,40 @@
+
+
+
diff --git a/client/web/src/onboarding/OnboardingCreateContainer.js b/client/web/src/onboarding/OnboardingCreateContainer.js
index 0d755f24..82a1c9d8 100644
--- a/client/web/src/onboarding/OnboardingCreateContainer.js
+++ b/client/web/src/onboarding/OnboardingCreateContainer.js
@@ -28,9 +28,10 @@ import {
} from './ducks'
import { useDispatch, useSelector } from 'react-redux'
import { selectConfig } from '../settings/ducks'
-import { selectAllPlans, fetchPlans, selectPlanLoading } from '../billing/ducks'
+//import { selectAllPlans, fetchPlans, selectPlanLoading } from '../billing/ducks'
import { saveToPresignedBucket } from '../settings/ducks'
import { selectAllTiers } from '../tier/ducks'
+import {transform} from "framer-motion";
export default function OnboardingCreateContainer() {
const dispatch = useDispatch()
@@ -39,12 +40,13 @@ export default function OnboardingCreateContainer() {
const error = useSelector(selectError)
const errorName = useSelector(selectErrorName)
const loading = useSelector(selectLoading)
- const plans = useSelector(selectAllPlans)
- const plansLoading = useSelector(selectPlanLoading)
+ //const plans = useSelector(selectAllPlans)
+ //const plansLoading = useSelector(selectPlanLoading)
const tiers = useSelector(selectAllTiers)
const [file, setFile] = useState({})
+ /*
useEffect(() => {
const fetchPlansThunk = dispatch(fetchPlans())
return () => {
@@ -54,6 +56,7 @@ export default function OnboardingCreateContainer() {
dispatch(dismissError())
}
}, [dispatch])
+ */
const nullBlankProps = (obj) => {
const ret = { ...obj }
@@ -65,16 +68,36 @@ export default function OnboardingCreateContainer() {
})
return ret
}
-
+ const dataTransform = (data)=> {
+ const transform = {
+ "name": data.name,
+ "tier": data.tier,
+ "subdomain": data.subdomain,
+ "adminUsers": [
+ {
+ "username": data.username,
+ "email": data.email,
+ "phoneNumber": data.phoneNumber,
+ "givenName": data.givenName,
+ "familyName": data.familyName
+ }
+ ]
+ };
+ return transform;
+ };
const submitOnboardingRequestForm = async (
values,
{ resetForm, setSubmitting }
) => {
- const { hasDomain, hasBilling, ...rest } = values
- const valsToSend = nullBlankProps(rest)
- let onboardingResponse
+ //const { hasDomain, hasBilling, ...rest } = values
+ const { hasDomain, ...rest } = values
+ const valsToSend = nullBlankProps(rest);
+ const data = dataTransform(valsToSend);
+
+ let onboardingResponse;
+ console.log('data: ', JSON.stringify(data));
try {
- onboardingResponse = await dispatch(createOnboarding(valsToSend))
+ onboardingResponse = await dispatch(createOnboarding(data))
const presignedS3url = onboardingResponse.payload.zipFile
if (presignedS3url && !!file && file.name) {
await dispatch(
@@ -105,8 +128,8 @@ export default function OnboardingCreateContainer() {
return (
tier.defaultTier)[0].name || '',
subdomain: '',
- billingPlan: '',
- hasBilling: hasBilling,
+ //billingPlan: '',
+ //hasBilling: hasBilling,
hasDomain: hasDomain,
}
@@ -105,6 +107,7 @@ export default function OnboardingFormComponent(props) {
)
}
+ /*
const getBillingUi = (plans, hasBilling) => {
const options = plans.map((plan) => {
return (
@@ -131,6 +134,7 @@ export default function OnboardingFormComponent(props) {
)
)
}
+ */
const getDomainUi = (domainName, hasDomain) => {
return hasDomain ? (
@@ -172,11 +176,17 @@ export default function OnboardingFormComponent(props) {
otherwise: Yup.string(),
})
.max(25, 'Must be 25 characters or less.'),
+ username: Yup.string()
+ .max(100, 'Must be 100 characters or less.')
+ .required('Required'),
+ email: Yup.string().email()
+ .required('Required'),
+ /*
billingPlan: Yup.string().when('hasBilling', {
is: true,
then: Yup.string().required('Billing plan is a required field'),
otherwise: Yup.string(),
- }),
+ }),*/
})
return (
@@ -205,7 +215,49 @@ export default function OnboardingFormComponent(props) {
/>
{getTiers(tiers, formik.values.tier)}
{getDomainUi(domainName, hasDomain)}
- {getBillingUi(billingPlans, hasBilling)}
+ {/* {getBillingUi(billingPlans, hasBilling)} */}
+ Admin User
+
+
+
+
+
+
+
+
+
+
+ {
{!!error && showError(error, dismissError)}
-
+ {/*
Onboarding tenants requires an application image to be uploaded for each service.
If you haven't done so, view the upload instructions for each service
here.
-
-
+ */}
+
@@ -200,7 +198,7 @@ TenantContainer.propTypes = {
detail: PropTypes.object,
loading: PropTypes.string,
config: PropTypes.object,
- plans: PropTypes.array,
+ //plans: PropTypes.array,
}
export const TenantContainerWithRouter = connect(
diff --git a/client/web/src/tenant/api/index.js b/client/web/src/tenant/api/index.js
index 18af710d..1e423786 100644
--- a/client/web/src/tenant/api/index.js
+++ b/client/web/src/tenant/api/index.js
@@ -55,25 +55,25 @@ class Aborted extends Error {
}
}
const tenantAPI = {
- fetchAll: async () => {
- try {
- const authorizationToken = await fetchAccessToken()
- const response = await fetch(`${apiUri}/tenants/provisioned`, {
- method: 'GET',
- mode: 'cors',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: "Bearer " + authorizationToken,
- },
- })
+ // fetchAll: async () => {
+ // try {
+ // const authorizationToken = await fetchAccessToken()
+ // const response = await fetch(`${apiUri}/tenants/provisioned`, {
+ // method: 'GET',
+ // mode: 'cors',
+ // headers: {
+ // 'Content-Type': 'application/json',
+ // Authorization: "Bearer " + authorizationToken,
+ // },
+ // })
- const responseJSON = await handleErrorResponse(response)
- return responseJSON
- } catch (err) {
- console.error(err)
- throw Error('Unable to fetch tenants')
- }
- },
+ // const responseJSON = await handleErrorResponse(response)
+ // return responseJSON
+ // } catch (err) {
+ // console.error(err)
+ // throw Error('Unable to fetch tenants')
+ // }
+ // },
fetchAllAxios: async (ops) => {
const { signal } = ops
diff --git a/client/web/yarn.lock b/client/web/yarn.lock
index 274ae772..24422c15 100644
--- a/client/web/yarn.lock
+++ b/client/web/yarn.lock
@@ -24,2109 +24,6 @@
jsonpointer "^5.0.0"
leven "^3.1.0"
-"@aws-amplify/analytics@5.2.31":
- version "5.2.31"
- resolved "https://registry.yarnpkg.com/@aws-amplify/analytics/-/analytics-5.2.31.tgz#8a8a786110c880a8d5de15353f884ccf1552c600"
- integrity sha512-u2j5qZRTDGD7d1TpbKU3D7928VFJK602537TWDuUibUCQWafCDLzPj1IJCiC6UdZ1yShqEmexa02/cqtq+gbwg==
- dependencies:
- "@aws-amplify/cache" "4.0.66"
- "@aws-amplify/core" "4.7.15"
- "@aws-sdk/client-firehose" "3.6.1"
- "@aws-sdk/client-kinesis" "3.6.1"
- "@aws-sdk/client-personalize-events" "3.6.1"
- "@aws-sdk/client-pinpoint" "3.6.1"
- "@aws-sdk/util-utf8-browser" "3.6.1"
- lodash "^4.17.20"
- uuid "^3.2.1"
-
-"@aws-amplify/api-graphql@2.3.28":
- version "2.3.28"
- resolved "https://registry.yarnpkg.com/@aws-amplify/api-graphql/-/api-graphql-2.3.28.tgz#d0f2f75eb8cb4bfb9be1b6b3045599caa442d1c5"
- integrity sha512-n/8dwUx2i9sojcAnK1vITamx/FODGPmDM08lTfZNwpTVJ1aXB/bcA9GitF7gWa4jstVACDgQAKmTAr7j2d0tGw==
- dependencies:
- "@aws-amplify/api-rest" "2.0.64"
- "@aws-amplify/auth" "4.6.17"
- "@aws-amplify/cache" "4.0.66"
- "@aws-amplify/core" "4.7.15"
- "@aws-amplify/pubsub" "4.5.14"
- graphql "15.8.0"
- uuid "^3.2.1"
- zen-observable-ts "0.8.19"
-
-"@aws-amplify/api-rest@2.0.64":
- version "2.0.64"
- resolved "https://registry.yarnpkg.com/@aws-amplify/api-rest/-/api-rest-2.0.64.tgz#ccf7ffd2d2fb1b7194c07a0ddfd5dfd21aa6638d"
- integrity sha512-hS+ImRnkyjGJj5gTet+Gd979Vnsp1lKTmiUngt3MXY/0b6CeUgMAACxnIQ628J00frvguUcgmOlZ502jeHsiKQ==
- dependencies:
- "@aws-amplify/core" "4.7.15"
- axios "0.26.0"
-
-"@aws-amplify/api@4.0.64":
- version "4.0.64"
- resolved "https://registry.yarnpkg.com/@aws-amplify/api/-/api-4.0.64.tgz#20c9d89dce4092a8735ccd70b2f9a16071dfb964"
- integrity sha512-nhg7Z+TQcEnLR5ZotxvKnJgqNwDtUYVBcNuktsHgUVszkKT/Oj2vC28xv8RufdljIofrXFsBDeERviwSpVXiFA==
- dependencies:
- "@aws-amplify/api-graphql" "2.3.28"
- "@aws-amplify/api-rest" "2.0.64"
-
-"@aws-amplify/auth@4.6.17":
- version "4.6.17"
- resolved "https://registry.yarnpkg.com/@aws-amplify/auth/-/auth-4.6.17.tgz#5030e515467d2f9469eaa388a46370c7d5648772"
- integrity sha512-KIWHP6qODphwtzyJ6jmcSQewH0a8dOOsQ35OtAALwmPNEaftGmoUjm8wMHAtyH3EwWv1iknhPwMVzmGylr+l1A==
- dependencies:
- "@aws-amplify/cache" "4.0.66"
- "@aws-amplify/core" "4.7.15"
- amazon-cognito-identity-js "5.2.14"
- crypto-js "^4.1.1"
-
-"@aws-amplify/cache@4.0.66":
- version "4.0.66"
- resolved "https://registry.yarnpkg.com/@aws-amplify/cache/-/cache-4.0.66.tgz#16977bd9a3d7740c4b98101173e4bf31983a360f"
- integrity sha512-dG5TSx1VbUMnIchqwoT+Pa5W+PdPTZVcXfg/4bjpv0HJ0s3LUeYMI93cpQGg0DlegKNvwV5Ib+B7UqXlWp/JEQ==
- dependencies:
- "@aws-amplify/core" "4.7.15"
-
-"@aws-amplify/core@4.7.15":
- version "4.7.15"
- resolved "https://registry.yarnpkg.com/@aws-amplify/core/-/core-4.7.15.tgz#b19c65c0ea8b2b52f53e15343a374bf2751c261d"
- integrity sha512-upRxT6MN90pQZnJw2VwGdA7vHO6tGY1c3qLrXkq+x5XT45KrfGjbSSHmYBo7PkjWQYAUMGuX4KYwmPBuI58svg==
- dependencies:
- "@aws-crypto/sha256-js" "1.0.0-alpha.0"
- "@aws-sdk/client-cloudwatch-logs" "3.6.1"
- "@aws-sdk/client-cognito-identity" "3.6.1"
- "@aws-sdk/credential-provider-cognito-identity" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/util-hex-encoding" "3.6.1"
- universal-cookie "^4.0.4"
- zen-observable-ts "0.8.19"
-
-"@aws-amplify/datastore@3.14.7":
- version "3.14.7"
- resolved "https://registry.yarnpkg.com/@aws-amplify/datastore/-/datastore-3.14.7.tgz#d4d683d6aa238179fd45ac899fd547c02c3a7b17"
- integrity sha512-nzZHK0LXOsvmZzeBHL8VL/nrTm9dmBYdOWZOf7zSrbZBVaLEMim2l2os3DUx0+1u44XPr166QSF8OXLpl+56+w==
- dependencies:
- "@aws-amplify/api" "4.0.64"
- "@aws-amplify/auth" "4.6.17"
- "@aws-amplify/core" "4.7.15"
- "@aws-amplify/pubsub" "4.5.14"
- amazon-cognito-identity-js "5.2.14"
- idb "5.0.6"
- immer "9.0.6"
- ulid "2.3.0"
- uuid "3.3.2"
- zen-observable-ts "0.8.19"
- zen-push "0.2.1"
-
-"@aws-amplify/geo@1.3.27":
- version "1.3.27"
- resolved "https://registry.yarnpkg.com/@aws-amplify/geo/-/geo-1.3.27.tgz#b28b472022298a26070289b599f3dc83cdfb4102"
- integrity sha512-7ytYD0M3EJxq9aiqJVQSRoXXUYf/bp7MU2Bb+UvKjqxOb29theJp3RJ7yJnqjxAV+6K7+jRpjoqH8lR+y3zkwQ==
- dependencies:
- "@aws-amplify/core" "4.7.15"
- "@aws-sdk/client-location" "3.186.0"
- "@turf/boolean-clockwise" "6.5.0"
- camelcase-keys "6.2.2"
-
-"@aws-amplify/interactions@4.1.12":
- version "4.1.12"
- resolved "https://registry.yarnpkg.com/@aws-amplify/interactions/-/interactions-4.1.12.tgz#b4e953c335b2638890459f66a58b8484f914186f"
- integrity sha512-MQjq4wdGuA7DNRywMrlwjbWZ/b5VFP0ASZdMYWSGVVkjPpHKR+/iCy/kkJvUFXIl8kEXHlFQTidv4RiNd4sYdQ==
- dependencies:
- "@aws-amplify/core" "4.7.15"
- "@aws-sdk/client-lex-runtime-service" "3.186.0"
- "@aws-sdk/client-lex-runtime-v2" "3.186.0"
- base-64 "1.0.0"
- fflate "0.7.3"
- pako "2.0.4"
-
-"@aws-amplify/predictions@4.0.64":
- version "4.0.64"
- resolved "https://registry.yarnpkg.com/@aws-amplify/predictions/-/predictions-4.0.64.tgz#edfa0e916982d1a42a20484310d71c55e9b52cba"
- integrity sha512-EcRwCqf0xFGoJLAzns7TIgKZxKZUlXubVPMTGIm9imVT/ZuF7ELX/YhIygzR33M+75rzLJxQcx5OOTFj6df/1Q==
- dependencies:
- "@aws-amplify/core" "4.7.15"
- "@aws-amplify/storage" "4.5.17"
- "@aws-sdk/client-comprehend" "3.6.1"
- "@aws-sdk/client-polly" "3.6.1"
- "@aws-sdk/client-rekognition" "3.6.1"
- "@aws-sdk/client-textract" "3.6.1"
- "@aws-sdk/client-translate" "3.6.1"
- "@aws-sdk/eventstream-marshaller" "3.6.1"
- "@aws-sdk/util-utf8-node" "3.6.1"
- uuid "^3.2.1"
-
-"@aws-amplify/pubsub@4.5.14":
- version "4.5.14"
- resolved "https://registry.yarnpkg.com/@aws-amplify/pubsub/-/pubsub-4.5.14.tgz#dead3329e64ad0a69ce005de70703d185d6f05cc"
- integrity sha512-WGR26nOMW2+DQE1DuWE4W9Ehx1RxmNmQN6Mq27DnKicLL0nMgyKT7OGBAHmQzVtsvMzFgUo/KcMBL3GltZ0M5g==
- dependencies:
- "@aws-amplify/auth" "4.6.17"
- "@aws-amplify/cache" "4.0.66"
- "@aws-amplify/core" "4.7.15"
- graphql "15.8.0"
- paho-mqtt "^1.1.0"
- uuid "^3.2.1"
- zen-observable-ts "0.8.19"
-
-"@aws-amplify/storage@4.5.17":
- version "4.5.17"
- resolved "https://registry.yarnpkg.com/@aws-amplify/storage/-/storage-4.5.17.tgz#9fd75d2e89fce220d0d1e1dd823416a77f2d2284"
- integrity sha512-GZJvTdZ8zjlSfQ32x4EY56sOTafL843s6geqd8d/ybpJYZqEyBpfbcLZnsZFStAEERBKB4hCyCs/m+E2zZg/xg==
- dependencies:
- "@aws-amplify/core" "4.7.15"
- "@aws-sdk/client-s3" "3.6.1"
- "@aws-sdk/s3-request-presigner" "3.6.1"
- "@aws-sdk/util-create-request" "3.6.1"
- "@aws-sdk/util-format-url" "3.6.1"
- axios "0.26.0"
- events "^3.1.0"
-
-"@aws-amplify/ui@2.0.7":
- version "2.0.7"
- resolved "https://registry.yarnpkg.com/@aws-amplify/ui/-/ui-2.0.7.tgz#1d0b230306ca4fcd9c9ab5475f37d33c3eb83b37"
- integrity sha512-tT7onRv+OCznFhUE2mKPpbGHHV+oODZk4VDX3lYNIfJ7LXv1hVtllQbPNJF5beNBRw9r6uotlXpeJrkph6v07A==
-
-"@aws-amplify/xr@3.0.64":
- version "3.0.64"
- resolved "https://registry.yarnpkg.com/@aws-amplify/xr/-/xr-3.0.64.tgz#a852c3d857373d34415d8050780a302ed3ac269c"
- integrity sha512-YZJbHVEU9uN8yKHms2uIWyikUPEj4go6qL40vcIDwCv9LNyer2lP+yZ1Djn1FFhqUgLi5lK+yh4PUCoqPUWE8w==
- dependencies:
- "@aws-amplify/core" "4.7.15"
-
-"@aws-crypto/crc32@2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-2.0.0.tgz#4ad432a3c03ec3087c5540ff6e41e6565d2dc153"
- integrity sha512-TvE1r2CUueyXOuHdEigYjIZVesInd9KN+K/TFFNfkkxRThiNxO6i4ZqqAVMoEjAamZZ1AA8WXJkjCz7YShHPQA==
- dependencies:
- "@aws-crypto/util" "^2.0.0"
- "@aws-sdk/types" "^3.1.0"
- tslib "^1.11.1"
-
-"@aws-crypto/crc32@^1.0.0":
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-1.2.2.tgz#4a758a596fa8cb3ab463f037a78c2ca4992fe81f"
- integrity sha512-8K0b1672qbv05chSoKpwGZ3fhvVp28Fg3AVHVkEHFl2lTLChO7wD/hTyyo8ING7uc31uZRt7bNra/hA74Td7Tw==
- dependencies:
- "@aws-crypto/util" "^1.2.2"
- "@aws-sdk/types" "^3.1.0"
- tslib "^1.11.1"
-
-"@aws-crypto/ie11-detection@^1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@aws-crypto/ie11-detection/-/ie11-detection-1.0.0.tgz#d3a6af29ba7f15458f79c41d1cd8cac3925e726a"
- integrity sha512-kCKVhCF1oDxFYgQrxXmIrS5oaWulkvRcPz+QBDMsUr2crbF4VGgGT6+uQhSwJFdUAQ2A//Vq+uT83eJrkzFgXA==
- dependencies:
- tslib "^1.11.1"
-
-"@aws-crypto/ie11-detection@^2.0.0":
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/@aws-crypto/ie11-detection/-/ie11-detection-2.0.2.tgz#9c39f4a5558196636031a933ec1b4792de959d6a"
- integrity sha512-5XDMQY98gMAf/WRTic5G++jfmS/VLM0rwpiOpaainKi4L0nqWMSB1SzsrEG5rjFZGYN6ZAefO+/Yta2dFM0kMw==
- dependencies:
- tslib "^1.11.1"
-
-"@aws-crypto/sha256-browser@2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-browser/-/sha256-browser-2.0.0.tgz#741c9024df55ec59b51e5b1f5d806a4852699fb5"
- integrity sha512-rYXOQ8BFOaqMEHJrLHul/25ckWH6GTJtdLSajhlqGMx0PmSueAuvboCuZCTqEKlxR8CQOwRarxYMZZSYlhRA1A==
- dependencies:
- "@aws-crypto/ie11-detection" "^2.0.0"
- "@aws-crypto/sha256-js" "^2.0.0"
- "@aws-crypto/supports-web-crypto" "^2.0.0"
- "@aws-crypto/util" "^2.0.0"
- "@aws-sdk/types" "^3.1.0"
- "@aws-sdk/util-locate-window" "^3.0.0"
- "@aws-sdk/util-utf8-browser" "^3.0.0"
- tslib "^1.11.1"
-
-"@aws-crypto/sha256-browser@^1.0.0":
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-browser/-/sha256-browser-1.2.2.tgz#004d806e3bbae130046c259ec3279a02d4a0b576"
- integrity sha512-0tNR4kBtJp+9S0kis4+JLab3eg6QWuIeuPhzaYoYwNUXGBgsWIkktA2mnilet+EGWzf3n1zknJXC4X4DVyyXbg==
- dependencies:
- "@aws-crypto/ie11-detection" "^1.0.0"
- "@aws-crypto/sha256-js" "^1.2.2"
- "@aws-crypto/supports-web-crypto" "^1.0.0"
- "@aws-crypto/util" "^1.2.2"
- "@aws-sdk/types" "^3.1.0"
- "@aws-sdk/util-locate-window" "^3.0.0"
- tslib "^1.11.1"
-
-"@aws-crypto/sha256-js@1.0.0-alpha.0":
- version "1.0.0-alpha.0"
- resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-1.0.0-alpha.0.tgz#1146f6fa823001a9065ce60db5bf1afcc7c1cc3a"
- integrity sha512-GidX2lccEtHZw8mXDKJQj6tea7qh3pAnsNSp1eZNxsN4MMu2OvSraPSqiB1EihsQkZBMg0IiZPpZHoACUX/QMQ==
- dependencies:
- "@aws-sdk/types" "^1.0.0-alpha.0"
- "@aws-sdk/util-utf8-browser" "^1.0.0-alpha.0"
- tslib "^1.9.3"
-
-"@aws-crypto/sha256-js@2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-2.0.0.tgz#f1f936039bdebd0b9e2dd834d65afdc2aac4efcb"
- integrity sha512-VZY+mCY4Nmrs5WGfitmNqXzaE873fcIZDu54cbaDaaamsaTOP1DBImV9F4pICc3EHjQXujyE8jig+PFCaew9ig==
- dependencies:
- "@aws-crypto/util" "^2.0.0"
- "@aws-sdk/types" "^3.1.0"
- tslib "^1.11.1"
-
-"@aws-crypto/sha256-js@^1.0.0", "@aws-crypto/sha256-js@^1.2.2":
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-1.2.2.tgz#02acd1a1fda92896fc5a28ec7c6e164644ea32fc"
- integrity sha512-Nr1QJIbW/afYYGzYvrF70LtaHrIRtd4TNAglX8BvlfxJLZ45SAmueIKYl5tWoNBPzp65ymXGFK0Bb1vZUpuc9g==
- dependencies:
- "@aws-crypto/util" "^1.2.2"
- "@aws-sdk/types" "^3.1.0"
- tslib "^1.11.1"
-
-"@aws-crypto/sha256-js@^2.0.0":
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-2.0.2.tgz#c81e5d378b8a74ff1671b58632779986e50f4c99"
- integrity sha512-iXLdKH19qPmIC73fVCrHWCSYjN/sxaAvZ3jNNyw6FclmHyjLKg0f69WlC9KTnyElxCR5MO9SKaG00VwlJwyAkQ==
- dependencies:
- "@aws-crypto/util" "^2.0.2"
- "@aws-sdk/types" "^3.110.0"
- tslib "^1.11.1"
-
-"@aws-crypto/supports-web-crypto@^1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@aws-crypto/supports-web-crypto/-/supports-web-crypto-1.0.0.tgz#c40901bc17ac1e875e248df16a2b47ad8bfd9a93"
- integrity sha512-IHLfv+WmVH89EW4n6a5eE8/hUlz6qkWGMn/v4r5ZgzcXdTC5nolii2z3k46y01hWRiC2PPhOdeSLzMUCUMco7g==
- dependencies:
- tslib "^1.11.1"
-
-"@aws-crypto/supports-web-crypto@^2.0.0":
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/@aws-crypto/supports-web-crypto/-/supports-web-crypto-2.0.2.tgz#9f02aafad8789cac9c0ab5faaebb1ab8aa841338"
- integrity sha512-6mbSsLHwZ99CTOOswvCRP3C+VCWnzBf+1SnbWxzzJ9lR0mA0JnY2JEAhp8rqmTE0GPFy88rrM27ffgp62oErMQ==
- dependencies:
- tslib "^1.11.1"
-
-"@aws-crypto/util@^1.2.2":
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-1.2.2.tgz#b28f7897730eb6538b21c18bd4de22d0ea09003c"
- integrity sha512-H8PjG5WJ4wz0UXAFXeJjWCW1vkvIJ3qUUD+rGRwJ2/hj+xT58Qle2MTql/2MGzkU+1JLAFuR6aJpLAjHwhmwwg==
- dependencies:
- "@aws-sdk/types" "^3.1.0"
- "@aws-sdk/util-utf8-browser" "^3.0.0"
- tslib "^1.11.1"
-
-"@aws-crypto/util@^2.0.0", "@aws-crypto/util@^2.0.2":
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/@aws-crypto/util/-/util-2.0.2.tgz#adf5ff5dfbc7713082f897f1d01e551ce0edb9c0"
- integrity sha512-Lgu5v/0e/BcrZ5m/IWqzPUf3UYFTy/PpeED+uc9SWUR1iZQL8XXbGQg10UfllwwBryO3hFF5dizK+78aoXC1eA==
- dependencies:
- "@aws-sdk/types" "^3.110.0"
- "@aws-sdk/util-utf8-browser" "^3.0.0"
- tslib "^1.11.1"
-
-"@aws-sdk/abort-controller@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/abort-controller/-/abort-controller-3.186.0.tgz#dfaccd296d57136930582e1a19203d6cb60debc7"
- integrity sha512-JFvvvtEcbYOvVRRXasi64Dd1VcOz5kJmPvtzsJ+HzMHvPbGGs/aopOJAZQJMJttzJmJwVTay0QL6yag9Kk8nYA==
- dependencies:
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/abort-controller@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/abort-controller/-/abort-controller-3.6.1.tgz#75812875bbef6ad17e0e3a6d96aab9df636376f9"
- integrity sha512-X81XkxX/2Tvv9YNcEto/rcQzPIdKJHFSnl9hBl/qkSdCFV/GaQ2XNWfKm5qFXMLlZNFS0Fn5CnBJ83qnBm47vg==
- dependencies:
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/chunked-blob-reader-native@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/chunked-blob-reader-native/-/chunked-blob-reader-native-3.6.1.tgz#21c2c8773c3cd8403c2a953fd0e9e4f69c120214"
- integrity sha512-vP6bc2v9h442Srmo7t2QcIbPjk5IqLSf4jGnKDAes8z+7eyjCtKugRP3lOM1fJCfGlPIsJGYnexxYdEGw008vA==
- dependencies:
- "@aws-sdk/util-base64-browser" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/chunked-blob-reader@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/chunked-blob-reader/-/chunked-blob-reader-3.6.1.tgz#63363025dcecc2f9dd47ae5c282d79c01b327d82"
- integrity sha512-QBGUBoD8D5nsM/EKoc0rjpApa5NE5pQVzw1caE8sG00QMMPkCXWSB/gTVKVY0GOAhJFoA/VpVPQchIlZcOrBFg==
- dependencies:
- tslib "^1.8.0"
-
-"@aws-sdk/client-cloudwatch-logs@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.6.1.tgz#5e8dba495a2ba9a901b0a1a2d53edef8bd452398"
- integrity sha512-QOxIDnlVTpnwJ26Gap6RGz61cDLH6TKrIp30VqwdMeT1pCGy8mn9rWln6XA+ymkofHy/08RfpGp+VN4axwd4Lw==
- dependencies:
- "@aws-crypto/sha256-browser" "^1.0.0"
- "@aws-crypto/sha256-js" "^1.0.0"
- "@aws-sdk/config-resolver" "3.6.1"
- "@aws-sdk/credential-provider-node" "3.6.1"
- "@aws-sdk/fetch-http-handler" "3.6.1"
- "@aws-sdk/hash-node" "3.6.1"
- "@aws-sdk/invalid-dependency" "3.6.1"
- "@aws-sdk/middleware-content-length" "3.6.1"
- "@aws-sdk/middleware-host-header" "3.6.1"
- "@aws-sdk/middleware-logger" "3.6.1"
- "@aws-sdk/middleware-retry" "3.6.1"
- "@aws-sdk/middleware-serde" "3.6.1"
- "@aws-sdk/middleware-signing" "3.6.1"
- "@aws-sdk/middleware-stack" "3.6.1"
- "@aws-sdk/middleware-user-agent" "3.6.1"
- "@aws-sdk/node-config-provider" "3.6.1"
- "@aws-sdk/node-http-handler" "3.6.1"
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/smithy-client" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/url-parser" "3.6.1"
- "@aws-sdk/url-parser-native" "3.6.1"
- "@aws-sdk/util-base64-browser" "3.6.1"
- "@aws-sdk/util-base64-node" "3.6.1"
- "@aws-sdk/util-body-length-browser" "3.6.1"
- "@aws-sdk/util-body-length-node" "3.6.1"
- "@aws-sdk/util-user-agent-browser" "3.6.1"
- "@aws-sdk/util-user-agent-node" "3.6.1"
- "@aws-sdk/util-utf8-browser" "3.6.1"
- "@aws-sdk/util-utf8-node" "3.6.1"
- tslib "^2.0.0"
-
-"@aws-sdk/client-cognito-identity@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.6.1.tgz#36992a4fef7eff1f2b1dbee30850e30ebdfc15bb"
- integrity sha512-FMj2GR9R5oCKb3/NI16GIvWeHcE4uX42fBAaQKPbjg2gALFDx9CcJYsdOtDP37V89GtPyZilLv6GJxrwJKzYGg==
- dependencies:
- "@aws-crypto/sha256-browser" "^1.0.0"
- "@aws-crypto/sha256-js" "^1.0.0"
- "@aws-sdk/config-resolver" "3.6.1"
- "@aws-sdk/credential-provider-node" "3.6.1"
- "@aws-sdk/fetch-http-handler" "3.6.1"
- "@aws-sdk/hash-node" "3.6.1"
- "@aws-sdk/invalid-dependency" "3.6.1"
- "@aws-sdk/middleware-content-length" "3.6.1"
- "@aws-sdk/middleware-host-header" "3.6.1"
- "@aws-sdk/middleware-logger" "3.6.1"
- "@aws-sdk/middleware-retry" "3.6.1"
- "@aws-sdk/middleware-serde" "3.6.1"
- "@aws-sdk/middleware-signing" "3.6.1"
- "@aws-sdk/middleware-stack" "3.6.1"
- "@aws-sdk/middleware-user-agent" "3.6.1"
- "@aws-sdk/node-config-provider" "3.6.1"
- "@aws-sdk/node-http-handler" "3.6.1"
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/smithy-client" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/url-parser" "3.6.1"
- "@aws-sdk/url-parser-native" "3.6.1"
- "@aws-sdk/util-base64-browser" "3.6.1"
- "@aws-sdk/util-base64-node" "3.6.1"
- "@aws-sdk/util-body-length-browser" "3.6.1"
- "@aws-sdk/util-body-length-node" "3.6.1"
- "@aws-sdk/util-user-agent-browser" "3.6.1"
- "@aws-sdk/util-user-agent-node" "3.6.1"
- "@aws-sdk/util-utf8-browser" "3.6.1"
- "@aws-sdk/util-utf8-node" "3.6.1"
- tslib "^2.0.0"
-
-"@aws-sdk/client-comprehend@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/client-comprehend/-/client-comprehend-3.6.1.tgz#d640d510b49feafa94ac252cdd7942cbe5537249"
- integrity sha512-Y2ixlSTjjAp2HJhkUArtYqC/X+zG5Qqu3Bl+Ez22u4u4YnG8HsNFD6FE1axuWSdSa5AFtWTEt+Cz2Ghj/tDySA==
- dependencies:
- "@aws-crypto/sha256-browser" "^1.0.0"
- "@aws-crypto/sha256-js" "^1.0.0"
- "@aws-sdk/config-resolver" "3.6.1"
- "@aws-sdk/credential-provider-node" "3.6.1"
- "@aws-sdk/fetch-http-handler" "3.6.1"
- "@aws-sdk/hash-node" "3.6.1"
- "@aws-sdk/invalid-dependency" "3.6.1"
- "@aws-sdk/middleware-content-length" "3.6.1"
- "@aws-sdk/middleware-host-header" "3.6.1"
- "@aws-sdk/middleware-logger" "3.6.1"
- "@aws-sdk/middleware-retry" "3.6.1"
- "@aws-sdk/middleware-serde" "3.6.1"
- "@aws-sdk/middleware-signing" "3.6.1"
- "@aws-sdk/middleware-stack" "3.6.1"
- "@aws-sdk/middleware-user-agent" "3.6.1"
- "@aws-sdk/node-config-provider" "3.6.1"
- "@aws-sdk/node-http-handler" "3.6.1"
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/smithy-client" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/url-parser" "3.6.1"
- "@aws-sdk/url-parser-native" "3.6.1"
- "@aws-sdk/util-base64-browser" "3.6.1"
- "@aws-sdk/util-base64-node" "3.6.1"
- "@aws-sdk/util-body-length-browser" "3.6.1"
- "@aws-sdk/util-body-length-node" "3.6.1"
- "@aws-sdk/util-user-agent-browser" "3.6.1"
- "@aws-sdk/util-user-agent-node" "3.6.1"
- "@aws-sdk/util-utf8-browser" "3.6.1"
- "@aws-sdk/util-utf8-node" "3.6.1"
- tslib "^2.0.0"
- uuid "^3.0.0"
-
-"@aws-sdk/client-firehose@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/client-firehose/-/client-firehose-3.6.1.tgz#87a8ef0c18267907b3ce712e6d3de8f36b0a7c7b"
- integrity sha512-KhiKCm+cJmnRFuAEyO3DBpFVDNix1XcVikdxk2lvYbFWkM1oUZoBpudxaJ+fPf2W3stF3CXIAOP+TnGqSZCy9g==
- dependencies:
- "@aws-crypto/sha256-browser" "^1.0.0"
- "@aws-crypto/sha256-js" "^1.0.0"
- "@aws-sdk/config-resolver" "3.6.1"
- "@aws-sdk/credential-provider-node" "3.6.1"
- "@aws-sdk/fetch-http-handler" "3.6.1"
- "@aws-sdk/hash-node" "3.6.1"
- "@aws-sdk/invalid-dependency" "3.6.1"
- "@aws-sdk/middleware-content-length" "3.6.1"
- "@aws-sdk/middleware-host-header" "3.6.1"
- "@aws-sdk/middleware-logger" "3.6.1"
- "@aws-sdk/middleware-retry" "3.6.1"
- "@aws-sdk/middleware-serde" "3.6.1"
- "@aws-sdk/middleware-signing" "3.6.1"
- "@aws-sdk/middleware-stack" "3.6.1"
- "@aws-sdk/middleware-user-agent" "3.6.1"
- "@aws-sdk/node-config-provider" "3.6.1"
- "@aws-sdk/node-http-handler" "3.6.1"
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/smithy-client" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/url-parser" "3.6.1"
- "@aws-sdk/url-parser-native" "3.6.1"
- "@aws-sdk/util-base64-browser" "3.6.1"
- "@aws-sdk/util-base64-node" "3.6.1"
- "@aws-sdk/util-body-length-browser" "3.6.1"
- "@aws-sdk/util-body-length-node" "3.6.1"
- "@aws-sdk/util-user-agent-browser" "3.6.1"
- "@aws-sdk/util-user-agent-node" "3.6.1"
- "@aws-sdk/util-utf8-browser" "3.6.1"
- "@aws-sdk/util-utf8-node" "3.6.1"
- tslib "^2.0.0"
-
-"@aws-sdk/client-kinesis@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/client-kinesis/-/client-kinesis-3.6.1.tgz#48583cc854f9108bc8ff6168005d9a05b24bae31"
- integrity sha512-Ygo+92LxHeUZmiyhiHT+k7hIOhJd6S7ckCEVUsQs2rfwe9bAygUY/3cCoZSqgWy7exFRRKsjhzStcyV6i6jrVQ==
- dependencies:
- "@aws-crypto/sha256-browser" "^1.0.0"
- "@aws-crypto/sha256-js" "^1.0.0"
- "@aws-sdk/config-resolver" "3.6.1"
- "@aws-sdk/credential-provider-node" "3.6.1"
- "@aws-sdk/eventstream-serde-browser" "3.6.1"
- "@aws-sdk/eventstream-serde-config-resolver" "3.6.1"
- "@aws-sdk/eventstream-serde-node" "3.6.1"
- "@aws-sdk/fetch-http-handler" "3.6.1"
- "@aws-sdk/hash-node" "3.6.1"
- "@aws-sdk/invalid-dependency" "3.6.1"
- "@aws-sdk/middleware-content-length" "3.6.1"
- "@aws-sdk/middleware-host-header" "3.6.1"
- "@aws-sdk/middleware-logger" "3.6.1"
- "@aws-sdk/middleware-retry" "3.6.1"
- "@aws-sdk/middleware-serde" "3.6.1"
- "@aws-sdk/middleware-signing" "3.6.1"
- "@aws-sdk/middleware-stack" "3.6.1"
- "@aws-sdk/middleware-user-agent" "3.6.1"
- "@aws-sdk/node-config-provider" "3.6.1"
- "@aws-sdk/node-http-handler" "3.6.1"
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/smithy-client" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/url-parser" "3.6.1"
- "@aws-sdk/url-parser-native" "3.6.1"
- "@aws-sdk/util-base64-browser" "3.6.1"
- "@aws-sdk/util-base64-node" "3.6.1"
- "@aws-sdk/util-body-length-browser" "3.6.1"
- "@aws-sdk/util-body-length-node" "3.6.1"
- "@aws-sdk/util-user-agent-browser" "3.6.1"
- "@aws-sdk/util-user-agent-node" "3.6.1"
- "@aws-sdk/util-utf8-browser" "3.6.1"
- "@aws-sdk/util-utf8-node" "3.6.1"
- "@aws-sdk/util-waiter" "3.6.1"
- tslib "^2.0.0"
-
-"@aws-sdk/client-lex-runtime-service@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/client-lex-runtime-service/-/client-lex-runtime-service-3.186.0.tgz#81deea7402cb76e7f2dce56bc5778e51909e1374"
- integrity sha512-EgjQvFxa/o1urxpnWV2A/D0k4m763NqrPLuL074LR+cOkNxVl9W27aYL/tddDBmmDzzx4KcuRL6/n+UBZIheTg==
- dependencies:
- "@aws-crypto/sha256-browser" "2.0.0"
- "@aws-crypto/sha256-js" "2.0.0"
- "@aws-sdk/client-sts" "3.186.0"
- "@aws-sdk/config-resolver" "3.186.0"
- "@aws-sdk/credential-provider-node" "3.186.0"
- "@aws-sdk/fetch-http-handler" "3.186.0"
- "@aws-sdk/hash-node" "3.186.0"
- "@aws-sdk/invalid-dependency" "3.186.0"
- "@aws-sdk/middleware-content-length" "3.186.0"
- "@aws-sdk/middleware-host-header" "3.186.0"
- "@aws-sdk/middleware-logger" "3.186.0"
- "@aws-sdk/middleware-recursion-detection" "3.186.0"
- "@aws-sdk/middleware-retry" "3.186.0"
- "@aws-sdk/middleware-serde" "3.186.0"
- "@aws-sdk/middleware-signing" "3.186.0"
- "@aws-sdk/middleware-stack" "3.186.0"
- "@aws-sdk/middleware-user-agent" "3.186.0"
- "@aws-sdk/node-config-provider" "3.186.0"
- "@aws-sdk/node-http-handler" "3.186.0"
- "@aws-sdk/protocol-http" "3.186.0"
- "@aws-sdk/smithy-client" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- "@aws-sdk/url-parser" "3.186.0"
- "@aws-sdk/util-base64-browser" "3.186.0"
- "@aws-sdk/util-base64-node" "3.186.0"
- "@aws-sdk/util-body-length-browser" "3.186.0"
- "@aws-sdk/util-body-length-node" "3.186.0"
- "@aws-sdk/util-defaults-mode-browser" "3.186.0"
- "@aws-sdk/util-defaults-mode-node" "3.186.0"
- "@aws-sdk/util-user-agent-browser" "3.186.0"
- "@aws-sdk/util-user-agent-node" "3.186.0"
- "@aws-sdk/util-utf8-browser" "3.186.0"
- "@aws-sdk/util-utf8-node" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/client-lex-runtime-v2@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/client-lex-runtime-v2/-/client-lex-runtime-v2-3.186.0.tgz#36d153f80e1dbc466c541fd70002d5f9846c9afa"
- integrity sha512-oDN07yCWc9gsEYL44KSjPj8wdHHcf5Kti+w31fE7JHZqvRXxLsLx7G+kEcPmSTRk3Y4wDPXJozL6sDUAOAEb7A==
- dependencies:
- "@aws-crypto/sha256-browser" "2.0.0"
- "@aws-crypto/sha256-js" "2.0.0"
- "@aws-sdk/client-sts" "3.186.0"
- "@aws-sdk/config-resolver" "3.186.0"
- "@aws-sdk/credential-provider-node" "3.186.0"
- "@aws-sdk/eventstream-handler-node" "3.186.0"
- "@aws-sdk/eventstream-serde-browser" "3.186.0"
- "@aws-sdk/eventstream-serde-config-resolver" "3.186.0"
- "@aws-sdk/eventstream-serde-node" "3.186.0"
- "@aws-sdk/fetch-http-handler" "3.186.0"
- "@aws-sdk/hash-node" "3.186.0"
- "@aws-sdk/invalid-dependency" "3.186.0"
- "@aws-sdk/middleware-content-length" "3.186.0"
- "@aws-sdk/middleware-eventstream" "3.186.0"
- "@aws-sdk/middleware-host-header" "3.186.0"
- "@aws-sdk/middleware-logger" "3.186.0"
- "@aws-sdk/middleware-recursion-detection" "3.186.0"
- "@aws-sdk/middleware-retry" "3.186.0"
- "@aws-sdk/middleware-serde" "3.186.0"
- "@aws-sdk/middleware-signing" "3.186.0"
- "@aws-sdk/middleware-stack" "3.186.0"
- "@aws-sdk/middleware-user-agent" "3.186.0"
- "@aws-sdk/node-config-provider" "3.186.0"
- "@aws-sdk/node-http-handler" "3.186.0"
- "@aws-sdk/protocol-http" "3.186.0"
- "@aws-sdk/smithy-client" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- "@aws-sdk/url-parser" "3.186.0"
- "@aws-sdk/util-base64-browser" "3.186.0"
- "@aws-sdk/util-base64-node" "3.186.0"
- "@aws-sdk/util-body-length-browser" "3.186.0"
- "@aws-sdk/util-body-length-node" "3.186.0"
- "@aws-sdk/util-defaults-mode-browser" "3.186.0"
- "@aws-sdk/util-defaults-mode-node" "3.186.0"
- "@aws-sdk/util-user-agent-browser" "3.186.0"
- "@aws-sdk/util-user-agent-node" "3.186.0"
- "@aws-sdk/util-utf8-browser" "3.186.0"
- "@aws-sdk/util-utf8-node" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/client-location@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/client-location/-/client-location-3.186.0.tgz#0801433a1c3fb1fe534771daf67b5d57ffd474f4"
- integrity sha512-RXT1Z7jgYrPEdD1VkErH9Wm+z6y7c/ua1Pu9VQ8weu9vtD15S8Qnyd1m4HS8ZPQUUM/gTxs/fL9+s53wRWpfGQ==
- dependencies:
- "@aws-crypto/sha256-browser" "2.0.0"
- "@aws-crypto/sha256-js" "2.0.0"
- "@aws-sdk/client-sts" "3.186.0"
- "@aws-sdk/config-resolver" "3.186.0"
- "@aws-sdk/credential-provider-node" "3.186.0"
- "@aws-sdk/fetch-http-handler" "3.186.0"
- "@aws-sdk/hash-node" "3.186.0"
- "@aws-sdk/invalid-dependency" "3.186.0"
- "@aws-sdk/middleware-content-length" "3.186.0"
- "@aws-sdk/middleware-host-header" "3.186.0"
- "@aws-sdk/middleware-logger" "3.186.0"
- "@aws-sdk/middleware-recursion-detection" "3.186.0"
- "@aws-sdk/middleware-retry" "3.186.0"
- "@aws-sdk/middleware-serde" "3.186.0"
- "@aws-sdk/middleware-signing" "3.186.0"
- "@aws-sdk/middleware-stack" "3.186.0"
- "@aws-sdk/middleware-user-agent" "3.186.0"
- "@aws-sdk/node-config-provider" "3.186.0"
- "@aws-sdk/node-http-handler" "3.186.0"
- "@aws-sdk/protocol-http" "3.186.0"
- "@aws-sdk/smithy-client" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- "@aws-sdk/url-parser" "3.186.0"
- "@aws-sdk/util-base64-browser" "3.186.0"
- "@aws-sdk/util-base64-node" "3.186.0"
- "@aws-sdk/util-body-length-browser" "3.186.0"
- "@aws-sdk/util-body-length-node" "3.186.0"
- "@aws-sdk/util-defaults-mode-browser" "3.186.0"
- "@aws-sdk/util-defaults-mode-node" "3.186.0"
- "@aws-sdk/util-user-agent-browser" "3.186.0"
- "@aws-sdk/util-user-agent-node" "3.186.0"
- "@aws-sdk/util-utf8-browser" "3.186.0"
- "@aws-sdk/util-utf8-node" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/client-personalize-events@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/client-personalize-events/-/client-personalize-events-3.6.1.tgz#86942bb64108cfc2f6c31a8b54aab6fa7f7be00f"
- integrity sha512-x9Jl/7emSQsB6GwBvjyw5BiSO26CnH4uvjNit6n54yNMtJ26q0+oIxkplnUDyjLTfLRe373c/z5/4dQQtDffkw==
- dependencies:
- "@aws-crypto/sha256-browser" "^1.0.0"
- "@aws-crypto/sha256-js" "^1.0.0"
- "@aws-sdk/config-resolver" "3.6.1"
- "@aws-sdk/credential-provider-node" "3.6.1"
- "@aws-sdk/fetch-http-handler" "3.6.1"
- "@aws-sdk/hash-node" "3.6.1"
- "@aws-sdk/invalid-dependency" "3.6.1"
- "@aws-sdk/middleware-content-length" "3.6.1"
- "@aws-sdk/middleware-host-header" "3.6.1"
- "@aws-sdk/middleware-logger" "3.6.1"
- "@aws-sdk/middleware-retry" "3.6.1"
- "@aws-sdk/middleware-serde" "3.6.1"
- "@aws-sdk/middleware-signing" "3.6.1"
- "@aws-sdk/middleware-stack" "3.6.1"
- "@aws-sdk/middleware-user-agent" "3.6.1"
- "@aws-sdk/node-config-provider" "3.6.1"
- "@aws-sdk/node-http-handler" "3.6.1"
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/smithy-client" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/url-parser" "3.6.1"
- "@aws-sdk/url-parser-native" "3.6.1"
- "@aws-sdk/util-base64-browser" "3.6.1"
- "@aws-sdk/util-base64-node" "3.6.1"
- "@aws-sdk/util-body-length-browser" "3.6.1"
- "@aws-sdk/util-body-length-node" "3.6.1"
- "@aws-sdk/util-user-agent-browser" "3.6.1"
- "@aws-sdk/util-user-agent-node" "3.6.1"
- "@aws-sdk/util-utf8-browser" "3.6.1"
- "@aws-sdk/util-utf8-node" "3.6.1"
- tslib "^2.0.0"
-
-"@aws-sdk/client-pinpoint@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/client-pinpoint/-/client-pinpoint-3.6.1.tgz#6b93f46475ae2667d77053be51ea62f52e330155"
- integrity sha512-dueBedp91EKAHxcWLR3aNx/eUEdxdF9niEQTzOO2O4iJL2yvO2Hh7ZYiO7B3g7FuuICTpWSHd//Y9mGmSVLMCg==
- dependencies:
- "@aws-crypto/sha256-browser" "^1.0.0"
- "@aws-crypto/sha256-js" "^1.0.0"
- "@aws-sdk/config-resolver" "3.6.1"
- "@aws-sdk/credential-provider-node" "3.6.1"
- "@aws-sdk/fetch-http-handler" "3.6.1"
- "@aws-sdk/hash-node" "3.6.1"
- "@aws-sdk/invalid-dependency" "3.6.1"
- "@aws-sdk/middleware-content-length" "3.6.1"
- "@aws-sdk/middleware-host-header" "3.6.1"
- "@aws-sdk/middleware-logger" "3.6.1"
- "@aws-sdk/middleware-retry" "3.6.1"
- "@aws-sdk/middleware-serde" "3.6.1"
- "@aws-sdk/middleware-signing" "3.6.1"
- "@aws-sdk/middleware-stack" "3.6.1"
- "@aws-sdk/middleware-user-agent" "3.6.1"
- "@aws-sdk/node-config-provider" "3.6.1"
- "@aws-sdk/node-http-handler" "3.6.1"
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/smithy-client" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/url-parser" "3.6.1"
- "@aws-sdk/url-parser-native" "3.6.1"
- "@aws-sdk/util-base64-browser" "3.6.1"
- "@aws-sdk/util-base64-node" "3.6.1"
- "@aws-sdk/util-body-length-browser" "3.6.1"
- "@aws-sdk/util-body-length-node" "3.6.1"
- "@aws-sdk/util-user-agent-browser" "3.6.1"
- "@aws-sdk/util-user-agent-node" "3.6.1"
- "@aws-sdk/util-utf8-browser" "3.6.1"
- "@aws-sdk/util-utf8-node" "3.6.1"
- tslib "^2.0.0"
-
-"@aws-sdk/client-polly@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/client-polly/-/client-polly-3.6.1.tgz#869deb186e57fca29737bfa7af094599d7879841"
- integrity sha512-y6fxVYndGS7z2KqHViPCqagBEOsZlxBUYUJZuD6WWTiQrI0Pwe5qG02oKJVaa5OmxE20QLf6bRBWj2rQpeF4IQ==
- dependencies:
- "@aws-crypto/sha256-browser" "^1.0.0"
- "@aws-crypto/sha256-js" "^1.0.0"
- "@aws-sdk/config-resolver" "3.6.1"
- "@aws-sdk/credential-provider-node" "3.6.1"
- "@aws-sdk/fetch-http-handler" "3.6.1"
- "@aws-sdk/hash-node" "3.6.1"
- "@aws-sdk/invalid-dependency" "3.6.1"
- "@aws-sdk/middleware-content-length" "3.6.1"
- "@aws-sdk/middleware-host-header" "3.6.1"
- "@aws-sdk/middleware-logger" "3.6.1"
- "@aws-sdk/middleware-retry" "3.6.1"
- "@aws-sdk/middleware-serde" "3.6.1"
- "@aws-sdk/middleware-signing" "3.6.1"
- "@aws-sdk/middleware-stack" "3.6.1"
- "@aws-sdk/middleware-user-agent" "3.6.1"
- "@aws-sdk/node-config-provider" "3.6.1"
- "@aws-sdk/node-http-handler" "3.6.1"
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/smithy-client" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/url-parser" "3.6.1"
- "@aws-sdk/url-parser-native" "3.6.1"
- "@aws-sdk/util-base64-browser" "3.6.1"
- "@aws-sdk/util-base64-node" "3.6.1"
- "@aws-sdk/util-body-length-browser" "3.6.1"
- "@aws-sdk/util-body-length-node" "3.6.1"
- "@aws-sdk/util-user-agent-browser" "3.6.1"
- "@aws-sdk/util-user-agent-node" "3.6.1"
- "@aws-sdk/util-utf8-browser" "3.6.1"
- "@aws-sdk/util-utf8-node" "3.6.1"
- tslib "^2.0.0"
-
-"@aws-sdk/client-rekognition@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/client-rekognition/-/client-rekognition-3.6.1.tgz#710ba6d4509a2caa417cf0702ba81b5b65aa73eb"
- integrity sha512-Ia4FEog9RrI0IoDRbOJO6djwhVAAaEZutxEKrWbjrVz4bgib28L+V+yAio2SUneeirj8pNYXwBKPfoYOUqGHhA==
- dependencies:
- "@aws-crypto/sha256-browser" "^1.0.0"
- "@aws-crypto/sha256-js" "^1.0.0"
- "@aws-sdk/config-resolver" "3.6.1"
- "@aws-sdk/credential-provider-node" "3.6.1"
- "@aws-sdk/fetch-http-handler" "3.6.1"
- "@aws-sdk/hash-node" "3.6.1"
- "@aws-sdk/invalid-dependency" "3.6.1"
- "@aws-sdk/middleware-content-length" "3.6.1"
- "@aws-sdk/middleware-host-header" "3.6.1"
- "@aws-sdk/middleware-logger" "3.6.1"
- "@aws-sdk/middleware-retry" "3.6.1"
- "@aws-sdk/middleware-serde" "3.6.1"
- "@aws-sdk/middleware-signing" "3.6.1"
- "@aws-sdk/middleware-stack" "3.6.1"
- "@aws-sdk/middleware-user-agent" "3.6.1"
- "@aws-sdk/node-config-provider" "3.6.1"
- "@aws-sdk/node-http-handler" "3.6.1"
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/smithy-client" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/url-parser" "3.6.1"
- "@aws-sdk/url-parser-native" "3.6.1"
- "@aws-sdk/util-base64-browser" "3.6.1"
- "@aws-sdk/util-base64-node" "3.6.1"
- "@aws-sdk/util-body-length-browser" "3.6.1"
- "@aws-sdk/util-body-length-node" "3.6.1"
- "@aws-sdk/util-user-agent-browser" "3.6.1"
- "@aws-sdk/util-user-agent-node" "3.6.1"
- "@aws-sdk/util-utf8-browser" "3.6.1"
- "@aws-sdk/util-utf8-node" "3.6.1"
- "@aws-sdk/util-waiter" "3.6.1"
- tslib "^2.0.0"
-
-"@aws-sdk/client-s3@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.6.1.tgz#aab1e0e92b353d9d51152d9347b7e1809f3593d0"
- integrity sha512-59cTmZj92iwgNoAeJirK5sZNQNXLc/oI3luqrEHRNLuOh70bjdgad70T0a5k2Ysd/v/QNamqJxnCJMPuX1bhgw==
- dependencies:
- "@aws-crypto/sha256-browser" "^1.0.0"
- "@aws-crypto/sha256-js" "^1.0.0"
- "@aws-sdk/config-resolver" "3.6.1"
- "@aws-sdk/credential-provider-node" "3.6.1"
- "@aws-sdk/eventstream-serde-browser" "3.6.1"
- "@aws-sdk/eventstream-serde-config-resolver" "3.6.1"
- "@aws-sdk/eventstream-serde-node" "3.6.1"
- "@aws-sdk/fetch-http-handler" "3.6.1"
- "@aws-sdk/hash-blob-browser" "3.6.1"
- "@aws-sdk/hash-node" "3.6.1"
- "@aws-sdk/hash-stream-node" "3.6.1"
- "@aws-sdk/invalid-dependency" "3.6.1"
- "@aws-sdk/md5-js" "3.6.1"
- "@aws-sdk/middleware-apply-body-checksum" "3.6.1"
- "@aws-sdk/middleware-bucket-endpoint" "3.6.1"
- "@aws-sdk/middleware-content-length" "3.6.1"
- "@aws-sdk/middleware-expect-continue" "3.6.1"
- "@aws-sdk/middleware-host-header" "3.6.1"
- "@aws-sdk/middleware-location-constraint" "3.6.1"
- "@aws-sdk/middleware-logger" "3.6.1"
- "@aws-sdk/middleware-retry" "3.6.1"
- "@aws-sdk/middleware-sdk-s3" "3.6.1"
- "@aws-sdk/middleware-serde" "3.6.1"
- "@aws-sdk/middleware-signing" "3.6.1"
- "@aws-sdk/middleware-ssec" "3.6.1"
- "@aws-sdk/middleware-stack" "3.6.1"
- "@aws-sdk/middleware-user-agent" "3.6.1"
- "@aws-sdk/node-config-provider" "3.6.1"
- "@aws-sdk/node-http-handler" "3.6.1"
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/smithy-client" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/url-parser" "3.6.1"
- "@aws-sdk/url-parser-native" "3.6.1"
- "@aws-sdk/util-base64-browser" "3.6.1"
- "@aws-sdk/util-base64-node" "3.6.1"
- "@aws-sdk/util-body-length-browser" "3.6.1"
- "@aws-sdk/util-body-length-node" "3.6.1"
- "@aws-sdk/util-user-agent-browser" "3.6.1"
- "@aws-sdk/util-user-agent-node" "3.6.1"
- "@aws-sdk/util-utf8-browser" "3.6.1"
- "@aws-sdk/util-utf8-node" "3.6.1"
- "@aws-sdk/util-waiter" "3.6.1"
- "@aws-sdk/xml-builder" "3.6.1"
- fast-xml-parser "^3.16.0"
- tslib "^2.0.0"
-
-"@aws-sdk/client-sso@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.186.0.tgz#233bdd1312dbf88ef9452f8a62c3c3f1ac580330"
- integrity sha512-qwLPomqq+fjvp42izzEpBEtGL2+dIlWH5pUCteV55hTEwHgo+m9LJPIrMWkPeoMBzqbNiu5n6+zihnwYlCIlEA==
- dependencies:
- "@aws-crypto/sha256-browser" "2.0.0"
- "@aws-crypto/sha256-js" "2.0.0"
- "@aws-sdk/config-resolver" "3.186.0"
- "@aws-sdk/fetch-http-handler" "3.186.0"
- "@aws-sdk/hash-node" "3.186.0"
- "@aws-sdk/invalid-dependency" "3.186.0"
- "@aws-sdk/middleware-content-length" "3.186.0"
- "@aws-sdk/middleware-host-header" "3.186.0"
- "@aws-sdk/middleware-logger" "3.186.0"
- "@aws-sdk/middleware-recursion-detection" "3.186.0"
- "@aws-sdk/middleware-retry" "3.186.0"
- "@aws-sdk/middleware-serde" "3.186.0"
- "@aws-sdk/middleware-stack" "3.186.0"
- "@aws-sdk/middleware-user-agent" "3.186.0"
- "@aws-sdk/node-config-provider" "3.186.0"
- "@aws-sdk/node-http-handler" "3.186.0"
- "@aws-sdk/protocol-http" "3.186.0"
- "@aws-sdk/smithy-client" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- "@aws-sdk/url-parser" "3.186.0"
- "@aws-sdk/util-base64-browser" "3.186.0"
- "@aws-sdk/util-base64-node" "3.186.0"
- "@aws-sdk/util-body-length-browser" "3.186.0"
- "@aws-sdk/util-body-length-node" "3.186.0"
- "@aws-sdk/util-defaults-mode-browser" "3.186.0"
- "@aws-sdk/util-defaults-mode-node" "3.186.0"
- "@aws-sdk/util-user-agent-browser" "3.186.0"
- "@aws-sdk/util-user-agent-node" "3.186.0"
- "@aws-sdk/util-utf8-browser" "3.186.0"
- "@aws-sdk/util-utf8-node" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/client-sts@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.186.0.tgz#12514601b0b01f892ddb11d8a2ab4bee1b03cbf1"
- integrity sha512-lyAPI6YmIWWYZHQ9fBZ7QgXjGMTtktL5fk8kOcZ98ja+8Vu0STH1/u837uxqvZta8/k0wijunIL3jWUhjsNRcg==
- dependencies:
- "@aws-crypto/sha256-browser" "2.0.0"
- "@aws-crypto/sha256-js" "2.0.0"
- "@aws-sdk/config-resolver" "3.186.0"
- "@aws-sdk/credential-provider-node" "3.186.0"
- "@aws-sdk/fetch-http-handler" "3.186.0"
- "@aws-sdk/hash-node" "3.186.0"
- "@aws-sdk/invalid-dependency" "3.186.0"
- "@aws-sdk/middleware-content-length" "3.186.0"
- "@aws-sdk/middleware-host-header" "3.186.0"
- "@aws-sdk/middleware-logger" "3.186.0"
- "@aws-sdk/middleware-recursion-detection" "3.186.0"
- "@aws-sdk/middleware-retry" "3.186.0"
- "@aws-sdk/middleware-sdk-sts" "3.186.0"
- "@aws-sdk/middleware-serde" "3.186.0"
- "@aws-sdk/middleware-signing" "3.186.0"
- "@aws-sdk/middleware-stack" "3.186.0"
- "@aws-sdk/middleware-user-agent" "3.186.0"
- "@aws-sdk/node-config-provider" "3.186.0"
- "@aws-sdk/node-http-handler" "3.186.0"
- "@aws-sdk/protocol-http" "3.186.0"
- "@aws-sdk/smithy-client" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- "@aws-sdk/url-parser" "3.186.0"
- "@aws-sdk/util-base64-browser" "3.186.0"
- "@aws-sdk/util-base64-node" "3.186.0"
- "@aws-sdk/util-body-length-browser" "3.186.0"
- "@aws-sdk/util-body-length-node" "3.186.0"
- "@aws-sdk/util-defaults-mode-browser" "3.186.0"
- "@aws-sdk/util-defaults-mode-node" "3.186.0"
- "@aws-sdk/util-user-agent-browser" "3.186.0"
- "@aws-sdk/util-user-agent-node" "3.186.0"
- "@aws-sdk/util-utf8-browser" "3.186.0"
- "@aws-sdk/util-utf8-node" "3.186.0"
- entities "2.2.0"
- fast-xml-parser "3.19.0"
- tslib "^2.3.1"
-
-"@aws-sdk/client-textract@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/client-textract/-/client-textract-3.6.1.tgz#b8972f53f0353222b4c052adc784291e602be6aa"
- integrity sha512-nLrBzWDt3ToiGVFF4lW7a/eZpI2zjdvu7lwmOWyXX8iiPzhBVVEfd5oOorRyJYBsGMslp4sqV8TBkU5Ld/a97Q==
- dependencies:
- "@aws-crypto/sha256-browser" "^1.0.0"
- "@aws-crypto/sha256-js" "^1.0.0"
- "@aws-sdk/config-resolver" "3.6.1"
- "@aws-sdk/credential-provider-node" "3.6.1"
- "@aws-sdk/fetch-http-handler" "3.6.1"
- "@aws-sdk/hash-node" "3.6.1"
- "@aws-sdk/invalid-dependency" "3.6.1"
- "@aws-sdk/middleware-content-length" "3.6.1"
- "@aws-sdk/middleware-host-header" "3.6.1"
- "@aws-sdk/middleware-logger" "3.6.1"
- "@aws-sdk/middleware-retry" "3.6.1"
- "@aws-sdk/middleware-serde" "3.6.1"
- "@aws-sdk/middleware-signing" "3.6.1"
- "@aws-sdk/middleware-stack" "3.6.1"
- "@aws-sdk/middleware-user-agent" "3.6.1"
- "@aws-sdk/node-config-provider" "3.6.1"
- "@aws-sdk/node-http-handler" "3.6.1"
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/smithy-client" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/url-parser" "3.6.1"
- "@aws-sdk/url-parser-native" "3.6.1"
- "@aws-sdk/util-base64-browser" "3.6.1"
- "@aws-sdk/util-base64-node" "3.6.1"
- "@aws-sdk/util-body-length-browser" "3.6.1"
- "@aws-sdk/util-body-length-node" "3.6.1"
- "@aws-sdk/util-user-agent-browser" "3.6.1"
- "@aws-sdk/util-user-agent-node" "3.6.1"
- "@aws-sdk/util-utf8-browser" "3.6.1"
- "@aws-sdk/util-utf8-node" "3.6.1"
- tslib "^2.0.0"
-
-"@aws-sdk/client-translate@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/client-translate/-/client-translate-3.6.1.tgz#ce855c9fe7885b930d4039c2e45c869e3c0a6656"
- integrity sha512-RIHY+Og1i43B5aWlfUUk0ZFnNfM7j2vzlYUwOqhndawV49GFf96M3pmskR5sKEZI+5TXY77qR9TgZ/r3UxVCRQ==
- dependencies:
- "@aws-crypto/sha256-browser" "^1.0.0"
- "@aws-crypto/sha256-js" "^1.0.0"
- "@aws-sdk/config-resolver" "3.6.1"
- "@aws-sdk/credential-provider-node" "3.6.1"
- "@aws-sdk/fetch-http-handler" "3.6.1"
- "@aws-sdk/hash-node" "3.6.1"
- "@aws-sdk/invalid-dependency" "3.6.1"
- "@aws-sdk/middleware-content-length" "3.6.1"
- "@aws-sdk/middleware-host-header" "3.6.1"
- "@aws-sdk/middleware-logger" "3.6.1"
- "@aws-sdk/middleware-retry" "3.6.1"
- "@aws-sdk/middleware-serde" "3.6.1"
- "@aws-sdk/middleware-signing" "3.6.1"
- "@aws-sdk/middleware-stack" "3.6.1"
- "@aws-sdk/middleware-user-agent" "3.6.1"
- "@aws-sdk/node-config-provider" "3.6.1"
- "@aws-sdk/node-http-handler" "3.6.1"
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/smithy-client" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/url-parser" "3.6.1"
- "@aws-sdk/url-parser-native" "3.6.1"
- "@aws-sdk/util-base64-browser" "3.6.1"
- "@aws-sdk/util-base64-node" "3.6.1"
- "@aws-sdk/util-body-length-browser" "3.6.1"
- "@aws-sdk/util-body-length-node" "3.6.1"
- "@aws-sdk/util-user-agent-browser" "3.6.1"
- "@aws-sdk/util-user-agent-node" "3.6.1"
- "@aws-sdk/util-utf8-browser" "3.6.1"
- "@aws-sdk/util-utf8-node" "3.6.1"
- tslib "^2.0.0"
- uuid "^3.0.0"
-
-"@aws-sdk/config-resolver@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/config-resolver/-/config-resolver-3.186.0.tgz#68bbf82b572f03ee3ec9ac84d000147e1050149b"
- integrity sha512-l8DR7Q4grEn1fgo2/KvtIfIHJS33HGKPQnht8OPxkl0dMzOJ0jxjOw/tMbrIcPnr2T3Fi7LLcj3dY1Fo1poruQ==
- dependencies:
- "@aws-sdk/signature-v4" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- "@aws-sdk/util-config-provider" "3.186.0"
- "@aws-sdk/util-middleware" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/config-resolver@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/config-resolver/-/config-resolver-3.6.1.tgz#3bcc5e6a0ebeedf0981b0540e1f18a72b4dafebf"
- integrity sha512-qjP1g3jLIm+XvOIJ4J7VmZRi87vsDmTRzIFePVeG+EFWwYQLxQjTGMdIj3yKTh1WuZ0HByf47mGcpiS4HZLm1Q==
- dependencies:
- "@aws-sdk/signature-v4" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/credential-provider-cognito-identity@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.6.1.tgz#df928951612a34832c2df15fb899251d828c2df3"
- integrity sha512-uJ9q+yq+Dhdo32gcv0p/AT7sKSAUH0y4ts9XRK/vx0dW9Q3XJy99mOJlq/6fkh4LfWeavJJlaCo9lSHNMWXx4w==
- dependencies:
- "@aws-sdk/client-cognito-identity" "3.6.1"
- "@aws-sdk/property-provider" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/credential-provider-env@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.186.0.tgz#55dec9c4c29ebbdff4f3bce72de9e98f7a1f92e1"
- integrity sha512-N9LPAqi1lsQWgxzmU4NPvLPnCN5+IQ3Ai1IFf3wM6FFPNoSUd1kIA2c6xaf0BE7j5Kelm0raZOb4LnV3TBAv+g==
- dependencies:
- "@aws-sdk/property-provider" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/credential-provider-env@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.6.1.tgz#d8b2dd36836432a9b8ec05a5cf9fe428b04c9964"
- integrity sha512-coeFf/HnhpGidcAN1i1NuFgyFB2M6DeN1zNVy4f6s4mAh96ftr9DgWM1CcE3C+cLHEdpNqleVgC/2VQpyzOBLQ==
- dependencies:
- "@aws-sdk/property-provider" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/credential-provider-imds@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.186.0.tgz#73e0f62832726c7734b4f6c50a02ab0d869c00e1"
- integrity sha512-iJeC7KrEgPPAuXjCZ3ExYZrRQvzpSdTZopYgUm5TnNZ8S1NU/4nvv5xVy61JvMj3JQAeG8UDYYgC421Foc8wQw==
- dependencies:
- "@aws-sdk/node-config-provider" "3.186.0"
- "@aws-sdk/property-provider" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- "@aws-sdk/url-parser" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/credential-provider-imds@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.6.1.tgz#b5a8b8ef15eac26c58e469451a6c7c34ab3ca875"
- integrity sha512-bf4LMI418OYcQbyLZRAW8Q5AYM2IKrNqOnIcfrFn2f17ulG7TzoWW3WN/kMOw4TC9+y+vIlCWOv87GxU1yP0Bg==
- dependencies:
- "@aws-sdk/property-provider" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/credential-provider-ini@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.186.0.tgz#3b3873ccae855ee3f6f15dcd8212c5ca4ec01bf3"
- integrity sha512-ecrFh3MoZhAj5P2k/HXo/hMJQ3sfmvlommzXuZ/D1Bj2yMcyWuBhF1A83Fwd2gtYrWRrllsK3IOMM5Jr8UIVZA==
- dependencies:
- "@aws-sdk/credential-provider-env" "3.186.0"
- "@aws-sdk/credential-provider-imds" "3.186.0"
- "@aws-sdk/credential-provider-sso" "3.186.0"
- "@aws-sdk/credential-provider-web-identity" "3.186.0"
- "@aws-sdk/property-provider" "3.186.0"
- "@aws-sdk/shared-ini-file-loader" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/credential-provider-ini@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.6.1.tgz#0da6d9341e621f8e0815814ed017b88e268fbc3d"
- integrity sha512-3jguW6+ttRNddRZvbrs1yb3F1jrUbqyv0UfRoHuOGthjTt+L9sDpJaJGugYnT3bS9WBu1NydLVE2kDV++mJGVw==
- dependencies:
- "@aws-sdk/property-provider" "3.6.1"
- "@aws-sdk/shared-ini-file-loader" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/credential-provider-node@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.186.0.tgz#0be58623660b41eed3a349a89b31a01d4cc773ea"
- integrity sha512-HIt2XhSRhEvVgRxTveLCzIkd/SzEBQfkQ6xMJhkBtfJw1o3+jeCk+VysXM0idqmXytctL0O3g9cvvTHOsUgxOA==
- dependencies:
- "@aws-sdk/credential-provider-env" "3.186.0"
- "@aws-sdk/credential-provider-imds" "3.186.0"
- "@aws-sdk/credential-provider-ini" "3.186.0"
- "@aws-sdk/credential-provider-process" "3.186.0"
- "@aws-sdk/credential-provider-sso" "3.186.0"
- "@aws-sdk/credential-provider-web-identity" "3.186.0"
- "@aws-sdk/property-provider" "3.186.0"
- "@aws-sdk/shared-ini-file-loader" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/credential-provider-node@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.6.1.tgz#0055292a4f0f49d053e8dfcc9174d8d2cf6862bb"
- integrity sha512-VAHOcsqkPrF1k/fA62pv9c75lUWe5bHpcbFX83C3EUPd2FXV10Lfkv6bdWhyZPQy0k8T+9/yikHH3c7ZQeFE5A==
- dependencies:
- "@aws-sdk/credential-provider-env" "3.6.1"
- "@aws-sdk/credential-provider-imds" "3.6.1"
- "@aws-sdk/credential-provider-ini" "3.6.1"
- "@aws-sdk/credential-provider-process" "3.6.1"
- "@aws-sdk/property-provider" "3.6.1"
- "@aws-sdk/shared-ini-file-loader" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/credential-provider-process@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.186.0.tgz#e3be60983261a58c212f5c38b6fb76305bbb8ce7"
- integrity sha512-ATRU6gbXvWC1TLnjOEZugC/PBXHBoZgBADid4fDcEQY1vF5e5Ux1kmqkJxyHtV5Wl8sE2uJfwWn+FlpUHRX67g==
- dependencies:
- "@aws-sdk/property-provider" "3.186.0"
- "@aws-sdk/shared-ini-file-loader" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/credential-provider-process@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.6.1.tgz#5bf851f3ee232c565b8c82608926df0ad28c1958"
- integrity sha512-d0/TpMoEV4qMYkdpyyjU2Otse9X2jC1DuxWajHOWZYEw8oejMvXYTZ10hNaXZvAcNM9q214rp+k4mkt6gIcI6g==
- dependencies:
- "@aws-sdk/credential-provider-ini" "3.6.1"
- "@aws-sdk/property-provider" "3.6.1"
- "@aws-sdk/shared-ini-file-loader" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/credential-provider-sso@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.186.0.tgz#e1aa466543b3b0877d45b885a1c11b329232df22"
- integrity sha512-mJ+IZljgXPx99HCmuLgBVDPLepHrwqnEEC/0wigrLCx6uz3SrAWmGZsNbxSEtb2CFSAaczlTHcU/kIl7XZIyeQ==
- dependencies:
- "@aws-sdk/client-sso" "3.186.0"
- "@aws-sdk/property-provider" "3.186.0"
- "@aws-sdk/shared-ini-file-loader" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/credential-provider-web-identity@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.186.0.tgz#db43f37f7827b553490dd865dbaa9a2c45f95494"
- integrity sha512-KqzI5eBV72FE+8SuOQAu+r53RXGVHg4AuDJmdXyo7Gc4wS/B9FNElA8jVUjjYgVnf0FSiri+l41VzQ44dCopSA==
- dependencies:
- "@aws-sdk/property-provider" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/eventstream-codec@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-codec/-/eventstream-codec-3.186.0.tgz#9da9608866b38179edf72987f2bc3b865d11db13"
- integrity sha512-3kLcJ0/H+zxFlhTlE1SGoFpzd/SitwXOsTSlYVwrwdISKRjooGg0BJpm1CSTkvmWnQIUlYijJvS96TAJ+fCPIA==
- dependencies:
- "@aws-crypto/crc32" "2.0.0"
- "@aws-sdk/types" "3.186.0"
- "@aws-sdk/util-hex-encoding" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/eventstream-handler-node@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.186.0.tgz#d58aec9a8617ed1a9a3800d5526333deb3efebb2"
- integrity sha512-S8eAxCHyFAGSH7F6GHKU2ckpiwFPwJUQwMzewISLg3wzLQeu6lmduxBxVaV3/SoEbEMsbNmrgw9EXtw3Vt/odQ==
- dependencies:
- "@aws-sdk/eventstream-codec" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/eventstream-marshaller@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-marshaller/-/eventstream-marshaller-3.6.1.tgz#6abfbdf3639249d1a77686cbcae5d8e47bcba989"
- integrity sha512-ZvN3Nvxn2Gul08L9MOSN123LwSO0E1gF/CqmOGZtEWzPnoSX/PWM9mhPPeXubyw2KdlXylOodYYw3EAATk3OmA==
- dependencies:
- "@aws-crypto/crc32" "^1.0.0"
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/util-hex-encoding" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/eventstream-serde-browser@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-browser/-/eventstream-serde-browser-3.186.0.tgz#2a0bd942f977b3e2f1a77822ac091ddebe069475"
- integrity sha512-0r2c+yugBdkP5bglGhGOgztjeHdHTKqu2u6bvTByM0nJShNO9YyqWygqPqDUOE5axcYQE1D0aFDGzDtP3mGJhw==
- dependencies:
- "@aws-sdk/eventstream-serde-universal" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/eventstream-serde-browser@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-browser/-/eventstream-serde-browser-3.6.1.tgz#1253bd5215745f79d534fc9bc6bd006ee7a0f239"
- integrity sha512-J8B30d+YUfkBtgWRr7+9AfYiPnbG28zjMlFGsJf8Wxr/hDCfff+Z8NzlBYFEbS7McXXhRiIN8DHUvCtolJtWJQ==
- dependencies:
- "@aws-sdk/eventstream-marshaller" "3.6.1"
- "@aws-sdk/eventstream-serde-universal" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/eventstream-serde-config-resolver@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.186.0.tgz#6c277058bb0fa14752f0b6d7043576e0b5f13da4"
- integrity sha512-xhwCqYrAX5c7fg9COXVw6r7Sa3BO5cCfQMSR5S1QisE7do8K1GDKEHvUCheOx+RLon+P3glLjuNBMdD0HfCVNA==
- dependencies:
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/eventstream-serde-config-resolver@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.6.1.tgz#ebb5c1614f55d0ebb225defac1f76c420e188086"
- integrity sha512-72pCzcT/KeD4gPgRVBSQzEzz4JBim8bNwPwZCGaIYdYAsAI8YMlvp0JNdis3Ov9DFURc87YilWKQlAfw7CDJxA==
- dependencies:
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/eventstream-serde-node@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-node/-/eventstream-serde-node-3.186.0.tgz#dabeab714f447790c5dd31d401c5a3822b795109"
- integrity sha512-9p/gdukJYfmA+OEYd6MfIuufxrrfdt15lBDM3FODuc9j09LSYSRHSxthkIhiM5XYYaaUM+4R0ZlSMdaC3vFDFQ==
- dependencies:
- "@aws-sdk/eventstream-serde-universal" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/eventstream-serde-node@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-node/-/eventstream-serde-node-3.6.1.tgz#705e12bea185905a198d7812af10e3a679dfc841"
- integrity sha512-rjBbJFjCrEcm2NxZctp+eJmyPxKYayG3tQZo8PEAQSViIlK5QexQI3fgqNAeCtK7l/SFAAvnOMRZF6Z3NdUY6A==
- dependencies:
- "@aws-sdk/eventstream-marshaller" "3.6.1"
- "@aws-sdk/eventstream-serde-universal" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/eventstream-serde-universal@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-universal/-/eventstream-serde-universal-3.186.0.tgz#85a88a2cd5c336b1271976fa8db70654ec90fbf4"
- integrity sha512-rIgPmwUxn2tzainBoh+cxAF+b7o01CcW+17yloXmawsi0kiR7QK7v9m/JTGQPWKtHSsPOrtRzuiWQNX57SlcsQ==
- dependencies:
- "@aws-sdk/eventstream-codec" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/eventstream-serde-universal@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/eventstream-serde-universal/-/eventstream-serde-universal-3.6.1.tgz#5be6865adb55436cbc90557df3a3c49b53553470"
- integrity sha512-rpRu97yAGHr9GQLWMzcGICR2PxNu1dHU/MYc9Kb6UgGeZd4fod4o1zjhAJuj98cXn2xwHNFM4wMKua6B4zKrZg==
- dependencies:
- "@aws-sdk/eventstream-marshaller" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/fetch-http-handler@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.186.0.tgz#c1adc5f741e1ba9ad9d3fb13c9c2afdc88530a85"
- integrity sha512-k2v4AAHRD76WnLg7arH94EvIclClo/YfuqO7NoQ6/KwOxjRhs4G6TgIsAZ9E0xmqoJoV81Xqy8H8ldfy9F8LEw==
- dependencies:
- "@aws-sdk/protocol-http" "3.186.0"
- "@aws-sdk/querystring-builder" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- "@aws-sdk/util-base64-browser" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/fetch-http-handler@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.6.1.tgz#c5fb4a4ee158161fca52b220d2c11dddcda9b092"
- integrity sha512-N8l6ZbwhINuWG5hsl625lmIQmVjzsqRPmlgh061jm5D90IhsM5/3A3wUxpB/k0av1dmuMRw/m0YtBU5w4LOwvw==
- dependencies:
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/querystring-builder" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/util-base64-browser" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/hash-blob-browser@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/hash-blob-browser/-/hash-blob-browser-3.6.1.tgz#f44a1857b75769e21cd6091211171135e03531e6"
- integrity sha512-9jPaZ/e3F8gf9JZd44DD6MvbYV6bKnn99rkG3GFIINOy9etoxPrLehp2bH2DK/j0ow60RNuwgUjj5qHV/zF67g==
- dependencies:
- "@aws-sdk/chunked-blob-reader" "3.6.1"
- "@aws-sdk/chunked-blob-reader-native" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/hash-node@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/hash-node/-/hash-node-3.186.0.tgz#8cb13aae8f46eb360fed76baf5062f66f27dfb70"
- integrity sha512-G3zuK8/3KExDTxqrGqko+opOMLRF0BwcwekV/wm3GKIM/NnLhHblBs2zd/yi7VsEoWmuzibfp6uzxgFpEoJ87w==
- dependencies:
- "@aws-sdk/types" "3.186.0"
- "@aws-sdk/util-buffer-from" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/hash-node@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/hash-node/-/hash-node-3.6.1.tgz#72d75ec3b9c7e7f9b0c498805364f1f897165ce9"
- integrity sha512-iKEpzpyaG9PYCnaOGwTIf0lffsF/TpsXrzAfnBlfeOU/3FbgniW2z/yq5xBbtMDtLobtOYC09kUFwDnDvuveSA==
- dependencies:
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/util-buffer-from" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/hash-stream-node@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/hash-stream-node/-/hash-stream-node-3.6.1.tgz#91c77e382ef3d0472160a49b1109395a4a70c801"
- integrity sha512-ePaWjCItIWxuSxA/UnUM/keQ3IAOsQz3FYSxu0KK8K0e1bKTEUgDIG9oMLBq7jIl9TzJG0HBXuPfMe73QHUNug==
- dependencies:
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/invalid-dependency@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/invalid-dependency/-/invalid-dependency-3.186.0.tgz#aa6331ccf404cb659ec38483116080e4b82b0663"
- integrity sha512-hjeZKqORhG2DPWYZ776lQ9YO3gjw166vZHZCZU/43kEYaCZHsF4mexHwHzreAY6RfS25cH60Um7dUh1aeVIpkw==
- dependencies:
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/invalid-dependency@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/invalid-dependency/-/invalid-dependency-3.6.1.tgz#fd2519f5482c6d6113d38a73b7143fd8d5b5b670"
- integrity sha512-d0RLqK7yeDCZJKopnGmGXo2rYkQNE7sGKVmBHQD1j1kKZ9lWwRoJeWqo834JNPZzY5XRvZG5SuIjJ1kFy8LpyQ==
- dependencies:
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/is-array-buffer@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/is-array-buffer/-/is-array-buffer-3.186.0.tgz#7700e36f29d416c2677f4bf8816120f96d87f1b7"
- integrity sha512-fObm+P6mjWYzxoFY4y2STHBmSdgKbIAXez0xope563mox62I8I4hhVPUCaDVydXvDpJv8tbedJMk0meJl22+xA==
- dependencies:
- tslib "^2.3.1"
-
-"@aws-sdk/is-array-buffer@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/is-array-buffer/-/is-array-buffer-3.6.1.tgz#96df5d64b2d599947f81b164d5d92623f85c659c"
- integrity sha512-qm2iDJmCrxlQE2dsFG+TujPe7jw4DF+4RTrsFMhk/e3lOl3MAzQ6Fc2kXtgeUcVrZVFTL8fQvXE1ByYyI6WbCw==
- dependencies:
- tslib "^1.8.0"
-
-"@aws-sdk/md5-js@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/md5-js/-/md5-js-3.6.1.tgz#bffe21106fba0174d73ccc2c29ca1c5364d2af2d"
- integrity sha512-lzCqkZF1sbzGFDyq1dI+lR3AmlE33rbC/JhZ5fzw3hJZvfZ6Beq3Su7YwDo65IWEu0zOKYaNywTeOloXP/CkxQ==
- dependencies:
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/util-utf8-browser" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/middleware-apply-body-checksum@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-apply-body-checksum/-/middleware-apply-body-checksum-3.6.1.tgz#dece86e489531981b8aa2786dafbbef69edce1d6"
- integrity sha512-IncmXR1MPk6aYvmD37It8dP6wVMzaxxzgrkIU2ACkN5UVwA+/0Sr3ZNd9dNwjpyoH1AwpL9BetnlJaWtT6K5ew==
- dependencies:
- "@aws-sdk/is-array-buffer" "3.6.1"
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/middleware-bucket-endpoint@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.6.1.tgz#7ebdd79fac0f78d8af549f4fd799d4f7d02e78de"
- integrity sha512-Frcqn2RQDNHy+e2Q9hv3ejT3mQWtGlfZESbXEF6toR4M0R8MmEVqIB/ohI6VKBj11lRmGwvpPsR6zz+PJ8HS7A==
- dependencies:
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/util-arn-parser" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/middleware-content-length@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-content-length/-/middleware-content-length-3.186.0.tgz#8cc7aeec527738c46fdaf4a48b17c5cbfdc7ce58"
- integrity sha512-Ol3c1ks3IK1s+Okc/rHIX7w2WpXofuQdoAEme37gHeml+8FtUlWH/881h62xfMdf+0YZpRuYv/eM7lBmJBPNJw==
- dependencies:
- "@aws-sdk/protocol-http" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/middleware-content-length@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-content-length/-/middleware-content-length-3.6.1.tgz#f9c00a4045b2b56c1ff8bcbb3dec9c3d42332992"
- integrity sha512-QRcocG9f5YjYzbjs2HjKla6ZIjvx8Y8tm1ZSFOPey81m18CLif1O7M3AtJXvxn+0zeSck9StFdhz5gfjVNYtDg==
- dependencies:
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/middleware-eventstream@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.186.0.tgz#64a66102ed2e182182473948f131f23dda84e729"
- integrity sha512-7yjFiitTGgfKL6cHK3u3HYFnld26IW5aUAFuEd6ocR/FjliysfBd8g0g1bw3bRfIMgCDD8OIOkXK8iCk2iYGWQ==
- dependencies:
- "@aws-sdk/protocol-http" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/middleware-expect-continue@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.6.1.tgz#56e56db572f81dd4fa8803e85bd1f36005f9fffa"
- integrity sha512-vvMOqVYU3uvdJzg/X6NHewZUEBZhSqND1IEcdahLb6RmvDhsS39iS97VZmEFsjj/UFGoePtYjrrdEgRG9Rm1kQ==
- dependencies:
- "@aws-sdk/middleware-header-default" "3.6.1"
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/middleware-header-default@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-header-default/-/middleware-header-default-3.6.1.tgz#a3a108d22cbdd1e1754910625fafb2f2a67fbcfc"
- integrity sha512-YD137iIctXVH8Eut0WOBalvvA+uL0jM0UXZ9N2oKrC8kPQPpqjK9lYGFKZQFsl/XlQHAjJi+gCAFrYsBntRWJQ==
- dependencies:
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/middleware-host-header@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.186.0.tgz#fce4f1219ce1835e2348c787d8341080b0024e34"
- integrity sha512-5bTzrRzP2IGwyF3QCyMGtSXpOOud537x32htZf344IvVjrqZF/P8CDfGTkHkeBCIH+wnJxjK+l/QBb3ypAMIqQ==
- dependencies:
- "@aws-sdk/protocol-http" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/middleware-host-header@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.6.1.tgz#6e1b4b95c5bfea5a4416fa32f11d8fa2e6edaeff"
- integrity sha512-nwq8R2fGBRZQE0Fr/jiOgqfppfiTQCUoD8hyX3qSS7Qc2uqpsDOt2TnnoZl56mpQYkF/344IvMAkp+ew6wR73w==
- dependencies:
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/middleware-location-constraint@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.6.1.tgz#6fc2dd6a42968f011eb060ca564e9f749649eb01"
- integrity sha512-nFisTc0O5D+4I+sRxiiLPasC/I4NDc3s+hgbPPt/b3uAdrujJjhwFBOSaTx8qQvz/xJPAA8pUA/bfWIyeZKi/w==
- dependencies:
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/middleware-logger@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.186.0.tgz#8a027fbbb1b8098ccc888bce51f34b000c0a0550"
- integrity sha512-/1gGBImQT8xYh80pB7QtyzA799TqXtLZYQUohWAsFReYB7fdh5o+mu2rX0FNzZnrLIh2zBUNs4yaWGsnab4uXg==
- dependencies:
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/middleware-logger@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.6.1.tgz#78b3732cf188d5e4df13488db6418f7f98a77d6d"
- integrity sha512-zxaSLpwKlja7JvK20UsDTxPqBZUo3rbDA1uv3VWwpxzOrEWSlVZYx/KLuyGWGkx9V71ZEkf6oOWWJIstS0wyQQ==
- dependencies:
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/middleware-recursion-detection@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.186.0.tgz#9d9d3212e9a954b557840bb80415987f4484487e"
- integrity sha512-Za7k26Kovb4LuV5tmC6wcVILDCt0kwztwSlB991xk4vwNTja8kKxSt53WsYG8Q2wSaW6UOIbSoguZVyxbIY07Q==
- dependencies:
- "@aws-sdk/protocol-http" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/middleware-retry@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-retry/-/middleware-retry-3.186.0.tgz#0ff9af58d73855863683991a809b40b93c753ad1"
- integrity sha512-/VI9emEKhhDzlNv9lQMmkyxx3GjJ8yPfXH3HuAeOgM1wx1BjCTLRYEWnTbQwq7BDzVENdneleCsGAp7yaj80Aw==
- dependencies:
- "@aws-sdk/protocol-http" "3.186.0"
- "@aws-sdk/service-error-classification" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- "@aws-sdk/util-middleware" "3.186.0"
- tslib "^2.3.1"
- uuid "^8.3.2"
-
-"@aws-sdk/middleware-retry@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-retry/-/middleware-retry-3.6.1.tgz#202aadb1a3bf0e1ceabcd8319a5fa308b32db247"
- integrity sha512-WHeo4d2jsXxBP+cec2SeLb0btYXwYXuE56WLmNt0RvJYmiBzytUeGJeRa9HuwV574kgigAuHGCeHlPO36G4Y0Q==
- dependencies:
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/service-error-classification" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- react-native-get-random-values "^1.4.0"
- tslib "^1.8.0"
- uuid "^3.0.0"
-
-"@aws-sdk/middleware-sdk-s3@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.6.1.tgz#371f8991ac82432982153c035ab9450d8df14546"
- integrity sha512-HEA9kynNTsOSIIz8p5GEEAH03pnn+SSohwPl80sGqkmI1yl1tzjqgYZRii0e6acJTh4j9655XFzSx36hYPeB2w==
- dependencies:
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/util-arn-parser" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/middleware-sdk-sts@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.186.0.tgz#18f3d6b7b42c1345b5733ac3e3119d370a403e94"
- integrity sha512-GDcK0O8rjtnd+XRGnxzheq1V2jk4Sj4HtjrxW/ROyhzLOAOyyxutBt+/zOpDD6Gba3qxc69wE+Cf/qngOkEkDw==
- dependencies:
- "@aws-sdk/middleware-signing" "3.186.0"
- "@aws-sdk/property-provider" "3.186.0"
- "@aws-sdk/protocol-http" "3.186.0"
- "@aws-sdk/signature-v4" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/middleware-serde@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-serde/-/middleware-serde-3.186.0.tgz#f7944241ad5fb31cb15cd250c9e92147942b9ec6"
- integrity sha512-6FEAz70RNf18fKL5O7CepPSwTKJEIoyG9zU6p17GzKMgPeFsxS5xO94Hcq5tV2/CqeHliebjqhKY7yi+Pgok7g==
- dependencies:
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/middleware-serde@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-serde/-/middleware-serde-3.6.1.tgz#734c7d16c2aa9ccc01f6cca5e2f6aa2993b6739d"
- integrity sha512-EdQCFZRERfP3uDuWcPNuaa2WUR3qL1WFDXafhcx+7ywQxagdYqBUWKFJlLYi6njbkOKXFM+eHBzoXGF0OV3MJA==
- dependencies:
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/middleware-signing@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.186.0.tgz#37633bf855667b4841464e0044492d0aec5778b9"
- integrity sha512-riCJYG/LlF/rkgVbHkr4xJscc0/sECzDivzTaUmfb9kJhAwGxCyNqnTvg0q6UO00kxSdEB9zNZI2/iJYVBijBQ==
- dependencies:
- "@aws-sdk/property-provider" "3.186.0"
- "@aws-sdk/protocol-http" "3.186.0"
- "@aws-sdk/signature-v4" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- "@aws-sdk/util-middleware" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/middleware-signing@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-signing/-/middleware-signing-3.6.1.tgz#e70a2f35d85d70e33c9fddfb54b9520f6382db16"
- integrity sha512-1woKq+1sU3eausdl8BNdAMRZMkSYuy4mxhLsF0/qAUuLwo1eJLLUCOQp477tICawgu4O4q2OAyUHk7wMqYnQCg==
- dependencies:
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/signature-v4" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/middleware-ssec@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.6.1.tgz#c7dd80e4c1e06be9050c742af7879619b400f0d1"
- integrity sha512-svuH6s91uKUTORt51msiL/ZBjtYSW32c3uVoWxludd/PEf6zO5wCmUEsKoyVwa88L7rrCq+81UBv5A8S5kc3Cw==
- dependencies:
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/middleware-stack@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-stack/-/middleware-stack-3.186.0.tgz#da3445fe74b867ee6d7eec4f0dde28aaca1125d6"
- integrity sha512-fENMoo0pW7UBrbuycPf+3WZ+fcUgP9PnQ0jcOK3WWZlZ9d2ewh4HNxLh4EE3NkNYj4VIUFXtTUuVNHlG8trXjQ==
- dependencies:
- tslib "^2.3.1"
-
-"@aws-sdk/middleware-stack@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-stack/-/middleware-stack-3.6.1.tgz#d7483201706bb5935a62884e9b60f425f1c6434f"
- integrity sha512-EPsIxMi8LtCt7YwTFpWGlVGYJc0q4kwFbOssY02qfqdCnyqi2y5wo089dH7OdxUooQ0D7CPsXM1zTTuzvm+9Fw==
- dependencies:
- tslib "^1.8.0"
-
-"@aws-sdk/middleware-user-agent@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.186.0.tgz#6d881e9cea5fe7517e220f3a47c2f3557c7f27fc"
- integrity sha512-fb+F2PF9DLKOVMgmhkr+ltN8ZhNJavTla9aqmbd01846OLEaN1n5xEnV7p8q5+EznVBWDF38Oz9Ae5BMt3Hs7w==
- dependencies:
- "@aws-sdk/protocol-http" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/middleware-user-agent@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.6.1.tgz#6845dfb3bc6187897f348c2c87dec833e6a65c99"
- integrity sha512-YvXvwllNDVvxQ30vIqLsx+P6jjnfFEQUmhlv64n98gOme6h2BqoyQDcC3yHRGctuxRZEsR7W/H1ASTKC+iabbQ==
- dependencies:
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/node-config-provider@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/node-config-provider/-/node-config-provider-3.186.0.tgz#64259429d39f2ef5a76663162bf2e8db6032a322"
- integrity sha512-De93mgmtuUUeoiKXU8pVHXWKPBfJQlS/lh1k2H9T2Pd9Tzi0l7p5ttddx4BsEx4gk+Pc5flNz+DeptiSjZpa4A==
- dependencies:
- "@aws-sdk/property-provider" "3.186.0"
- "@aws-sdk/shared-ini-file-loader" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/node-config-provider@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/node-config-provider/-/node-config-provider-3.6.1.tgz#cb85d06329347fde566f08426f8714b1f65d2fb7"
- integrity sha512-x2Z7lm0ZhHYqMybvkaI5hDKfBkaLaXhTDfgrLl9TmBZ3QHO4fIHgeL82VZ90Paol+OS+jdq2AheLmzbSxv3HrA==
- dependencies:
- "@aws-sdk/property-provider" "3.6.1"
- "@aws-sdk/shared-ini-file-loader" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/node-http-handler@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/node-http-handler/-/node-http-handler-3.186.0.tgz#8be1598a9187637a767dc337bf22fe01461e86eb"
- integrity sha512-CbkbDuPZT9UNJ4dAZJWB3BV+Z65wFy7OduqGkzNNrKq6ZYMUfehthhUOTk8vU6RMe/0FkN+J0fFXlBx/bs/cHw==
- dependencies:
- "@aws-sdk/abort-controller" "3.186.0"
- "@aws-sdk/protocol-http" "3.186.0"
- "@aws-sdk/querystring-builder" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/node-http-handler@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/node-http-handler/-/node-http-handler-3.6.1.tgz#4b65c4dcc0cf46ba44cb6c3bf29c5f817bb8d9a7"
- integrity sha512-6XSaoqbm9ZF6T4UdBCcs/Gn2XclwBotkdjj46AxO+9vRAgZDP+lH/8WwZsvfqJhhRhS0qxWrks98WGJwmaTG8g==
- dependencies:
- "@aws-sdk/abort-controller" "3.6.1"
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/querystring-builder" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/property-provider@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/property-provider/-/property-provider-3.186.0.tgz#af41e615662a2749d3ff7da78c41f79f4be95b3b"
- integrity sha512-nWKqt36UW3xV23RlHUmat+yevw9up+T+953nfjcmCBKtgWlCWu/aUzewTRhKj3VRscbN+Wer95SBw9Lr/MMOlQ==
- dependencies:
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/property-provider@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/property-provider/-/property-provider-3.6.1.tgz#d973fc87d199d32c44d947e17f2ee2dd140a9593"
- integrity sha512-2gR2DzDySXKFoj9iXLm1TZBVSvFIikEPJsbRmAZx5RBY+tp1IXWqZM6PESjaLdLg/ZtR0QhW2ZcRn0fyq2JfnQ==
- dependencies:
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/protocol-http@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/protocol-http/-/protocol-http-3.186.0.tgz#99115870846312dd4202b5e2cc68fe39324b9bfa"
- integrity sha512-l/KYr/UBDUU5ginqTgHtFfHR3X6ljf/1J1ThIiUg3C3kVC/Zwztm7BEOw8hHRWnWQGU/jYasGYcrcPLdQqFZyQ==
- dependencies:
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/protocol-http@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/protocol-http/-/protocol-http-3.6.1.tgz#d3d276846bec19ddb339d06bbc48116d17bbc656"
- integrity sha512-WkQz7ncVYTLvCidDfXWouDzqxgSNPZDz3Bql+7VhZeITnzAEcr4hNMyEqMAVYBVugGmkG2W6YiUqNNs1goOcDA==
- dependencies:
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/querystring-builder@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-builder/-/querystring-builder-3.186.0.tgz#a380db0e1c71004932d9e2f3e6dc6761d1165c47"
- integrity sha512-mweCpuLufImxfq/rRBTEpjGuB4xhQvbokA+otjnUxlPdIobytLqEs7pCGQfLzQ7+1ZMo8LBXt70RH4A2nSX/JQ==
- dependencies:
- "@aws-sdk/types" "3.186.0"
- "@aws-sdk/util-uri-escape" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/querystring-builder@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-builder/-/querystring-builder-3.6.1.tgz#4c769829a3760ef065d0d3801f297a7f0cd324d4"
- integrity sha512-ESe255Yl6vB1AMNqaGSQow3TBYYnpw0AFjE40q2VyiNrkbaqKmW2EzjeCy3wEmB1IfJDHy3O12ZOMUMOnjFT8g==
- dependencies:
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/util-uri-escape" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/querystring-parser@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-parser/-/querystring-parser-3.186.0.tgz#4db6d31ad4df0d45baa2a35e371fbaa23e45ddd2"
- integrity sha512-0iYfEloghzPVXJjmnzHamNx1F1jIiTW9Svy5ZF9LVqyr/uHZcQuiWYsuhWloBMLs8mfWarkZM02WfxZ8buAuhg==
- dependencies:
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/querystring-parser@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/querystring-parser/-/querystring-parser-3.6.1.tgz#e3fa5a710429c7dd411e802a0b82beb48012cce2"
- integrity sha512-hh6dhqamKrWWaDSuO2YULci0RGwJWygoy8hpCRxs/FpzzHIcbm6Cl6Jhrn5eKBzOBv+PhCcYwbfad0kIZZovcQ==
- dependencies:
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/s3-request-presigner@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.6.1.tgz#ec83c70171692862a7f7ebbd151242a5af443695"
- integrity sha512-OI7UHCKBwuiO/RmHHewBKnL2NYqdilXRmpX67TJ4tTszIrWP2+vpm3lIfrx/BM8nf8nKTzgkO98uFhoJsEhmTg==
- dependencies:
- "@aws-sdk/protocol-http" "3.6.1"
- "@aws-sdk/signature-v4" "3.6.1"
- "@aws-sdk/smithy-client" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/util-create-request" "3.6.1"
- "@aws-sdk/util-format-url" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/service-error-classification@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/service-error-classification/-/service-error-classification-3.186.0.tgz#6e4e1d4b53d68bd28c28d9cf0b3b4cb6a6a59dbb"
- integrity sha512-DRl3ORk4tF+jmH5uvftlfaq0IeKKpt0UPAOAFQ/JFWe+TjOcQd/K+VC0iiIG97YFp3aeFmH1JbEgsNxd+8fdxw==
-
-"@aws-sdk/service-error-classification@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/service-error-classification/-/service-error-classification-3.6.1.tgz#296fe62ac61338341e8a009c9a2dab013a791903"
- integrity sha512-kZ7ZhbrN1f+vrSRkTJvXsu7BlOyZgym058nPA745+1RZ1Rtv4Ax8oknf2RvJyj/1qRUi8LBaAREjzQ3C8tmLBA==
-
-"@aws-sdk/shared-ini-file-loader@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.186.0.tgz#a2d285bb3c4f8d69f7bfbde7a5868740cd3f7795"
- integrity sha512-2FZqxmICtwN9CYid4dwfJSz/gGFHyStFQ3HCOQ8DsJUf2yREMSBsVmKqsyWgOrYcQ98gPcD5GIa7QO5yl3XF6A==
- dependencies:
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/shared-ini-file-loader@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.6.1.tgz#2b7182cbb0d632ad7c9712bebffdeee24a6f7eb6"
- integrity sha512-BnLHtsNLOoow6rPV+QVi6jnovU5g1m0YzoUG0BQYZ1ALyVlWVr0VvlUX30gMDfdYoPMp+DHvF8GXdMuGINq6kQ==
- dependencies:
- tslib "^1.8.0"
-
-"@aws-sdk/signature-v4@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4/-/signature-v4-3.186.0.tgz#bbd56e71af95548abaeec6307ea1dfe7bd26b4e4"
- integrity sha512-18i96P5c4suMqwSNhnEOqhq4doqqyjH4fn0YV3F8TkekHPIWP4mtIJ0PWAN4eievqdtcKgD/GqVO6FaJG9texw==
- dependencies:
- "@aws-sdk/is-array-buffer" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- "@aws-sdk/util-hex-encoding" "3.186.0"
- "@aws-sdk/util-middleware" "3.186.0"
- "@aws-sdk/util-uri-escape" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/signature-v4@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4/-/signature-v4-3.6.1.tgz#b20a3cf3e891131f83b012651f7d4af2bf240611"
- integrity sha512-EAR0qGVL4AgzodZv4t+BSuBfyOXhTNxDxom50IFI1MqidR9vI6avNZKcPHhgXbm7XVcsDGThZKbzQ2q7MZ2NTA==
- dependencies:
- "@aws-sdk/is-array-buffer" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- "@aws-sdk/util-hex-encoding" "3.6.1"
- "@aws-sdk/util-uri-escape" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/smithy-client@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/smithy-client/-/smithy-client-3.186.0.tgz#67514544fb55d7eff46300e1e73311625cf6f916"
- integrity sha512-rdAxSFGSnrSprVJ6i1BXi65r4X14cuya6fYe8dSdgmFSa+U2ZevT97lb3tSINCUxBGeMXhENIzbVGkRZuMh+DQ==
- dependencies:
- "@aws-sdk/middleware-stack" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/smithy-client@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/smithy-client/-/smithy-client-3.6.1.tgz#683fef89802e318922f8529a5433592d71a7ce9d"
- integrity sha512-AVpRK4/iUxNeDdAm8UqP0ZgtgJMQeWcagTylijwelhWXyXzHUReY1sgILsWcdWnoy6gq845W7K2VBhBleni8+w==
- dependencies:
- "@aws-sdk/middleware-stack" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/types@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.186.0.tgz#f6fb6997b6a364f399288bfd5cd494bc680ac922"
- integrity sha512-NatmSU37U+XauMFJCdFI6nougC20JUFZar+ump5wVv0i54H+2Refg1YbFDxSs0FY28TSB9jfhWIpfFBmXgL5MQ==
-
-"@aws-sdk/types@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.6.1.tgz#00686db69e998b521fcd4a5f81ef0960980f80c4"
- integrity sha512-4Dx3eRTrUHLxhFdLJL8zdNGzVsJfAxtxPYYGmIddUkO2Gj3WA1TGjdfG4XN/ClI6e1XonCHafQX3UYO/mgnH3g==
-
-"@aws-sdk/types@^1.0.0-alpha.0":
- version "1.0.0-rc.10"
- resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-1.0.0-rc.10.tgz#729127fbfac5da1a3368ffe6ec2e90acc9ad69c3"
- integrity sha512-9gwhYnkTNuYZ+etCtM4T8gjpZ0SWSXbzQxY34UjSS+dt3C/UnbX0J22tMahp/9Z1yCa9pihtXrkD+nO2xn7nVQ==
-
-"@aws-sdk/types@^3.1.0", "@aws-sdk/types@^3.110.0":
- version "3.329.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.329.0.tgz#bc20659abfcd666954196c3a24ad47785db80dd3"
- integrity sha512-wFBW4yciDfzQBSFmWNaEvHShnSGLMxSu9Lls6EUf6xDMavxSB36bsrVRX6CyAo/W0NeIIyEOW1LclGPgJV1okg==
- dependencies:
- tslib "^2.5.0"
-
-"@aws-sdk/url-parser-native@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser-native/-/url-parser-native-3.6.1.tgz#a5e787f98aafa777e73007f9490df334ef3389a2"
- integrity sha512-3O+ktsrJoE8YQCho9L41YXO8EWILXrSeES7amUaV3mgIV5w4S3SB/r4RkmylpqRpQF7Ry8LFiAnMqH1wa4WBPA==
- dependencies:
- "@aws-sdk/querystring-parser" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
- url "^0.11.0"
-
-"@aws-sdk/url-parser@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser/-/url-parser-3.186.0.tgz#e42f845cd405c1920fdbdcc796a350d4ace16ae9"
- integrity sha512-jfdJkKqJZp8qjjwEjIGDqbqTuajBsddw02f86WiL8bPqD8W13/hdqbG4Fpwc+Bm6GwR6/4MY6xWXFnk8jDUKeA==
- dependencies:
- "@aws-sdk/querystring-parser" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/url-parser@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/url-parser/-/url-parser-3.6.1.tgz#f5d89fb21680469a61cb9fe08a7da3ef887884dd"
- integrity sha512-pWFIePDx0PMCleQRsQDWoDl17YiijOLj0ZobN39rQt+wv5PhLSZDz9PgJsqS48nZ6hqsKgipRcjiBMhn5NtFcQ==
- dependencies:
- "@aws-sdk/querystring-parser" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/util-arn-parser@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.6.1.tgz#aa60b1bfa752ad3fa331f22fea4f703b741d1d6d"
- integrity sha512-NFdYeuhaSrgnBG6Pt3zHNU7QwvhHq6sKUTWZShUayLMJYYbQr6IjmYVlPST4c84b+lyDoK68y/Zga621VfIdBg==
- dependencies:
- tslib "^1.8.0"
-
-"@aws-sdk/util-base64-browser@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-base64-browser/-/util-base64-browser-3.186.0.tgz#0310482752163fa819718ce9ea9250836b20346d"
- integrity sha512-TpQL8opoFfzTwUDxKeon/vuc83kGXpYqjl6hR8WzmHoQgmFfdFlV+0KXZOohra1001OP3FhqvMqaYbO8p9vXVQ==
- dependencies:
- tslib "^2.3.1"
-
-"@aws-sdk/util-base64-browser@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-base64-browser/-/util-base64-browser-3.6.1.tgz#eddea1311b41037fc3fddd889d3e0a9882363215"
- integrity sha512-+DHAIgt0AFARDVC7J0Z9FkSmJhBMlkYdOPeAAgO0WaQoKj7rtsLQJ7P3v3aS1paKN5/sk5xNY7ziVB6uHtOvHA==
- dependencies:
- tslib "^1.8.0"
-
-"@aws-sdk/util-base64-node@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-base64-node/-/util-base64-node-3.186.0.tgz#500bd04b1ef7a6a5c0a2d11c0957a415922e05c7"
- integrity sha512-wH5Y/EQNBfGS4VkkmiMyZXU+Ak6VCoFM1GKWopV+sj03zR2D4FHexi4SxWwEBMpZCd6foMtihhbNBuPA5fnh6w==
- dependencies:
- "@aws-sdk/util-buffer-from" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/util-base64-node@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-base64-node/-/util-base64-node-3.6.1.tgz#a79c233861e50d3a30728c72b736afdee07d4009"
- integrity sha512-oiqzpsvtTSS92+cL3ykhGd7t3qBJKeHvrgOwUyEf1wFWHQ2DPJR+dIMy5rMFRXWLKCl3w7IddY2rJCkLYMjaqQ==
- dependencies:
- "@aws-sdk/util-buffer-from" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/util-body-length-browser@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.186.0.tgz#a898eda9f874f6974a9c5c60fcc76bcb6beac820"
- integrity sha512-zKtjkI/dkj9oGkjo+7fIz+I9KuHrVt1ROAeL4OmDESS8UZi3/O8uMDFMuCp8jft6H+WFuYH6qRVWAVwXMiasXw==
- dependencies:
- tslib "^2.3.1"
-
-"@aws-sdk/util-body-length-browser@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.6.1.tgz#2e8088f2d9a5a8258b4f56079a8890f538c2797e"
- integrity sha512-IdWwE3rm/CFDk2F+IwTZOFTnnNW5SB8y1lWiQ54cfc7y03hO6jmXNnpZGZ5goHhT+vf1oheNQt1J47m0pM/Irw==
- dependencies:
- tslib "^1.8.0"
-
-"@aws-sdk/util-body-length-node@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-body-length-node/-/util-body-length-node-3.186.0.tgz#95efbacbd13cb739b942c126c5d16ecf6712d4db"
- integrity sha512-U7Ii8u8Wvu9EnBWKKeuwkdrWto3c0j7LG677Spe6vtwWkvY70n9WGfiKHTgBpVeLNv8jvfcx5+H0UOPQK1o9SQ==
- dependencies:
- tslib "^2.3.1"
-
-"@aws-sdk/util-body-length-node@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-body-length-node/-/util-body-length-node-3.6.1.tgz#6e4f2eae46c5a7b0417a12ca7f4b54c390d4cacd"
- integrity sha512-CUG3gc18bSOsqViQhB3M4AlLpAWV47RE6yWJ6rLD0J6/rSuzbwbjzxM39q0YTAVuSo/ivdbij+G9c3QCirC+QQ==
- dependencies:
- tslib "^1.8.0"
-
-"@aws-sdk/util-buffer-from@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-buffer-from/-/util-buffer-from-3.186.0.tgz#01f7edb683d2f40374d0ca8ef2d16346dc8040a1"
- integrity sha512-be2GCk2lsLWg/2V5Y+S4/9pOMXhOQo4DR4dIqBdR2R+jrMMHN9Xsr5QrkT6chcqLaJ/SBlwiAEEi3StMRmCOXA==
- dependencies:
- "@aws-sdk/is-array-buffer" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/util-buffer-from@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-buffer-from/-/util-buffer-from-3.6.1.tgz#24184ce74512f764d84002201b7f5101565e26f9"
- integrity sha512-OGUh2B5NY4h7iRabqeZ+EgsrzE1LUmNFzMyhoZv0tO4NExyfQjxIYXLQQvydeOq9DJUbCw+yrRZrj8vXNDQG+g==
- dependencies:
- "@aws-sdk/is-array-buffer" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/util-config-provider@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-config-provider/-/util-config-provider-3.186.0.tgz#52ce3711edceadfac1b75fccc7c615e90c33fb2f"
- integrity sha512-71Qwu/PN02XsRLApyxG0EUy/NxWh/CXxtl2C7qY14t+KTiRapwbDkdJ1cMsqYqghYP4BwJoj1M+EFMQSSlkZQQ==
- dependencies:
- tslib "^2.3.1"
-
-"@aws-sdk/util-create-request@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-create-request/-/util-create-request-3.6.1.tgz#ecc4364551c7b3d0d9834ca3f56528fb8b083838"
- integrity sha512-jR1U8WpwXl+xZ9ThS42Jr5MXuegQ7QioHsZjQn3V5pbm8CXTkBF0B2BcULQu/2G1XtHOJb8qUZQlk/REoaORfQ==
- dependencies:
- "@aws-sdk/middleware-stack" "3.6.1"
- "@aws-sdk/smithy-client" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/util-defaults-mode-browser@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.186.0.tgz#d30b2f572e273d7d98287274c37c9ee00b493507"
- integrity sha512-U8GOfIdQ0dZ7RRVpPynGteAHx4URtEh+JfWHHVfS6xLPthPHWTbyRhkQX++K/F8Jk+T5U8Anrrqlea4TlcO2DA==
- dependencies:
- "@aws-sdk/property-provider" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- bowser "^2.11.0"
- tslib "^2.3.1"
-
-"@aws-sdk/util-defaults-mode-node@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.186.0.tgz#8572453ba910fd2ab08d2cfee130ce5a0db83ba7"
- integrity sha512-N6O5bpwCiE4z8y7SPHd7KYlszmNOYREa+mMgtOIXRU3VXSEHVKVWTZsHKvNTTHpW0qMqtgIvjvXCo3vsch5l3A==
- dependencies:
- "@aws-sdk/config-resolver" "3.186.0"
- "@aws-sdk/credential-provider-imds" "3.186.0"
- "@aws-sdk/node-config-provider" "3.186.0"
- "@aws-sdk/property-provider" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/util-format-url@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-format-url/-/util-format-url-3.6.1.tgz#a011444aed0c47698d65095bcce95d7b4716324b"
- integrity sha512-FvhcXcqLyJ0j0WdlmGs7PtjCCv8NaY4zBuXYO2iwAmqoy2SIZXQL63uAvmilqWj25q47ASAsUwSFLReCCfMklQ==
- dependencies:
- "@aws-sdk/querystring-builder" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/util-hex-encoding@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.186.0.tgz#7ed58b923997c6265f4dce60c8704237edb98895"
- integrity sha512-UL9rdgIZz1E/jpAfaKH8QgUxNK9VP5JPgoR0bSiaefMjnsoBh0x/VVMsfUyziOoJCMLebhJzFowtwrSKEGsxNg==
- dependencies:
- tslib "^2.3.1"
-
-"@aws-sdk/util-hex-encoding@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.6.1.tgz#84954fcc47b74ffbd2911ba5113e93bd9b1c6510"
- integrity sha512-pzsGOHtU2eGca4NJgFg94lLaeXDOg8pcS9sVt4f9LmtUGbrqRveeyBv0XlkHeZW2n0IZBssPHipVYQFlk7iaRA==
- dependencies:
- tslib "^1.8.0"
-
-"@aws-sdk/util-locate-window@^3.0.0":
- version "3.310.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-locate-window/-/util-locate-window-3.310.0.tgz#b071baf050301adee89051032bd4139bba32cc40"
- integrity sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w==
- dependencies:
- tslib "^2.5.0"
-
-"@aws-sdk/util-middleware@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-middleware/-/util-middleware-3.186.0.tgz#ba2e286b206cbead306b6d2564f9d0495f384b40"
- integrity sha512-fddwDgXtnHyL9mEZ4s1tBBsKnVQHqTUmFbZKUUKPrg9CxOh0Y/zZxEa5Olg/8dS/LzM1tvg0ATkcyd4/kEHIhg==
- dependencies:
- tslib "^2.3.1"
-
-"@aws-sdk/util-uri-escape@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-uri-escape/-/util-uri-escape-3.186.0.tgz#1752a93dfe58ec88196edb6929806807fd8986da"
- integrity sha512-imtOrJFpIZAipAg8VmRqYwv1G/x4xzyoxOJ48ZSn1/ZGnKEEnB6n6E9gwYRebi4mlRuMSVeZwCPLq0ey5hReeQ==
- dependencies:
- tslib "^2.3.1"
-
-"@aws-sdk/util-uri-escape@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-uri-escape/-/util-uri-escape-3.6.1.tgz#433e87458bb510d0e457a86c0acf12b046a5068c"
- integrity sha512-tgABiT71r0ScRJZ1pMX0xO0QPMMiISCtumph50IU5VDyZWYgeIxqkMhIcrL1lX0QbNCMgX0n6rZxGrrbjDNavA==
- dependencies:
- tslib "^1.8.0"
-
-"@aws-sdk/util-user-agent-browser@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.186.0.tgz#02e214887d30a69176c6a6c2d6903ce774b013b4"
- integrity sha512-fbRcTTutMk4YXY3A2LePI4jWSIeHOT8DaYavpc/9Xshz/WH9RTGMmokeVOcClRNBeDSi5cELPJJ7gx6SFD3ZlQ==
- dependencies:
- "@aws-sdk/types" "3.186.0"
- bowser "^2.11.0"
- tslib "^2.3.1"
-
-"@aws-sdk/util-user-agent-browser@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.6.1.tgz#11b9cc8743392761adb304460f4b54ec8acc2ee6"
- integrity sha512-KhJ4VED4QpuBVPXoTjb5LqspX1xHWJTuL8hbPrKfxj+cAaRRW2CNEe7PPy2CfuHtPzP3dU3urtGTachbwNb0jg==
- dependencies:
- "@aws-sdk/types" "3.6.1"
- bowser "^2.11.0"
- tslib "^1.8.0"
-
-"@aws-sdk/util-user-agent-node@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.186.0.tgz#1ef74973442c8650c7b64ff2fd15cf3c09d8c004"
- integrity sha512-oWZR7hN6NtOgnT6fUvHaafgbipQc2xJCRB93XHiF9aZGptGNLJzznIOP7uURdn0bTnF73ejbUXWLQIm8/6ue6w==
- dependencies:
- "@aws-sdk/node-config-provider" "3.186.0"
- "@aws-sdk/types" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/util-user-agent-node@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.6.1.tgz#98384095fa67d098ae7dd26f3ccaad028e8aebb6"
- integrity sha512-PWwL5EDRwhkXX40m5jjgttlBmLA7vDhHBen1Jcle0RPIDFRVPSE7GgvLF3y4r3SNH0WD6hxqadT50bHQynXW6w==
- dependencies:
- "@aws-sdk/node-config-provider" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/util-utf8-browser@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.186.0.tgz#5fee6385cfc3effa2be704edc2998abfd6633082"
- integrity sha512-n+IdFYF/4qT2WxhMOCeig8LndDggaYHw3BJJtfIBZRiS16lgwcGYvOUmhCkn0aSlG1f/eyg9YZHQG0iz9eLdHQ==
- dependencies:
- tslib "^2.3.1"
-
-"@aws-sdk/util-utf8-browser@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.6.1.tgz#97a8770cae9d29218adc0f32c7798350261377c7"
- integrity sha512-gZPySY6JU5gswnw3nGOEHl3tYE7vPKvtXGYoS2NRabfDKRejFvu+4/nNW6SSpoOxk6LSXsrWB39NO51k+G4PVA==
- dependencies:
- tslib "^1.8.0"
-
-"@aws-sdk/util-utf8-browser@^1.0.0-alpha.0":
- version "1.0.0-rc.8"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-1.0.0-rc.8.tgz#bf1f1cfed8c024f43a7c43b643fdf2b4523b5973"
- integrity sha512-clncPMJ23rxCIkZ9LoUC8SowwZGxWyN2TwRb0XvW/Cv9EavkRgRCOrCpneGyC326lqtMKx36onnpaSRHxErUYw==
- dependencies:
- tslib "^1.8.0"
-
-"@aws-sdk/util-utf8-browser@^3.0.0":
- version "3.259.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz#3275a6f5eb334f96ca76635b961d3c50259fd9ff"
- integrity sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==
- dependencies:
- tslib "^2.3.1"
-
-"@aws-sdk/util-utf8-node@3.186.0":
- version "3.186.0"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-node/-/util-utf8-node-3.186.0.tgz#722d9b0f5675ae2e9d79cf67322126d9c9d8d3d8"
- integrity sha512-7qlE0dOVdjuRbZTb7HFywnHHCrsN7AeQiTnsWT63mjXGDbPeUWQQw3TrdI20um3cxZXnKoeudGq8K6zbXyQ4iA==
- dependencies:
- "@aws-sdk/util-buffer-from" "3.186.0"
- tslib "^2.3.1"
-
-"@aws-sdk/util-utf8-node@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-utf8-node/-/util-utf8-node-3.6.1.tgz#18534c2069b61f5739ee4cdc70060c9f4b4c4c4f"
- integrity sha512-4s0vYfMUn74XLn13rUUhNsmuPMh0j1d4rF58wXtjlVUU78THxonnN8mbCLC48fI3fKDHTmDDkeEqy7+IWP9VyA==
- dependencies:
- "@aws-sdk/util-buffer-from" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/util-waiter@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/util-waiter/-/util-waiter-3.6.1.tgz#5c66c2da33ff98468726fefddc2ca7ac3352c17d"
- integrity sha512-CQMRteoxW1XZSzPBVrTsOTnfzsEGs8N/xZ8BuBnXLBjoIQmRKVxIH9lgphm1ohCtVHoSWf28XH/KoOPFULQ4Tg==
- dependencies:
- "@aws-sdk/abort-controller" "3.6.1"
- "@aws-sdk/types" "3.6.1"
- tslib "^1.8.0"
-
-"@aws-sdk/xml-builder@3.6.1":
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.6.1.tgz#d85d7db5e8e30ba74de93ddf0cf6197e6e4b15ea"
- integrity sha512-+HOCH4a0XO+I09okd0xdVP5Q5c9ZsEsDvnogiOcBQxoMivWhPUCo9pjXP3buCvVKP2oDHXQplBKSjGHvGaKFdg==
- dependencies:
- tslib "^1.8.0"
-
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.21.4", "@babel/code-frame@^7.8.3":
version "7.21.4"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.21.4.tgz#d0fa9e4413aca81f2b23b9442797bda1826edb39"
@@ -4226,26 +2123,6 @@
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
-"@turf/boolean-clockwise@6.5.0":
- version "6.5.0"
- resolved "https://registry.yarnpkg.com/@turf/boolean-clockwise/-/boolean-clockwise-6.5.0.tgz#34573ecc18f900080f00e4ff364631a8b1135794"
- integrity sha512-45+C7LC5RMbRWrxh3Z0Eihsc8db1VGBO5d9BLTOAwU4jR6SgsunTfRWR16X7JUwIDYlCVEmnjcXJNi/kIU3VIw==
- dependencies:
- "@turf/helpers" "^6.5.0"
- "@turf/invariant" "^6.5.0"
-
-"@turf/helpers@^6.5.0":
- version "6.5.0"
- resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-6.5.0.tgz#f79af094bd6b8ce7ed2bd3e089a8493ee6cae82e"
- integrity sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==
-
-"@turf/invariant@^6.5.0":
- version "6.5.0"
- resolved "https://registry.yarnpkg.com/@turf/invariant/-/invariant-6.5.0.tgz#970afc988023e39c7ccab2341bd06979ddc7463f"
- integrity sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==
- dependencies:
- "@turf/helpers" "^6.5.0"
-
"@types/aria-query@^5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc"
@@ -4314,11 +2191,6 @@
dependencies:
"@types/node" "*"
-"@types/cookie@^0.3.3":
- version "0.3.3"
- resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803"
- integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==
-
"@types/eslint-scope@^3.7.3":
version "3.7.4"
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
@@ -5001,33 +2873,6 @@ ajv@^8.0.0, ajv@^8.6.0, ajv@^8.9.0:
require-from-string "^2.0.2"
uri-js "^4.2.2"
-amazon-cognito-auth-js@^1.3.3:
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/amazon-cognito-auth-js/-/amazon-cognito-auth-js-1.3.3.tgz#8278a76091300094615a4552b02453e9b970bf9c"
- integrity sha512-Auyv8chr5Vw5p1FVAnczWHaXtiDX9jhlmPzB5viSqjLRDDs2UZlTA9aQAC7gaSvwx1b1INJaXvIOgm5Ctm91Nw==
- dependencies:
- js-cookie "^2.1.4"
-
-amazon-cognito-identity-js@5.2.14:
- version "5.2.14"
- resolved "https://registry.yarnpkg.com/amazon-cognito-identity-js/-/amazon-cognito-identity-js-5.2.14.tgz#780d633e2912971e77b00d60d5b959f26d66e6a5"
- integrity sha512-9LMgLZfbypbbGTpARQ+QqglE09b1MWti11NXhcD/wPom0uhU/L90dfmUOpTwknz//eE6/dGYf004mJucWzrfxQ==
- dependencies:
- buffer "4.9.2"
- crypto-js "^4.1.1"
- fast-base64-decode "^1.0.0"
- isomorphic-unfetch "^3.0.0"
- js-cookie "^2.2.1"
-
-amazon-cognito-identity-js@^3.2.7:
- version "3.3.3"
- resolved "https://registry.yarnpkg.com/amazon-cognito-identity-js/-/amazon-cognito-identity-js-3.3.3.tgz#3c10a91def29b998d0974bf7ce63a262a6d4dd89"
- integrity sha512-uB1Bk2ezxVUz0vELZ4tI40ZJEYEZZcWdz8TVyNOPjQCKS+SszNUORTkOkL0KgawZMak7KhDfLTEXbInBeTsiow==
- dependencies:
- buffer "4.9.1"
- crypto-js "^3.1.9-1"
- js-cookie "^2.1.4"
-
ansi-escapes@^4.2.1, ansi-escapes@^4.3.0, ansi-escapes@^4.3.1:
version "4.3.2"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
@@ -5300,33 +3145,6 @@ available-typed-arrays@^1.0.5:
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
-aws-amplify-react@^5.1.9:
- version "5.1.43"
- resolved "https://registry.yarnpkg.com/aws-amplify-react/-/aws-amplify-react-5.1.43.tgz#aa07b30da2e061fe55959252fc95a5a6726c1a01"
- integrity sha512-dhetRbT1qLXrH6p30J8VwVz0t/zzMyMopb/ciMSxhuukfgGo8/8dsiIYgABdrvoZojwhlY24M7RHPAMIPva+Vg==
- dependencies:
- qrcode.react "^0.8.0"
- regenerator-runtime "^0.11.1"
-
-aws-amplify@^4.3.16:
- version "4.3.46"
- resolved "https://registry.yarnpkg.com/aws-amplify/-/aws-amplify-4.3.46.tgz#e8897d97796fa5475ed75f37bb590fc9cc4cf013"
- integrity sha512-LygkBq+mrV+hFf3DCrVcyYNxFsiYwL0HLN89X1Eg+s3f7df6T2xpjh4JuaDJFbmodEdAlZNfdtRGLMk6ApnPcA==
- dependencies:
- "@aws-amplify/analytics" "5.2.31"
- "@aws-amplify/api" "4.0.64"
- "@aws-amplify/auth" "4.6.17"
- "@aws-amplify/cache" "4.0.66"
- "@aws-amplify/core" "4.7.15"
- "@aws-amplify/datastore" "3.14.7"
- "@aws-amplify/geo" "1.3.27"
- "@aws-amplify/interactions" "4.1.12"
- "@aws-amplify/predictions" "4.0.64"
- "@aws-amplify/pubsub" "4.5.14"
- "@aws-amplify/storage" "4.5.17"
- "@aws-amplify/ui" "2.0.7"
- "@aws-amplify/xr" "3.0.64"
-
aws-sdk@^2.649.0:
version "2.1379.0"
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1379.0.tgz#2d5609a37287d25edda4395fbb89306939f98890"
@@ -5358,13 +3176,6 @@ axe-core@^4.6.2:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.1.tgz#04392c9ccb3d7d7c5d2f8684f148d56d3442f33d"
integrity sha512-sCXXUhA+cljomZ3ZAwb8i1p3oOlkABzPy08ZDAoGcYuvtBPlQ1Ytde129ArXyHWDhfeewq7rlx9F+cUx2SSlkg==
-axios@0.26.0:
- version "0.26.0"
- resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.0.tgz#9a318f1c69ec108f8cd5f3c3d390366635e13928"
- integrity sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==
- dependencies:
- follow-redirects "^1.14.8"
-
axios@^0.21.1:
version "0.21.4"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
@@ -5550,11 +3361,6 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
-base-64@1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/base-64/-/base-64-1.0.0.tgz#09d0f2084e32a3fd08c2475b973788eee6ae8f4a"
- integrity sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==
-
base64-js@^1.0.2:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@@ -5635,11 +3441,6 @@ bootstrap@^5.1.3:
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.2.3.tgz#54739f4414de121b9785c5da3c87b37ff008322b"
integrity sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ==
-bowser@^2.11.0:
- version "2.11.0"
- resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f"
- integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==
-
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -5689,15 +3490,6 @@ buffer-from@^1.0.0:
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
-buffer@4.9.1:
- version "4.9.1"
- resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298"
- integrity sha512-DNK4ruAqtyHaN8Zne7PkBTO+dD1Lr0YfTduMqlIyjvQIoztBkUxrvL+hKeLW8NXFKHOq/2upkxuoS9znQ9bW9A==
- dependencies:
- base64-js "^1.0.2"
- ieee754 "^1.1.4"
- isarray "^1.0.0"
-
buffer@4.9.2:
version "4.9.2"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
@@ -5772,7 +3564,7 @@ camelcase-css@^2.0.1:
resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
-camelcase-keys@6.2.2, camelcase-keys@^6.2.2:
+camelcase-keys@^6.2.2:
version "6.2.2"
resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0"
integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==
@@ -6136,11 +3928,6 @@ cookie@0.5.0:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
-cookie@^0.4.0:
- version "0.4.2"
- resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
- integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
-
copy-anything@^2.0.1:
version "2.0.6"
resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480"
@@ -6216,11 +4003,6 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3:
shebang-command "^2.0.0"
which "^2.0.1"
-crypto-js@^3.1.9-1:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.3.0.tgz#846dd1cce2f68aacfa156c8578f926a609b7976b"
- integrity sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==
-
crypto-js@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf"
@@ -6865,7 +4647,7 @@ enhanced-resolve@^5.14.0:
graceful-fs "^4.2.4"
tapable "^2.2.0"
-entities@2.2.0, entities@^2.0.0:
+entities@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
@@ -7301,7 +5083,7 @@ events@1.1.1:
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==
-events@^3.1.0, events@^3.2.0:
+events@^3.2.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
@@ -7403,11 +5185,6 @@ extsprintf@^1.2.0:
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07"
integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==
-fast-base64-decode@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz#b434a0dd7d92b12b43f26819300d2dafb83ee418"
- integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==
-
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@@ -7434,18 +5211,6 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
-fast-xml-parser@3.19.0:
- version "3.19.0"
- resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz#cb637ec3f3999f51406dd8ff0e6fc4d83e520d01"
- integrity sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg==
-
-fast-xml-parser@^3.16.0:
- version "3.21.1"
- resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz#152a1d51d445380f7046b304672dd55d15c9e736"
- integrity sha512-FTFVjYoBOZTJekiUsawGsSYV9QL0A+zDYCRj7y34IO6Jg+2IMYEtQa+bbictpdpV8dHxXywqU7C0gRDEOFtBFg==
- dependencies:
- strnum "^1.0.4"
-
fastq@^1.6.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a"
@@ -7467,11 +5232,6 @@ fb-watchman@^2.0.0:
dependencies:
bser "2.1.1"
-fflate@0.7.3:
- version "0.7.3"
- resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.7.3.tgz#288b034ff0e9c380eaa2feff48c787b8371b7fa5"
- integrity sha512-0Zz1jOzJWERhyhsimS54VTqOteCNwRtIlh8isdL0AXLo0g7xNTfTL7oWrkmCnPhZGocKIkWHBistBrrpoNH3aw==
-
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@@ -7581,7 +5341,7 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
-follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.8:
+follow-redirects@^1.0.0, follow-redirects@^1.14.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
@@ -7971,11 +5731,6 @@ grapheme-splitter@^1.0.4:
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
-graphql@15.8.0:
- version "15.8.0"
- resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38"
- integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==
-
gzip-size@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462"
@@ -8303,11 +6058,6 @@ icss-utils@^5.0.0, icss-utils@^5.1.0:
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
-idb@5.0.6:
- version "5.0.6"
- resolved "https://registry.yarnpkg.com/idb/-/idb-5.0.6.tgz#8c94624f5a8a026abe3bef3c7166a5febd1cadc1"
- integrity sha512-/PFvOWPzRcEPmlDt5jEvzVZVs0wyd/EvGvkDIcbBpGuMMLQKrTPG0TxvE2UJtgZtCQCmOtM2QD7yQJBVEjKGOw==
-
idb@^7.0.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b"
@@ -8340,11 +6090,6 @@ image-size@~0.5.0:
resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==
-immer@9.0.6:
- version "9.0.6"
- resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73"
- integrity sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==
-
immer@^9.0.21, immer@^9.0.7:
version "9.0.21"
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176"
@@ -8716,14 +6461,6 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
-isomorphic-unfetch@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f"
- integrity sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==
- dependencies:
- node-fetch "^2.6.1"
- unfetch "^4.2.0"
-
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@@ -9308,11 +7045,6 @@ js-base64@^2.4.9:
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4"
integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==
-js-cookie@^2.1.4, js-cookie@^2.2.1:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
- integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==
-
js-sdsl@^4.1.4:
version "4.4.0"
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.0.tgz#8b437dbe642daa95760400b602378ed8ffea8430"
@@ -10080,13 +7812,6 @@ no-case@^3.0.4:
lower-case "^2.0.2"
tslib "^2.0.3"
-node-fetch@^2.6.1:
- version "2.6.11"
- resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25"
- integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==
- dependencies:
- whatwg-url "^5.0.0"
-
node-forge@^1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
@@ -10462,16 +8187,6 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
-paho-mqtt@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/paho-mqtt/-/paho-mqtt-1.1.0.tgz#8c10e29eb162e966fb15188d965c3dce505de9d9"
- integrity sha512-KPbL9KAB0ASvhSDbOrZBaccXS+/s7/LIofbPyERww8hM5Ko71GUJQ6Nmg0BWqj8phAIT8zdf/Sd/RftHU9i2HA==
-
-pako@2.0.4:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/pako/-/pako-2.0.4.tgz#6cebc4bbb0b6c73b0d5b8d7e8476e2b2fbea576d"
- integrity sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg==
-
param-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5"
@@ -11295,7 +9010,7 @@ prop-types-extra@^1.1.0:
react-is "^16.3.2"
warning "^4.0.0"
-prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
+prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -11342,19 +9057,6 @@ q@^1.1.2:
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==
-qr.js@0.0.0:
- version "0.0.0"
- resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f"
- integrity sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==
-
-qrcode.react@^0.8.0:
- version "0.8.0"
- resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-0.8.0.tgz#413b31cc3b62910e39513f7bead945e01c4c34fb"
- integrity sha512-16wKpuFvLwciIq2YAsfmPUCnSR8GrYPsXRK5KVdcIuX0+W/MKZbBkFhl44ttRx4TWZHqRjfztoWOxdPF0Hb9JA==
- dependencies:
- prop-types "^15.6.0"
- qr.js "0.0.0"
-
qs@6.11.0:
version "6.11.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
@@ -11622,13 +9324,6 @@ react-moment@^1.1.1:
resolved "https://registry.yarnpkg.com/react-moment/-/react-moment-1.1.3.tgz#829b21dfb279aa6db47ce4f1ac2555af17a1bcdc"
integrity sha512-8EPvlUL8u6EknPp1ISF5MQ3wx2OHJVXIP/iZc4wRh3iV3XozftZERDv9ANZeAtMlhNNQHdFoqcZHFUkBSTONfA==
-react-native-get-random-values@^1.4.0:
- version "1.8.0"
- resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.8.0.tgz#1cb4bd4bd3966a356e59697b8f372999fe97cb16"
- integrity sha512-H/zghhun0T+UIJLmig3+ZuBCvF66rdbiWUfRSNS6kv5oDSpa1ZiVyvRWtuPesQpT8dXj+Bv7WJRQOUP+5TB1sA==
- dependencies:
- fast-base64-decode "^1.0.0"
-
react-oidc-context@^2.2.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/react-oidc-context/-/react-oidc-context-2.2.2.tgz#aa9b66596b63f1373d312007099c32b112646396"
@@ -11687,7 +9382,7 @@ react-router@5.3.4:
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
-react-scripts@^5.0.1:
+react-scripts@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.1.tgz#6285dbd65a8ba6e49ca8d651ce30645a6d980003"
integrity sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==
@@ -11883,11 +9578,6 @@ regenerate@^1.4.2:
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
-regenerator-runtime@^0.11.1:
- version "0.11.1"
- resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
- integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
-
regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.9:
version "0.13.11"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
@@ -12835,11 +10525,6 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
-strnum@^1.0.4:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db"
- integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==
-
style-loader@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.2.tgz#eaebca714d9e462c19aa1e3599057bc363924899"
@@ -13173,11 +10858,6 @@ tr46@^2.1.0:
dependencies:
punycode "^2.1.1"
-tr46@~0.0.3:
- version "0.0.3"
- resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
- integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
-
trim-newlines@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
@@ -13210,12 +10890,12 @@ tsconfig-paths@^3.14.1:
minimist "^1.2.6"
strip-bom "^3.0.0"
-tslib@^1.10.0, tslib@^1.11.1, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.3:
+tslib@^1.10.0, tslib@^1.8.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.5.0:
+tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
@@ -13317,11 +10997,6 @@ typescript@^3.8.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8"
integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==
-ulid@2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/ulid/-/ulid-2.3.0.tgz#93063522771a9774121a84d126ecd3eb9804071f"
- integrity sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==
-
unbox-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
@@ -13342,11 +11017,6 @@ uncontrollable@^7.2.1:
invariant "^2.2.4"
react-lifecycles-compat "^3.0.4"
-unfetch@^4.2.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be"
- integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==
-
unicode-canonical-property-names-ecmascript@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"
@@ -13391,14 +11061,6 @@ unique-string@^2.0.0:
dependencies:
crypto-random-string "^2.0.0"
-universal-cookie@^4.0.4:
- version "4.0.4"
- resolved "https://registry.yarnpkg.com/universal-cookie/-/universal-cookie-4.0.4.tgz#06e8b3625bf9af049569ef97109b4bb226ad798d"
- integrity sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==
- dependencies:
- "@types/cookie" "^0.3.3"
- cookie "^0.4.0"
-
universalify@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"
@@ -13460,14 +11122,6 @@ url@0.10.3:
punycode "1.3.2"
querystring "0.2.0"
-url@^0.11.0:
- version "0.11.0"
- resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
- integrity sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==
- dependencies:
- punycode "1.3.2"
- querystring "0.2.0"
-
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -13504,17 +11158,12 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
-uuid@3.3.2:
- version "3.3.2"
- resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
- integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
-
uuid@8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c"
integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==
-uuid@^3.0.0, uuid@^3.2.1, uuid@^3.3.2:
+uuid@^3.3.2:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
@@ -13608,11 +11257,6 @@ wbuf@^1.1.0, wbuf@^1.7.3:
dependencies:
minimalistic-assert "^1.0.0"
-webidl-conversions@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
- integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
-
webidl-conversions@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
@@ -13765,14 +11409,6 @@ whatwg-mimetype@^2.3.0:
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
-whatwg-url@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
- integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
- dependencies:
- tr46 "~0.0.3"
- webidl-conversions "^3.0.0"
-
whatwg-url@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
@@ -14163,28 +11799,3 @@ yup@^0.32.11:
nanoclone "^0.2.1"
property-expr "^2.0.4"
toposort "^2.0.2"
-
-zen-observable-ts@0.8.19:
- version "0.8.19"
- resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.19.tgz#c094cd20e83ddb02a11144a6e2a89706946b5694"
- integrity sha512-u1a2rpE13G+jSzrg3aiCqXU5tN2kw41b+cBZGmnc+30YimdkKiDj9bTowcB41eL77/17RF/h+393AuVgShyheQ==
- dependencies:
- tslib "^1.9.3"
- zen-observable "^0.8.0"
-
-zen-observable@^0.7.0:
- version "0.7.1"
- resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.7.1.tgz#f84075c0ee085594d3566e1d6454207f126411b3"
- integrity sha512-OI6VMSe0yeqaouIXtedC+F55Sr6r9ppS7+wTbSexkYdHbdt4ctTuPNXP/rwm7GTVI63YBc+EBT0b0tl7YnJLRg==
-
-zen-observable@^0.8.0:
- version "0.8.15"
- resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15"
- integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==
-
-zen-push@0.2.1:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/zen-push/-/zen-push-0.2.1.tgz#ddc33b90f66f9a84237d5f1893970f6be60c3c28"
- integrity sha512-Qv4qvc8ZIue51B/0zmeIMxpIGDVhz4GhJALBvnKs/FRa2T7jy4Ori9wFwaHVt0zWV7MIFglKAHbgnVxVTw7U1w==
- dependencies:
- zen-observable "^0.7.0"
diff --git a/functions/authorizer/pom.xml b/functions/authorizer/pom.xml
index b3069d1b..7219b9c3 100644
--- a/functions/authorizer/pom.xml
+++ b/functions/authorizer/pom.xml
@@ -33,6 +33,7 @@ limitations under the License.
+ ${project.basedir}/../..0
@@ -53,25 +54,38 @@ limitations under the License.
+
+ org.jacoco
+ jacoco-maven-plugin
+ org.apache.maven.pluginsmaven-assembly-plugin
- pl.project13.maven
- git-commit-id-plugin
+ io.github.git-commit-id
+ git-commit-id-maven-plugin
+
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+
+ Max
+ medium
+
+
+ software.amazon.lambda.snapstart
+ aws-lambda-snapstart-java-rules
+ 0.1.0
+
+
+ ${project.basedir}/src/main/resources/spotbugs-exclude.xml
+
-
- com.amazon.aws.partners.saasfactory.saasboost
- Utils
- 1.0.0
-
- provided
- com.fasterxml.jackson.corejackson-annotations
diff --git a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayAuthorizer.java b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayAuthorizer.java
index ce203a99..998ee3ef 100644
--- a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayAuthorizer.java
+++ b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayAuthorizer.java
@@ -18,6 +18,7 @@
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
+import com.auth0.jwt.interfaces.DecodedJWT;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.regions.Region;
@@ -27,13 +28,20 @@
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
+import java.util.*;
public class ApiGatewayAuthorizer implements RequestStreamHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(ApiGatewayAuthorizer.class);
private static final String AWS_REGION = System.getenv("AWS_REGION");
+ private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV");
private static final String IDENTITY_PROVIDER = System.getenv("IDENTITY_PROVIDER");
+ static final String ADMIN_WEB_APP_CLIENT_ID = System.getenv("ADMIN_WEB_APP_CLIENT_ID");
+ static final String API_APP_CLIENT_ID = System.getenv("API_APP_CLIENT_ID");
+ static final String PRIVATE_API_APP_CLIENT_ID = System.getenv("PRIVATE_API_APP_CLIENT_ID");
+ static final String READ_SCOPE = "saas-boost/" + SAAS_BOOST_ENV + "/read";
+ static final String WRITE_SCOPE = "saas-boost/" + SAAS_BOOST_ENV + "/write";
+ static final String PRIVATE_SCOPE = "saas-boost/" + SAAS_BOOST_ENV + "/private";
private final Authorizer authorizer;
public ApiGatewayAuthorizer() {
@@ -63,7 +71,8 @@ public void handleRequest(InputStream input, OutputStream output, Context contex
LOGGER.info(Utils.toJson(event));
AuthorizerResponse response;
- if (!authorizer.verifyToken(event)) {
+ DecodedJWT token = authorizer.verifyToken(event);
+ if (token == null) {
LOGGER.error("JWT not verified. Returning Not Authorized");
response = AuthorizerResponse.builder()
.principalId(event.getAccountId())
@@ -79,12 +88,39 @@ public void handleRequest(InputStream input, OutputStream output, Context contex
.build();
} else {
LOGGER.info("JWT verified. Returning Authorized.");
+ LOGGER.debug(Utils.toJson(token));
+
+ List resources = new ArrayList<>();
+ String scopes = token.getClaim("scope").asString();
+ if (ADMIN_WEB_APP_CLIENT_ID.equals(authorizer.getClientId(token))) {
+ List groups = authorizer.getGroups(token);
+ if (groups != null && groups.contains("admin")) {
+ LOGGER.debug("Token includes admin group, adding read/write scopes");
+ scopes = scopes + " " + READ_SCOPE + " " + WRITE_SCOPE;
+ } else {
+ LOGGER.error("Admin web app client does not contain RBAC groups!");
+ }
+ }
+ //LOGGER.info("Access token scopes {}", scopes);
+ if (scopes.contains(READ_SCOPE)) {
+ LOGGER.info("Adding READ scope resources");
+ resources.addAll(readApiResources(event));
+ }
+ if (scopes.contains(WRITE_SCOPE)) {
+ LOGGER.info("Adding WRITE scope resources");
+ resources.addAll(writeApiResources(event));
+ }
+ if (scopes.contains(PRIVATE_SCOPE)) {
+ LOGGER.info("Adding PRIVATE scope resources");
+ resources.addAll(privateApiResources(event));
+ }
+
response = AuthorizerResponse.builder()
.principalId(event.getAccountId())
.policyDocument(PolicyDocument.builder()
.statement(Statement.builder()
.effect("Allow")
- .resource(ApiGatewayAuthorizer.apiGatewayResource(event))
+ .resource(resources)
.build()
)
.build()
@@ -108,9 +144,8 @@ public static String apiGatewayResource(TokenAuthorizerRequest event) {
}
public static String apiGatewayResource(TokenAuthorizerRequest event, String method, String resource) {
- String partition = Region.of(AWS_REGION).metadata().partition().id();
String arn = String.format("arn:%s:execute-api:%s:%s:%s/%s/%s/%s",
- partition,
+ Region.of(event.getRegion()).metadata().partition().id(),
event.getRegion(),
event.getAccountId(),
event.getApiId(),
@@ -120,4 +155,76 @@ public static String apiGatewayResource(TokenAuthorizerRequest event, String met
);
return arn;
}
+
+ public static List readApiResources(TokenAuthorizerRequest event) {
+ Set> readResources = new LinkedHashSet<>();
+ readResources.add(new AbstractMap.SimpleEntry<>("api", "GET"));
+ readResources.add(new AbstractMap.SimpleEntry<>("api/*", "GET"));
+ readResources.add(new AbstractMap.SimpleEntry<>("billing/plans", "GET"));
+ //readResources.add(new AbstractMap.SimpleEntry<>("metrics/alb/*", "GET"));
+ //readResources.add(new AbstractMap.SimpleEntry<>("metrics/datasets", "GET"));
+ //readResources.add(new AbstractMap.SimpleEntry<>("metrics/query", "POST")); // Yes, this is a "read" resource
+ readResources.add(new AbstractMap.SimpleEntry<>("onboarding", "GET"));
+ readResources.add(new AbstractMap.SimpleEntry<>("onboarding/*", "GET"));
+ readResources.add(new AbstractMap.SimpleEntry<>("settings", "GET"));
+ readResources.add(new AbstractMap.SimpleEntry<>("settings/*", "GET"));
+ readResources.add(new AbstractMap.SimpleEntry<>("appconfig", "GET"));
+ readResources.add(new AbstractMap.SimpleEntry<>("appconfig/*", "GET"));
+ readResources.add(new AbstractMap.SimpleEntry<>("appconfig/options", "GET"));
+ readResources.add(new AbstractMap.SimpleEntry<>("sysusers", "GET"));
+ readResources.add(new AbstractMap.SimpleEntry<>("sysusers/*", "GET"));
+ readResources.add(new AbstractMap.SimpleEntry<>("tenants", "GET"));
+ readResources.add(new AbstractMap.SimpleEntry<>("tenants/*", "GET"));
+ readResources.add(new AbstractMap.SimpleEntry<>("tiers", "GET"));
+ readResources.add(new AbstractMap.SimpleEntry<>("tiers/*", "GET"));
+ readResources.add(new AbstractMap.SimpleEntry<>("identity", "GET"));
+ readResources.add(new AbstractMap.SimpleEntry<>("identity/providers", "GET"));
+
+ List resources = new ArrayList<>();
+ for (Map.Entry resource : readResources) {
+ resources.add(apiGatewayResource(event, resource.getValue(), resource.getKey()));
+ }
+ return resources;
+ }
+
+ public static List writeApiResources(TokenAuthorizerRequest event) {
+ Set> writeResources = new LinkedHashSet<>();
+ writeResources.add(new AbstractMap.SimpleEntry<>("onboarding*", "POST"));
+ writeResources.add(new AbstractMap.SimpleEntry<>("appconfig*", "PUT"));
+ writeResources.add(new AbstractMap.SimpleEntry<>("sysusers*", "POST"));
+ writeResources.add(new AbstractMap.SimpleEntry<>("sysusers/*", "DELETE"));
+ writeResources.add(new AbstractMap.SimpleEntry<>("sysusers/*", "PUT"));
+ writeResources.add(new AbstractMap.SimpleEntry<>("sysusers/*/disable", "PATCH"));
+ writeResources.add(new AbstractMap.SimpleEntry<>("sysusers/*/enable", "PATCH"));
+ writeResources.add(new AbstractMap.SimpleEntry<>("tenants/*", "DELETE"));
+ writeResources.add(new AbstractMap.SimpleEntry<>("tenants/*", "PUT"));
+ writeResources.add(new AbstractMap.SimpleEntry<>("tenants/*/disable", "PATCH"));
+ writeResources.add(new AbstractMap.SimpleEntry<>("tenants/*/enable", "PATCH"));
+ writeResources.add(new AbstractMap.SimpleEntry<>("tiers*", "POST"));
+ writeResources.add(new AbstractMap.SimpleEntry<>("tiers/*", "PUT"));
+ writeResources.add(new AbstractMap.SimpleEntry<>("tiers/*", "DELETE"));
+ writeResources.add(new AbstractMap.SimpleEntry<>("identity*", "POST"));
+ writeResources.add(new AbstractMap.SimpleEntry<>("metrics*", "POST"));
+ writeResources.add(new AbstractMap.SimpleEntry<>("settings/*", "PUT"));
+
+ List resources = new ArrayList<>();
+ for (Map.Entry resource : writeResources) {
+ resources.add(apiGatewayResource(event, resource.getValue(), resource.getKey()));
+ }
+ return resources;
+ }
+
+ public static List privateApiResources(TokenAuthorizerRequest event) {
+ Set> privateResources = new LinkedHashSet<>();
+ privateResources.add(new AbstractMap.SimpleEntry<>("quotas/check", "GET"));
+ privateResources.add(new AbstractMap.SimpleEntry<>("appconfig", "DELETE"));
+ privateResources.add(new AbstractMap.SimpleEntry<>("settings/*/secret", "GET"));
+ privateResources.add(new AbstractMap.SimpleEntry<>("tenants*", "POST"));
+
+ List resources = new ArrayList<>();
+ for (Map.Entry resource : privateResources) {
+ resources.add(apiGatewayResource(event, resource.getValue(), resource.getKey()));
+ }
+ return resources;
+ }
}
\ No newline at end of file
diff --git a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Authorizer.java b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Authorizer.java
index 40749957..59413f5a 100644
--- a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Authorizer.java
+++ b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Authorizer.java
@@ -16,7 +16,15 @@
package com.amazon.aws.partners.saasfactory.saasboost;
+import com.auth0.jwt.interfaces.DecodedJWT;
+
+import java.util.List;
+
public interface Authorizer {
- boolean verifyToken(TokenAuthorizerRequest request);
+ DecodedJWT verifyToken(TokenAuthorizerRequest request);
+
+ String getClientId(DecodedJWT token);
+
+ List getGroups(DecodedJWT token);
}
diff --git a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CognitoAuthorizer.java b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CognitoAuthorizer.java
index 9f1b3470..c745b29c 100644
--- a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CognitoAuthorizer.java
+++ b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CognitoAuthorizer.java
@@ -19,33 +19,46 @@
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
+import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.List;
+
public class CognitoAuthorizer implements Authorizer {
private static final Logger LOGGER = LoggerFactory.getLogger(CognitoAuthorizer.class);
@Override
- public boolean verifyToken(TokenAuthorizerRequest request) {
- // TODO add aud claim and pass client id in to the Lambda as an env variable
+ public DecodedJWT verifyToken(TokenAuthorizerRequest request) {
JWTVerifier verifier = JWT
.require(Algorithm.RSA256(new CognitoKeyProvider()))
.acceptLeeway(5L) // Allowed seconds of clock skew between token issuer and verifier
- .withClaim("token_use", (claim, token) -> (
- // Per Cognito documentation, make sure we got an Access or Identity token
- // (not a refresh token)
- "access".equals(claim.asString()) || "id".equals(claim.asString()))
+ .withClaim("token_use", "access")
+ .withClaim("client_id", (claim, token) -> (
+ List.of(ApiGatewayAuthorizer.ADMIN_WEB_APP_CLIENT_ID,
+ ApiGatewayAuthorizer.API_APP_CLIENT_ID,
+ ApiGatewayAuthorizer.PRIVATE_API_APP_CLIENT_ID).contains(claim.asString())
+ )
)
.build();
- boolean valid = false;
+ DecodedJWT token = null;
try {
- verifier.verify(request.tokenPayload());
- valid = true;
+ token = verifier.verify(request.tokenPayload());
} catch (JWTVerificationException e) {
LOGGER.error(Utils.getFullStackTrace(e));
}
- return valid;
+ return token;
+ }
+
+ @Override
+ public String getClientId(DecodedJWT token) {
+ return token.getClaim("client_id").asString();
+ }
+
+ @Override
+ public List getGroups(DecodedJWT token) {
+ return token.getClaim("cognito:groups").asList(String.class);
}
}
diff --git a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/KeycloakAuthorizer.java b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/KeycloakAuthorizer.java
index 36154cac..27c54097 100644
--- a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/KeycloakAuthorizer.java
+++ b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/KeycloakAuthorizer.java
@@ -19,27 +19,47 @@
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
+import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.List;
+
public class KeycloakAuthorizer implements Authorizer {
private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAuthorizer.class);
@Override
- public boolean verifyToken(TokenAuthorizerRequest request) {
+ public DecodedJWT verifyToken(TokenAuthorizerRequest request) {
JWTVerifier verifier = JWT
.require(Algorithm.RSA256(new KeycloakKeyProvider()))
.acceptLeeway(5L) // Allowed seconds of clock skew between token issuer and verifier
+ .withClaim("typ", "Bearer")
+ .withClaim("azp", (claim, token) -> (
+ List.of(ApiGatewayAuthorizer.ADMIN_WEB_APP_CLIENT_ID,
+ ApiGatewayAuthorizer.API_APP_CLIENT_ID,
+ ApiGatewayAuthorizer.PRIVATE_API_APP_CLIENT_ID).contains(claim.asString())
+ )
+ )
.build();
- boolean valid = false;
+ DecodedJWT token = null;
try {
- verifier.verify(request.tokenPayload());
- valid = true;
+ token = verifier.verify(request.tokenPayload());
} catch (JWTVerificationException e) {
LOGGER.error(Utils.getFullStackTrace(e));
}
- return valid;
+ return token;
+ }
+
+ @Override
+ public String getClientId(DecodedJWT token) {
+ // Also available as "claimId" when using a confidential client
+ return token.getClaim("azp").asString();
+ }
+
+ @Override
+ public List getGroups(DecodedJWT token) {
+ return token.getClaim("groups").asList(String.class);
}
}
diff --git a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Statement.java b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Statement.java
index ea362b06..20c73264 100644
--- a/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Statement.java
+++ b/functions/authorizer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Statement.java
@@ -22,6 +22,7 @@
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -76,8 +77,14 @@ public Builder resource(String... resource) {
if (resource != null && resource.length > 0) {
resources.clear();
Collections.addAll(this.resources, resource);
- } else {
- this.resources = new ArrayList<>(List.of("*"));
+ }
+ return this;
+ }
+
+ public Builder resource(Collection resource) {
+ if (resource != null && !resource.isEmpty()) {
+ resources.clear();
+ resources.addAll(resource);
}
return this;
}
diff --git a/functions/authorizer/src/main/resources/spotbugs-exclude.xml b/functions/authorizer/src/main/resources/spotbugs-exclude.xml
new file mode 100644
index 00000000..710974a4
--- /dev/null
+++ b/functions/authorizer/src/main/resources/spotbugs-exclude.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/functions/codepipeline-wait-handler/pom.xml b/functions/codepipeline-wait-handler/pom.xml
index 782b6f39..5580c738 100644
--- a/functions/codepipeline-wait-handler/pom.xml
+++ b/functions/codepipeline-wait-handler/pom.xml
@@ -33,6 +33,7 @@ limitations under the License.
+ ${project.basedir}/../..0
@@ -59,13 +60,6 @@ limitations under the License.
-
- com.amazon.aws.partners.saasfactory.saasboost
- Utils
- 1.0.0
-
- provided
- com.amazon.aws.partners.saasfactory.saasboostCloudFormationUtils
diff --git a/functions/codepipeline-wait-handler/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CodePipelineWaitHandler.java b/functions/codepipeline-wait-handler/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CodePipelineWaitHandler.java
index 01b4e7d9..2f095c3c 100644
--- a/functions/codepipeline-wait-handler/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CodePipelineWaitHandler.java
+++ b/functions/codepipeline-wait-handler/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CodePipelineWaitHandler.java
@@ -34,7 +34,7 @@
import java.util.regex.Pattern;
import java.util.stream.Collectors;
-public class CodePipelineWaitHandler implements RequestHandler
- junit
- junit
+ com.amazon.aws.partners.saasfactory.saasboost
+ SaaSBoostApiClientHelper
+ 1.0.0org.slf4j
- slf4j-nop
+ slf4j-api
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+
+
+ org.junit.jupiter
+ junit-jupiter-apiorg.mockitomockito-core
+
+ org.mockito
+ mockito-junit-jupiter
+ ${mockito.version}
+ test
+
+
+ org.slf4j
+ slf4j-nop
+ org.apache.logging.log4jlog4j-slf4j-implsoftware.amazon.awssdk
- cloudformation
+ apache-client
+ ${aws.java.sdk.version}
+
+
+ software.amazon.awssdk
+ iam-policy-builder${aws.java.sdk.version}software.amazon.awssdk
- quicksight
+ cloudformation${aws.java.sdk.version}
@@ -157,11 +182,6 @@ limitations under the License.
sts${aws.java.sdk.version}
-
- software.amazon.awssdk
- secretsmanager
- ${aws.java.sdk.version}
- software.amazon.awssdkroute53
diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostArtifactsBucket.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostArtifactsBucket.java
index be197c0a..98684b63 100644
--- a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostArtifactsBucket.java
+++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostArtifactsBucket.java
@@ -20,6 +20,7 @@
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.core.exception.SdkServiceException;
import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.policybuilder.iam.*;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
@@ -34,16 +35,30 @@ public class SaaSBoostArtifactsBucket {
private final String bucketName;
private final Region region;
+ private final String appPlaneAccountId;
public SaaSBoostArtifactsBucket(String bucketName, Region region) {
+ this(bucketName, region, null);
+ }
+
+ public SaaSBoostArtifactsBucket(String bucketName, Region region, String appPlaneAccountId) {
this.bucketName = bucketName;
this.region = region;
+ this.appPlaneAccountId = appPlaneAccountId;
}
public String getBucketName() {
return bucketName;
}
+ public Region getRegion() {
+ return region;
+ }
+
+ public String getAppPlaneAccountId() {
+ return appPlaneAccountId;
+ }
+
public String toString() {
return getBucketName();
}
@@ -62,7 +77,7 @@ public void putFile(S3Client s3, Path localPath, Path remotePath) {
s3.putObject(PutObjectRequest.builder()
.bucket(bucketName)
// java.nio.file.Path will use OS dependent file separators, so when we run the installer on
- // Windows, the S3 key will have back slashes instead of forward slashes. The CloudFormation
+ // Windows, the S3 key will have backslashes instead of forward slashes. The CloudFormation
// definitions of the Lambda functions will always use forward slashes for the S3Key property.
.key(remotePath.toString().replace('\\', '/'))
.build(), RequestBody.fromFile(localPath)
@@ -74,9 +89,9 @@ public void putFile(S3Client s3, Path localPath, Path remotePath) {
}
}
- protected static SaaSBoostArtifactsBucket createS3ArtifactBucket(S3Client s3, String envName, Region awsRegion) {
+ protected static SaaSBoostArtifactsBucket createS3ArtifactBucket(S3Client s3, String envName, Region awsRegion
+ , String appPlaneAccountId) {
String s3ArtifactBucketName = "sb-" + envName + "-artifacts-" + Utils.randomString(12, "[^a-z0-9]");
- LOGGER.info("Creating S3 Artifact Bucket {}", s3ArtifactBucketName);
try {
CreateBucketRequest.Builder createBucketRequestBuilder = CreateBucketRequest.builder();
// LocationConstraint is not valid in US_EAST_1
@@ -86,13 +101,16 @@ protected static SaaSBoostArtifactsBucket createS3ArtifactBucket(S3Client s3, St
config.locationConstraint(BucketLocationConstraint.fromValue(awsRegion.id())));
}
createBucketRequestBuilder.bucket(s3ArtifactBucketName);
+ LOGGER.info("Creating S3 artifact bucket {}", s3ArtifactBucketName);
s3.createBucket(createBucketRequestBuilder.build());
+ LOGGER.info("Enabling EventBridge bucket notifications {}", s3ArtifactBucketName);
s3.putBucketNotificationConfiguration(PutBucketNotificationConfigurationRequest.builder()
.bucket(s3ArtifactBucketName)
.notificationConfiguration(NotificationConfiguration.builder()
.eventBridgeConfiguration(EventBridgeConfiguration.builder().build())
.build())
.build());
+ LOGGER.info("Setting default bucket encryption {}", s3ArtifactBucketName);
s3.putBucketEncryption(PutBucketEncryptionRequest.builder()
.bucket(s3ArtifactBucketName)
.serverSideEncryptionConfiguration(ServerSideEncryptionConfiguration.builder()
@@ -103,35 +121,42 @@ protected static SaaSBoostArtifactsBucket createS3ArtifactBucket(S3Client s3, St
.build())
.build())
.build());
- final String partitionName = awsRegion.metadata().partition().id();
+ String partitionName = awsRegion.metadata().partition().id();
+ IamPolicy policy = IamPolicy.builder()
+ .addStatement(statement -> statement
+ .sid("DenyNonHttps")
+ .effect(IamEffect.DENY)
+ .addPrincipal(IamPrincipal.ALL)
+ .addAction("s3:*")
+ .addResource("arn:" + partitionName + ":s3:::" + s3ArtifactBucketName + "/*")
+ .addResource("arn:" + partitionName + ":s3:::" + s3ArtifactBucketName)
+ .addCondition(condition -> condition
+ .operator(IamConditionOperator.BOOL)
+ .key("aws:SecureTransport")
+ .value("false")
+ )
+ )
+ .addStatement(statement -> statement
+ .sid("AppPlaneAccountQuickLink")
+ .effect(IamEffect.ALLOW)
+ .addPrincipal(IamPrincipalType.AWS, "arn:aws:iam::" + appPlaneAccountId + ":root")
+ .addAction("s3:GetObject")
+ .addResource("arn:" + partitionName + ":s3:::" + s3ArtifactBucketName + "/saas-boost-app-integration.yaml")
+ )
+ .build();
+ String bucketPolicy = policy.toJson(IamPolicyWriter.builder().prettyPrint(true).build());
+ LOGGER.info("Creating bucket policy {}", s3ArtifactBucketName);
+ LOGGER.info(bucketPolicy);
s3.putBucketPolicy(PutBucketPolicyRequest.builder()
- .policy("{\n"
- + " \"Version\": \"2012-10-17\",\n"
- + " \"Statement\": [\n"
- + " {\n"
- + " \"Sid\": \"DenyNonHttps\",\n"
- + " \"Effect\": \"Deny\",\n"
- + " \"Principal\": \"*\",\n"
- + " \"Action\": \"s3:*\",\n"
- + " \"Resource\": [\n"
- + " \"arn:" + partitionName + ":s3:::" + s3ArtifactBucketName + "/*\",\n"
- + " \"arn:" + partitionName + ":s3:::" + s3ArtifactBucketName + "\"\n"
- + " ],\n"
- + " \"Condition\": {\n"
- + " \"Bool\": {\n"
- + " \"aws:SecureTransport\": \"false\"\n"
- + " }\n"
- + " }\n"
- + " }\n"
- + " ]\n"
- + "}")
+ .policy(bucketPolicy)
.bucket(s3ArtifactBucketName)
.build());
} catch (SdkServiceException s3Error) {
LOGGER.error("s3 error {}", s3Error.getMessage());
LOGGER.error(getFullStackTrace(s3Error));
+ //TODO delete bucket if that step worked but the other settings failed
throw s3Error;
}
- return new SaaSBoostArtifactsBucket(s3ArtifactBucketName, awsRegion);
+ return new SaaSBoostArtifactsBucket(s3ArtifactBucketName, awsRegion, appPlaneAccountId);
}
}
diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstall.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstall.java
index aa731180..b61d3668 100644
--- a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstall.java
+++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstall.java
@@ -16,7 +16,6 @@
package com.amazon.aws.partners.saasfactory.saasboost;
-import com.amazon.aws.partners.saasfactory.saasboost.clients.AwsClientBuilderFactory;
import com.amazon.aws.partners.saasfactory.saasboost.model.Environment;
import com.amazon.aws.partners.saasfactory.saasboost.model.EnvironmentLoadException;
import com.amazon.aws.partners.saasfactory.saasboost.model.ExistingEnvironmentFactory;
@@ -24,9 +23,10 @@
import com.amazon.aws.partners.saasfactory.saasboost.workflow.Workflow;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import software.amazon.awssdk.core.SdkBytes;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.core.exception.SdkServiceException;
import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.http.apache.ApacheHttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.acm.AcmClient;
import software.amazon.awssdk.services.acm.model.*;
@@ -34,21 +34,11 @@
import software.amazon.awssdk.services.cloudformation.CloudFormationClient;
import software.amazon.awssdk.services.cloudformation.model.*;
import software.amazon.awssdk.services.cloudformation.model.Parameter;
-import software.amazon.awssdk.services.cloudformation.model.ResourceStatus;
import software.amazon.awssdk.services.cloudformation.model.Stack;
import software.amazon.awssdk.services.ecr.EcrClient;
import software.amazon.awssdk.services.ecr.model.*;
import software.amazon.awssdk.services.iam.IamClient;
import software.amazon.awssdk.services.iam.model.*;
-import software.amazon.awssdk.services.lambda.LambdaClient;
-import software.amazon.awssdk.services.lambda.model.InvocationType;
-import software.amazon.awssdk.services.lambda.model.InvokeResponse;
-import software.amazon.awssdk.services.quicksight.QuickSightClient;
-import software.amazon.awssdk.services.quicksight.model.*;
-import software.amazon.awssdk.services.quicksight.model.ListUsersRequest;
-import software.amazon.awssdk.services.quicksight.model.ListUsersResponse;
-import software.amazon.awssdk.services.quicksight.model.Tag;
-import software.amazon.awssdk.services.quicksight.model.User;
import software.amazon.awssdk.services.route53.Route53Client;
import software.amazon.awssdk.services.route53.model.HostedZone;
import software.amazon.awssdk.services.route53.model.ListHostedZonesByNameRequest;
@@ -56,16 +46,14 @@
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
-import software.amazon.awssdk.services.secretsmanager.model.CreateSecretRequest;
-import software.amazon.awssdk.services.secretsmanager.model.ResourceNotFoundException;
-import software.amazon.awssdk.services.secretsmanager.model.SecretsManagerException;
import software.amazon.awssdk.services.ssm.SsmClient;
import software.amazon.awssdk.services.ssm.model.*;
+import software.amazon.awssdk.services.sts.StsClient;
import java.io.*;
-import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
@@ -82,19 +70,19 @@
public class SaaSBoostInstall {
+ static {
+ System.setProperty("logger.timestamp",
+ DateTimeFormatter.ofPattern("yyyy-MM-dd-HH:mm:ss").format(LocalDateTime.now()));
+ }
+
private static final Logger LOGGER = LoggerFactory.getLogger(SaaSBoostInstall.class);
- private final AwsClientBuilderFactory awsClientBuilderFactory;
private final ApiGatewayClient apigw;
private final CloudFormationClient cfn;
private final EcrClient ecr;
private final IamClient iam;
- // TODO do we need to reassign the quicksight client between getQuickSightUsername and setupQuicksight?
- private QuickSightClient quickSight;
private final S3Client s3;
private final SsmClient ssm;
- private final LambdaClient lambda;
- private final SecretsManagerClient secretsManager;
private final Route53Client route53;
private final AcmClient acm;
@@ -106,19 +94,15 @@ public class SaaSBoostInstall {
private String lambdaSourceFolder = "lambdas";
private String stackName;
private Map baseStackDetails = new HashMap<>();
- private boolean useAnalyticsModule = false;
- private boolean useQuickSight = false;
- private String quickSightUsername;
- private String quickSightUserArn;
+ private SaaSBoostApiHelper api;
protected enum ACTION {
- INSTALL(1, "New AWS SaaS Boost install.", false),
- ADD_ANALYTICS(2, "Install Metrics and Analytics into existing AWS SaaS Boost deployment.", true),
- UPDATE_WEB_APP(3, "Update Web Application for existing AWS SaaS Boost deployment.", true),
- UPDATE(4, "Update existing AWS SaaS Boost deployment.", true),
- DELETE(5, "Delete existing AWS SaaS Boost deployment.", true),
- CANCEL(6, "Exit installer.", false);
- //DEBUG(7, "Debug", false);
+ INSTALL(1, "Install AWS SaaS Boost", false),
+ UPDATE(2, "Update AWS SaaS Boost", true),
+ UPDATE_WEB_APP(3, "Update Admin Web Application", true),
+ DELETE(4, "Delete AWS SaaS Boost", true),
+ CANCEL(5, "Exit", false),
+ DEBUG(6, "Debug", false);
private final int choice;
private final String prompt;
@@ -151,23 +135,16 @@ public static ACTION ofChoice(int choice) {
}
public SaaSBoostInstall() {
- awsClientBuilderFactory = AwsClientBuilderFactory.builder()
- .region(AWS_REGION)
- .build();
-
- apigw = awsClientBuilderFactory.apiGatewayBuilder().build();
- cfn = awsClientBuilderFactory.cloudFormationBuilder().build();
- ecr = awsClientBuilderFactory.ecrBuilder().build();
- iam = awsClientBuilderFactory.iamBuilder().build();
- lambda = awsClientBuilderFactory.lambdaBuilder().build();
- quickSight = awsClientBuilderFactory.quickSightBuilder().build();
- s3 = awsClientBuilderFactory.s3Builder().build();
- ssm = awsClientBuilderFactory.ssmBuilder().build();
- secretsManager = awsClientBuilderFactory.secretsManagerBuilder().build();
- route53 = awsClientBuilderFactory.route53Builder().build();
- acm = awsClientBuilderFactory.acmBuilder().build();
-
- accountId = awsClientBuilderFactory.stsBuilder().build().getCallerIdentity().account();
+ apigw = Utils.sdkClient(ApiGatewayClient.builder(), ApiGatewayClient.SERVICE_NAME, ApacheHttpClient.builder(), DefaultCredentialsProvider.create());
+ cfn = Utils.sdkClient(CloudFormationClient.builder(), CloudFormationClient.SERVICE_NAME, ApacheHttpClient.builder(), DefaultCredentialsProvider.create());
+ ecr = Utils.sdkClient(EcrClient.builder(), EcrClient.SERVICE_NAME, ApacheHttpClient.builder(), DefaultCredentialsProvider.create());
+ iam = Utils.sdkClient(IamClient.builder(), IamClient.SERVICE_NAME, ApacheHttpClient.builder(), DefaultCredentialsProvider.create());
+ s3 = Utils.sdkClient(S3Client.builder(), S3Client.SERVICE_NAME, ApacheHttpClient.builder(), DefaultCredentialsProvider.create());
+ ssm = Utils.sdkClient(SsmClient.builder(), SsmClient.SERVICE_NAME, ApacheHttpClient.builder(), DefaultCredentialsProvider.create());
+ route53 = Utils.sdkClient(Route53Client.builder(), Route53Client.SERVICE_NAME, ApacheHttpClient.builder(), DefaultCredentialsProvider.create());
+ acm = Utils.sdkClient(AcmClient.builder(), AcmClient.SERVICE_NAME, ApacheHttpClient.builder(), DefaultCredentialsProvider.create());
+ StsClient sts = Utils.sdkClient(StsClient.builder(), StsClient.SERVICE_NAME, ApacheHttpClient.builder(), DefaultCredentialsProvider.create());
+ accountId = sts.getCallerIdentity().account();
}
public static void main(String[] args) {
@@ -181,13 +158,51 @@ public static void main(String[] args) {
} catch (Exception e) {
outputMessage("===========================================================");
outputMessage("Installation Error: " + e.getLocalizedMessage());
- outputMessage("Please see detailed log file saas-boost-install.log");
- LOGGER.error(getFullStackTrace(e));
+ outputMessage("Please see detailed log file installer-"
+ + System.getProperty("logger.timestamp") + ".log");
+ LOGGER.error(Utils.getFullStackTrace(e));
}
}
protected void debug(String existingBucket) {
- copyAdminWebAppSourceToS3(workingDir, null, null);
+ while (true) {
+ System.out.print("Enter name of the AWS SaaS Boost environment to deploy (dev, test, uat, prod, etc...): ");
+ this.envName = Keyboard.readString();
+ if (validateEnvironmentName(this.envName)) {
+ LOGGER.info("Setting SaaS Boost environment = [{}]", this.envName);
+ break;
+ } else {
+ outputMessage("Entered value is invalid, maximum of 10 alphanumeric characters, and cannot be AWS,"
+ + " Amazon, or Cognito. Please try again.");
+ }
+ }
+ if (existingBucket != null) {
+ saasBoostArtifactsBucket = new SaaSBoostArtifactsBucket(existingBucket, AWS_REGION);
+ try {
+ s3.headBucket(request -> request.bucket(saasBoostArtifactsBucket.getBucketName()));
+ } catch (SdkServiceException s3error) {
+ outputMessage("Bucket " + existingBucket + " does not exist!");
+ throw s3error;
+ }
+ }
+// else {
+// saasBoostArtifactsBucket = SaaSBoostArtifactsBucket.createS3ArtifactBucket(s3, envName, AWS_REGION);
+// outputMessage("Created S3 artifacts bucket: " + saasBoostArtifactsBucket);
+// }
+//
+// // Copy the CloudFormation templates
+// outputMessage("Uploading CloudFormation templates to S3 artifacts bucket");
+// copyResourcesToS3();
+//
+// // Compile all the source code
+// outputMessage("Compiling Lambda functions and uploading to S3 artifacts bucket. This will take some time...");
+// processLambdas();
+//
+// // Copy the source files up to S3 where CloudFormation resources expect them to be
+// outputMessage("Uploading admin web app source files to S3");
+// copyAdminWebAppSourceToS3(workingDir, saasBoostArtifactsBucket.getBucketName(), s3);
+ outputMessage("Log into the Application Plane AWS Account and run the CloudFormation Integration Stack");
+ outputMessage(quickCreateLink());
}
public void start(String existingBucket) {
@@ -195,7 +210,7 @@ public void start(String existingBucket) {
outputMessage("Welcome to the AWS SaaS Boost Installer");
outputMessage("Installer Version: " + VERSION);
- // Do we have Maven, Node and AWS CLI on the PATH?
+ // Do we have Maven and the AWS CLI on the PATH?
checkEnvironment();
ACTION installOption;
@@ -228,38 +243,22 @@ public void start(String existingBucket) {
switch (installOption) {
case INSTALL:
- installSaaSBoost(existingBucket);
+ install(existingBucket);
break;
case UPDATE:
- workflow = new UpdateWorkflow(
- this.workingDir,
- this.environment,
- this.awsClientBuilderFactory,
- doesCfnMacroResourceExist());
+ workflow = new UpdateWorkflow(this.workingDir, this.environment, this.s3, this.cfn, this.apigw);
break;
case UPDATE_WEB_APP:
SaaSBoostInstall.copyAdminWebAppSourceToS3(this.workingDir,
this.saasBoostArtifactsBucket.getBucketName(), this.s3);
break;
- case ADD_ANALYTICS:
- this.useAnalyticsModule = true;
- System.out.print("Would you like to setup Amazon Quicksight for the Analytics module?"
- + "You must have already registered for Quicksight in your account (y or n)? ");
- this.useQuickSight = Keyboard.readBoolean();
- if (this.useQuickSight) {
- getQuickSightUsername();
- }
- installAnalyticsModule();
- break;
case DELETE:
- deleteSaasBoostInstallation();
+ delete();
break;
- case CANCEL:
- cancel();
+ case DEBUG:
+ debug(existingBucket);
break;
- //case DEBUG:
- // debug(existingBucket);
- // break;
+ case CANCEL:
default:
cancel();
}
@@ -270,7 +269,7 @@ public void start(String existingBucket) {
}
}
- protected void installSaaSBoost(String existingBucket) {
+ protected void install(String existingBucket) {
LOGGER.info("Performing new installation of AWS SaaS Boost");
while (true) {
System.out.print("Enter name of the AWS SaaS Boost environment to deploy (Ex. dev, test, uat, prod, etc.): ");
@@ -304,16 +303,17 @@ protected void installSaaSBoost(String existingBucket) {
String systemIdentityProvider;
while (true) {
- System.out.print("Enter the identity provider to use for system users (Cognito or Keycloak) Press Enter for 'Cognito': ");
+ System.out.print("Enter the identity provider to use for system users (Cognito, Keycloak, or Auth0) Press Enter for 'Cognito': ");
systemIdentityProvider = Keyboard.readString();
if (isNotBlank(systemIdentityProvider)) {
if (systemIdentityProvider.toUpperCase().equals("COGNITO")
- || systemIdentityProvider.toUpperCase().equals("KEYCLOAK")) {
+ || systemIdentityProvider.toUpperCase().equals("KEYCLOAK")
+ || systemIdentityProvider.toUpperCase().equals("AUTH0")) {
systemIdentityProvider = systemIdentityProvider.toUpperCase();
LOGGER.info("Setting Identity Provider = [{}]", systemIdentityProvider);
break;
} else {
- outputMessage("Invalid identity provider. Enter either Cognito or Keycloak.");
+ outputMessage("Invalid identity provider. Enter either Cognito, Keycloak, or Auth0.");
}
} else {
systemIdentityProvider = "COGNITO";
@@ -407,10 +407,19 @@ protected void installSaaSBoost(String existingBucket) {
}
}
- boolean useCustomDomainForAdminWebApp = Utils.isChinaRegion(AWS_REGION);
+ String auth0ApiKey = "";
+ String auth0ApiClientId = "";
+ if ("AUTH0".equals(systemIdentityProvider)) {
+
+ }
+
+ Boolean useCustomDomainForAdminWebApp = Utils.isChinaRegion(AWS_REGION);
if (!useCustomDomainForAdminWebApp) {
- System.out.print("Would you like to use a custom domain name for the SaaS Boost admin web console (y or n)? ");
+ System.out.print("Would you like to use a custom domain name for the SaaS Boost admin web console (y or n) Press Enter for 'n'? ");
useCustomDomainForAdminWebApp = Keyboard.readBoolean();
+ if (useCustomDomainForAdminWebApp == null) {
+ useCustomDomainForAdminWebApp = Boolean.FALSE;
+ }
}
String adminWebAppCustomDomain = null;
String adminWebAppHostedZone = null;
@@ -549,16 +558,16 @@ protected void installSaaSBoost(String existingBucket) {
}
}
- System.out.print("Would you like to install the metrics and analytics module of AWS SaaS Boost (y or n)? ");
- this.useAnalyticsModule = Keyboard.readBoolean();
-
- // If installing the analytics module, ask about QuickSight.
- if (useAnalyticsModule) {
- System.out.print("Would you like to setup Amazon Quicksight for the Analytics module? You must have already registered for Quicksight in your account (y or n)? ");
- this.useQuickSight = Keyboard.readBoolean();
- }
- if (this.useQuickSight) {
- getQuickSightUsername();
+ String appPlaneAccountId;
+ while (true) {
+ System.out.print("Enter the AWS Account ID when you will install the application plane components for this SaaS Boost control plane: ");
+ appPlaneAccountId = Objects.toString(Keyboard.readString(), "").replace("-", "");
+ if (validateAwsAccountId(appPlaneAccountId)) {
+ LOGGER.info("Setting app plane account = [{}]", appPlaneAccountId);
+ break;
+ } else {
+ outputMessage("Entered value is invalid. Enter a 12 digit AWS Account ID. Please try again.");
+ }
}
System.out.println();
@@ -574,12 +583,7 @@ protected void installSaaSBoost(String existingBucket) {
+ (isNotBlank(identityProviderCustomDomain) ? identityProviderCustomDomain : "N/A"));
outputMessage("Custom Domain for SaaS Boost Admin Web Console: "
+ (isNotBlank(adminWebAppCustomDomain) ? adminWebAppCustomDomain : "N/A"));
- outputMessage("Install optional Analytics Module: " + this.useAnalyticsModule);
- if (this.useAnalyticsModule && isNotBlank(this.quickSightUsername)) {
- outputMessage("Amazon QuickSight user for Analytics Module: " + this.quickSightUsername);
- } else {
- outputMessage("Amazon QuickSight user for Analytics Module: N/A");
- }
+ outputMessage("Application Plane Account ID: " + appPlaneAccountId);
System.out.println();
System.out.print("Continue (y or n)? ");
@@ -600,7 +604,8 @@ protected void installSaaSBoost(String existingBucket) {
if (existingBucket == null) {
// Create the S3 artifacts bucket
outputMessage("Creating S3 artifacts bucket");
- saasBoostArtifactsBucket = SaaSBoostArtifactsBucket.createS3ArtifactBucket(s3, envName, AWS_REGION);
+ saasBoostArtifactsBucket = SaaSBoostArtifactsBucket.createS3ArtifactBucket(s3, envName, AWS_REGION
+ , appPlaneAccountId);
outputMessage("Created S3 artifacts bucket: " + saasBoostArtifactsBucket);
// Copy the CloudFormation templates
@@ -612,59 +617,63 @@ protected void installSaaSBoost(String existingBucket) {
processLambdas();
} else {
outputMessage("Reusing existing artifacts bucket " + existingBucket);
- saasBoostArtifactsBucket = new SaaSBoostArtifactsBucket(existingBucket, AWS_REGION);
- outputMessage("Uploading CloudFormation templates to S3 artifacts bucket");
- copyResourcesToS3();
+ saasBoostArtifactsBucket = new SaaSBoostArtifactsBucket(existingBucket, AWS_REGION, appPlaneAccountId);
try {
s3.headBucket(request -> request.bucket(saasBoostArtifactsBucket.getBucketName()));
} catch (SdkServiceException s3error) {
outputMessage("Bucket " + existingBucket + " does not exist!");
throw s3error;
}
+ outputMessage("Uploading CloudFormation templates to S3 artifacts bucket");
+ copyResourcesToS3();
}
// Copy the source files up to S3 where CloudFormation resources expect them to be
outputMessage("Uploading admin web app source files to S3");
copyAdminWebAppSourceToS3(workingDir, saasBoostArtifactsBucket.getBucketName(), s3);
+ // Copy the Api docs (SwaggerUI) source files up to S3 where CloudFormation resources expect them to be
+ outputMessage("Uploading api docs source files to S3");
+ copyApiDocsSourceToS3(workingDir, saasBoostArtifactsBucket.getBucketName(), s3);
+
// Run CloudFormation create stack
outputMessage("Running CloudFormation");
this.stackName = "sb-" + envName;
createSaaSBoostStack(stackName, emailAddress, systemIdentityProvider, identityProviderCustomDomain,
identityProviderHostedZone, identityProviderCertificate, adminWebAppCustomDomain,
- adminWebAppHostedZone, adminWebAppCertificate);
+ adminWebAppHostedZone, adminWebAppCertificate, appPlaneAccountId);
this.environment = ExistingEnvironmentFactory.findExistingEnvironment(
ssm, cfn, this.envName, this.accountId);
this.baseStackDetails = environment.getBaseCloudFormationStackInfo();
- if (useAnalyticsModule) {
- LOGGER.info("Install metrics and analytics module");
- // The analytics module stack reads baseStackDetails for its CloudFormation template parameters
- // because we're not yet creating the analytics resources as a nested child stack of the main stack
- installAnalyticsModule();
- }
- outputMessage("Check the admin email box for the temporary password.");
+ outputMessage("Check the admin email inbox for the temporary password.");
outputMessage("AWS SaaS Boost Artifacts Bucket: " + saasBoostArtifactsBucket);
outputMessage("AWS SaaS Boost Console URL is: " + baseStackDetails.get("AdminWebUrl"));
+
+ outputMessage("Log into the Application Plane AWS Account and run the CloudFormation Integration Stack:");
+ outputMessage(quickCreateLink());
}
- protected void deleteSaasBoostInstallation() {
+ protected void delete() {
// Confirm delete
outputMessage("****** W A R N I N G");
- outputMessage("Deleting the AWS SaaS Boost environment is IRREVERSIBLE and ALL deployed tenant resources will be deleted!");
+ outputMessage("Deleting the AWS SaaS Boost environment is IRREVERSIBLE and "
+ + "ALL deployed tenant resources will be deleted!");
while (true) {
System.out.print("Enter the SaaS Boost environment name to confirm: ");
String confirmEnvName = Keyboard.readString();
if (isNotBlank(confirmEnvName) && this.envName.equalsIgnoreCase(confirmEnvName)) {
- System.out.println("SaaS Boost environment " + this.envName + " for AWS Account " + this.accountId + " in region " + AWS_REGION + " will be deleted. This action cannot be undone!");
+ System.out.println("SaaS Boost environment " + this.envName + " for AWS Account " + this.accountId
+ + " in region " + AWS_REGION + " will be deleted. This action cannot be undone!");
break;
} else {
outputMessage("Entered value is incorrect, please try again.");
}
}
- System.out.print("Are you sure you want to delete the SaaS Boost environment " + this.envName + "? Enter y to continue or n to cancel: ");
+ System.out.print("Are you sure you want to delete the SaaS Boost environment "
+ + this.envName + "? Enter y to continue or n to cancel: ");
boolean continueDelete = Keyboard.readBoolean();
if (!continueDelete) {
outputMessage("Canceled Delete of AWS SaaS Boost environment");
@@ -674,48 +683,27 @@ protected void deleteSaasBoostInstallation() {
}
// Delete all the provisioned tenants
- List> tenants = getProvisionedTenants();
- for (LinkedHashMap tenant : tenants) {
+ List> tenants = getProvisionedTenants();
+ LOGGER.debug("Deleting {} provisioned tenants", tenants.size());
+ for (Map tenant : tenants) {
outputMessage("Deleting AWS SaaS Boost tenant " + tenant.get("id"));
deleteProvisionedTenant(tenant);
}
// Clear all the images from ECR or CloudFormation won't be able to delete the repository
- try {
- for (String ecrRepo : getEcrRepositories()) {
- outputMessage("Deleting images from ECR repository " + ecrRepo);
- deleteEcrImages(ecrRepo);
- }
- } catch (SdkServiceException ssmError) {
- LOGGER.error("ssm:GetParameter error", ssmError);
- LOGGER.error(getFullStackTrace(ssmError));
- // throw ssmError;
+ LOGGER.debug("Getting list of ECR repos to clear");
+ for (String ecrRepo : getEcrRepositories()) {
+ outputMessage("Deleting images from ECR repository " + ecrRepo);
+ deleteEcrImages(ecrRepo);
}
// Clear all the Parameter Store entries for this environment that CloudFormation doesn't own
+ LOGGER.debug("Deleting AppConfig");
deleteApplicationConfig();
- // Delete the analytics stack if it exists
- String analyticsStackName = analyticsStackName();
- if (checkCloudFormationStack(analyticsStackName)) {
- outputMessage("Deleting AWS SaaS Boost Analytics Module stack: " + analyticsStackName);
- deleteCloudFormationStack(analyticsStackName);
- }
-
// Delete the SaaS Boost stack
outputMessage("Deleting AWS SaaS Boost stack: " + this.stackName);
deleteCloudFormationStack(this.stackName);
- // Delete the ActiveDirectory password in SecretsManager if it exists
- try {
- secretsManager.deleteSecret(request -> request
- .forceDeleteWithoutRecovery(true)
- .secretId("/saas-boost/" + envName + "/ACTIVE_DIRECTORY_PASSWORD")
- .build()
- );
- outputMessage("ActiveDirectory secretsManager secret deleted.");
- } catch (ResourceNotFoundException rnfe) {
- // there is no ACTIVE_DIRECTORY_PASSWORD secret, so there is nothing to delete
- }
// Finally, remove the S3 artifacts bucket that this installer created outside of CloudFormation
LOGGER.info("Clean up s3 bucket: " + saasBoostArtifactsBucket);
@@ -745,185 +733,12 @@ protected void deleteSaasBoostInstallation() {
} catch (SdkServiceException ssmError) {
outputMessage("Failed to delete all Parameter Store entries");
LOGGER.error("ssm:DeleteParameters error", ssmError);
- LOGGER.error(getFullStackTrace(ssmError));
+ LOGGER.error(Utils.getFullStackTrace(ssmError));
}
outputMessage("Delete of SaaS Boost environment " + this.envName + " complete.");
}
- private List getEcrRepositories() {
- List repos = new ArrayList<>();
- Map systemApiRequest = new HashMap<>();
- Map detail = new HashMap<>();
- detail.put("resource", "settings/config");
- detail.put("method", "GET");
- systemApiRequest.put("detail", detail);
- final byte[] payload = Utils.toJson(systemApiRequest).getBytes();
- try {
- LOGGER.info("Invoking getSettings API");
- InvokeResponse response = lambda.invoke(request -> request
- .functionName("sb-" + this.envName + "-private-api-client")
- .invocationType(InvocationType.REQUEST_RESPONSE)
- .payload(SdkBytes.fromByteArray(payload))
- );
- if (response.sdkHttpResponse().isSuccessful()) {
- LOGGER.error("got response back: {}", response);
- String configJson = response.payload().asUtf8String();
- HashMap config = Utils.fromJson(configJson, HashMap.class);
- HashMap services = (HashMap) config.get("services");
- for (String serviceName : services.keySet()) {
- HashMap service = (HashMap) services.get(serviceName);
- Map compute = (Map) service.get("compute");
- repos.add((String) compute.get("containerRepo"));
- }
- } else {
- LOGGER.warn("Private API client Lambda returned HTTP " + response.sdkHttpResponse().statusCode());
- throw new RuntimeException(response.sdkHttpResponse().statusText().get());
- }
- } catch (SdkServiceException lambdaError) {
- LOGGER.error("lambda:Invoke error", lambdaError);
- LOGGER.error(getFullStackTrace(lambdaError));
- throw lambdaError;
- }
- return repos;
- }
-
- protected void installAnalyticsModule() {
- LOGGER.info("Installing Analytics module into existing AWS SaaS Boost installation.");
- outputMessage("Analytics will be deployed into the existing AWS SaaS Boost environment " + this.envName + ".");
-
- String metricsStackName = analyticsStackName();
- try {
- DescribeStacksResponse metricsStackResponse = cfn.describeStacks(request -> request.stackName(metricsStackName));
- if (metricsStackResponse.hasStacks()) {
- outputMessage("AWS SaaS Boost Analytics stack with name: " + metricsStackName + " is already deployed");
- System.exit(2);
- }
- } catch (CloudFormationException cfnError) {
- // Calling describe-stacks on a stack name that doesn't exist is an exception
- if (!cfnError.getMessage().contains("Stack with id " + metricsStackName + " does not exist")) {
- LOGGER.error("cloudformation:DescribeStacks error {}", cfnError.getMessage());
- LOGGER.error(getFullStackTrace(cfnError));
- throw cfnError;
- }
- }
-
- outputMessage("===========================================================");
- outputMessage("");
- outputMessage("Would you like to continue the Analytics module installation with the following options?");
- outputMessage("Existing AWS SaaS Boost environment : " + envName);
- if (useQuickSight) {
- outputMessage("Amazon QuickSight user for Analytics Module: " + quickSightUsername);
- } else {
- outputMessage("Amazon QuickSight user for Analytics Module: N/A");
- }
-
- System.out.print("Continue (y or n)? ");
- boolean continueInstall = Keyboard.readBoolean();
- if (!continueInstall) {
- outputMessage("Canceled installation of AWS SaaS Boost Analytics");
- cancel();
- }
- outputMessage("Continuing installation of AWS SaaS Boost Analytics");
- outputMessage("===========================================================");
- outputMessage("Installing AWS SaaS Boost Metrics and Analytics Module");
- outputMessage("===========================================================");
-
- // Generate a password for the RedShift database if we don't already have one
- String dbPassword;
- String dbPasswordParam = "/saas-boost/" + this.envName + "/REDSHIFT_MASTER_PASSWORD";
- try {
- GetParameterResponse existingDbPasswordResponse = ssm.getParameter(GetParameterRequest.builder()
- .name(dbPasswordParam)
- .withDecryption(true)
- .build()
- );
- // We actually need the secret value because we need to give it to QuickSight
- dbPassword = existingDbPasswordResponse.parameter().value();
- // And, we'll add the parameter version to the end of the name just in case it's greater than 1
- // so that CloudFormation can properly fetch the secret value
- dbPasswordParam = dbPasswordParam + ":" + existingDbPasswordResponse.parameter().version();
- LOGGER.info("Reusing existing RedShift password for Analytics");
- } catch (SdkServiceException noSuchParameter) {
- LOGGER.info("Generating new random RedShift password for Analytics");
- // Save the database password as a secret
- dbPassword = generatePassword(16);
- try {
- LOGGER.info("Saving RedShift password secret to Parameter Store");
- ssm.putParameter(PutParameterRequest.builder()
- .name(dbPasswordParam)
- .type(ParameterType.SECURE_STRING)
- .overwrite(true)
- .value(dbPassword)
- .build()
- );
- } catch (SdkServiceException ssmError) {
- LOGGER.error("ssm:PutParamter error {}", ssmError.getMessage());
- LOGGER.error(getFullStackTrace(ssmError));
- throw ssmError;
- }
- // CloudFormation ssm-secure resolution needs a version number, which is guaranteed to be 1
- // in this case where we just created it
- dbPasswordParam = dbPasswordParam + ":1";
- }
- outputMessage("Redshift Database User Password stored in secure SSM Parameter: " + dbPasswordParam);
-
- // Run CloudFormation
- outputMessage("Creating CloudFormation stack " + metricsStackName + " for Analytics Module");
- String databaseName = "sb_analytics_" + this.envName.replaceAll("-", "_");
- createMetricsStack(metricsStackName, dbPasswordParam, databaseName);
-
- // TODO Why doesn't the CloudFormation template own this?
- LOGGER.info("Update SSM param METRICS_ANALYTICS_DEPLOYED to true");
- try {
- ssm.putParameter(request -> request
- .name("/saas-boost/" + this.envName + "/METRICS_ANALYTICS_DEPLOYED")
- .type(ParameterType.STRING)
- .overwrite(true)
- .value("true")
- );
- } catch (SdkServiceException ssmError) {
- LOGGER.error("ssm:PutParameter error {}", ssmError.getMessage());
- LOGGER.error(getFullStackTrace(ssmError));
- throw ssmError;
- }
-
- // Upload the JSON path file for Redshift to the bucket provisioned by CloudFormation
- Map outputs = getMetricStackOutputs(metricsStackName);
- String metricsBucket = outputs.get("MetricsBucket");
- Path jsonPathFile = workingDir.resolve(Path.of("metrics-analytics", "deploy", "artifacts", "metrics_redshift_jsonpath.json"));
-
- LOGGER.info("Copying json files for Metrics and Analytics from {} to {}", jsonPathFile.toString(), metricsBucket);
- try {
- s3.putObject(PutObjectRequest.builder()
- .bucket(metricsBucket)
- .key("metrics_redshift_jsonpath.json")
- .contentType("text/json")
- .build(), RequestBody.fromFile(jsonPathFile)
- );
- } catch (SdkServiceException s3Error) {
- LOGGER.error("s3:PutObject error {}", s3Error.getMessage());
- LOGGER.error(getFullStackTrace(s3Error));
- outputMessage("Error copying " + jsonPathFile.toString() + " to " + metricsBucket);
- // TODO Why don't we bail here if that file is required?
- outputMessage("Continuing with installation so you will need to manually upload that file.");
- }
-
- // Setup the quicksight dataset
- if (useQuickSight) {
- outputMessage("Set up Amazon Quicksight for Analytics Module");
- try {
- // TODO does this fail if it's run more than once?
- setupQuickSight(metricsStackName, outputs, dbPassword);
- } catch (Exception e) {
- outputMessage("Error with setup of Quicksight datasource and dataset. Check log file.");
- outputMessage("Message: " + e.getMessage());
- LOGGER.error(getFullStackTrace(e));
- System.exit(2);
- }
- }
- }
-
protected void cancel() {
outputMessage("Cancelling.");
System.exit(0);
@@ -955,202 +770,98 @@ protected static Path getWorkingDirectory() {
return workingDir;
}
- protected void getQuickSightUsername() {
- Region quickSightRegion;
- QuickSightClient oldClient = null;
- while (true) {
- System.out.print("Region where you registered for Amazon QuickSight (Press Enter for " + AWS_REGION.id() + "): ");
- String quickSightAccountRegion = Keyboard.readString();
- if (isBlank(quickSightAccountRegion)) {
- quickSightRegion = AWS_REGION;
- } else {
- // Make sure we got a valid AWS region string
- quickSightRegion = Region.regions().stream().filter(request -> request
- .id()
- .equals(quickSightAccountRegion))
- .findAny()
- .orElse(null);
- }
- if (quickSightRegion != null) {
- // Update the SDK client for the proper AWS region if we need to
- if (!AWS_REGION.equals(quickSightRegion)) {
- oldClient = quickSight;
- quickSight = awsClientBuilderFactory.quickSightBuilder().region(quickSightRegion).build();
- }
- // See if there are QuickSight users in this account in this region
- LinkedHashMap quickSightUsers = getQuickSightUsers();
- if (!quickSightUsers.isEmpty()) {
- String defaultQuickSightUsername = quickSightUsers.keySet().stream().findFirst().orElse(null);
- System.out.print("Amazon Quicksight user name (Press Enter for '" + defaultQuickSightUsername + "'): ");
- this.quickSightUsername = Keyboard.readString();
- if (isBlank(this.quickSightUsername)) {
- this.quickSightUsername = defaultQuickSightUsername;
- }
- if (quickSightUsers.containsKey(this.quickSightUsername)) {
- this.quickSightUserArn = quickSightUsers.get(this.quickSightUsername).arn();
- break;
- } else {
- outputMessage("Entered value is not a valid Quicksight user in your account, please try again.");
- }
- } else {
- outputMessage("No users found in QuickSight. Please register in your AWS Account and try install again.");
- System.exit(2);
- }
- } else {
- outputMessage("Entered value is not a region, please try again.");
- }
+ protected SaaSBoostApiHelper api() {
+ if (api == null) {
+ SaaSBoostApiHelper.SaaSBoostApiHelperDependencyFactory init = () ->
+ Utils.sdkClient(SecretsManagerClient.builder(), SecretsManagerClient.SERVICE_NAME,
+ ApacheHttpClient.builder(), DefaultCredentialsProvider.create());
+ String secretId = "/saas-boost/" + envName + "/PRIVATE_API_APP_CLIENT";
+ api = new SaaSBoostApiHelper(init, secretId);
}
- // If we changed the QuickSight SDK client region to look up the username, put it back
- if (oldClient != null) {
- quickSight = oldClient;
+ return api;
+ }
+
+ protected List> getProvisionedTenants() {
+ LOGGER.info("Calling tenant service to fetch all provisioned tenants");
+ String getTenantsResponseBody = api().authorizedRequest("GET", "tenants?status=provisioned");
+ List> tenants = Utils.fromJson(getTenantsResponseBody, ArrayList.class);
+ if (tenants == null) {
+ tenants = new ArrayList<>();
}
+ return tenants;
}
- protected List> getProvisionedTenants() {
- List> provisionedTenants = new ArrayList<>();
- Map systemApiRequest = new HashMap<>();
- Map detail = new HashMap<>();
- detail.put("resource", "tenants");
- detail.put("method", "GET");
- systemApiRequest.put("detail", detail);
- final byte[] payload = Utils.toJson(systemApiRequest).getBytes(StandardCharsets.UTF_8);
- try {
- LOGGER.info("Invoking get provisioned tenants API");
- InvokeResponse response = lambda.invoke(request -> request
- .functionName("sb-" + this.envName + "-private-api-client")
- .invocationType(InvocationType.REQUEST_RESPONSE)
- .payload(SdkBytes.fromByteArray(payload))
- );
- if (response.sdkHttpResponse().isSuccessful()) {
- String responseBody = response.payload().asUtf8String();
- LOGGER.info("Response Body");
- LOGGER.info(responseBody);
- provisionedTenants = Utils.fromJson(responseBody, ArrayList.class);
- LOGGER.info("Loaded " + provisionedTenants.size() + " tenants");
- } else {
- LOGGER.warn("Private API client Lambda returned HTTP " + response.sdkHttpResponse().statusCode());
- throw new RuntimeException(response.sdkHttpResponse().statusText().get());
- }
- } catch (SdkServiceException lambdaError) {
- LOGGER.error("lambda:Invoke error", lambdaError);
- LOGGER.error(getFullStackTrace(lambdaError));
- throw lambdaError;
+ protected Map getTenant(String tenantId) {
+ LOGGER.info("Calling tenant service to fetch tenant {}", tenantId);
+ String getTenantResponseBody = api().authorizedRequest("GET", "tenants/" + tenantId);
+ Map tenant = Utils.fromJson(getTenantResponseBody, HashMap.class);
+ if (tenant == null) {
+ return Collections.emptyMap();
}
- return provisionedTenants;
+ return tenant;
}
- private LinkedHashMap getTenant(String tenantId) {
- LinkedHashMap tenantDetail = new LinkedHashMap<>();
- Map systemApiRequest = new HashMap<>();
- Map detail = new HashMap<>();
- detail.put("resource", "tenants/" + tenantId);
- detail.put("method", "GET");
- systemApiRequest.put("detail", detail);
- final byte[] payload = Utils.toJson(systemApiRequest).getBytes();
- try {
- LOGGER.info("Invoking get tenant by id API");
- InvokeResponse response = lambda.invoke(request -> request
- .functionName("sb-" + this.envName + "-private-api-client")
- .invocationType(InvocationType.REQUEST_RESPONSE)
- .payload(SdkBytes.fromByteArray(payload))
- );
- if (response.sdkHttpResponse().isSuccessful()) {
- String responseBody = response.payload().asUtf8String();
- LOGGER.info("Response Body");
- LOGGER.info(responseBody);
- tenantDetail = Utils.fromJson(responseBody, LinkedHashMap.class);
- } else {
- LOGGER.warn("Private API client Lambda returned HTTP " + response.sdkHttpResponse().statusCode());
- throw new RuntimeException(response.sdkHttpResponse().statusText().get());
- }
- } catch (SdkServiceException lambdaError) {
- LOGGER.error("lambda:Invoke error", lambdaError);
- LOGGER.error(getFullStackTrace(lambdaError));
- throw lambdaError;
+ protected Map getAppConfig() {
+ LOGGER.info("Calling appConfig service to fetch appConfig");
+ String getAppConfigResponseBody = api().authorizedRequest("GET", "appconfig");
+ Map appConfig = Utils.fromJson(getAppConfigResponseBody, HashMap.class);
+ if (appConfig == null) {
+ return Collections.emptyMap();
}
- return tenantDetail;
+ return appConfig;
}
protected void deleteApplicationConfig() {
- Map systemApiRequest = new HashMap<>();
- Map detail = new HashMap<>();
- detail.put("resource", "settings/config");
- detail.put("method", "DELETE");
- systemApiRequest.put("detail", detail);
- final byte[] payload = Utils.toJson(systemApiRequest).getBytes(StandardCharsets.UTF_8);
+ LOGGER.info("Calling appConfig service to delete appConfig");
try {
- LOGGER.info("Invoking delete application config API");
- InvokeResponse response = lambda.invoke(request -> request
- .functionName("sb-" + this.envName + "-private-api-client")
- .invocationType(InvocationType.REQUEST_RESPONSE)
- .payload(SdkBytes.fromByteArray(payload))
- );
- if (!response.sdkHttpResponse().isSuccessful()) {
- LOGGER.warn("Private API client Lambda returned HTTP " + response.sdkHttpResponse().statusCode());
- throw new RuntimeException(response.sdkHttpResponse().statusText().get());
- }
- } catch (SdkServiceException lambdaError) {
- LOGGER.error("lambda:Invoke error", lambdaError);
- LOGGER.error(getFullStackTrace(lambdaError));
- throw lambdaError;
+ api().authorizedRequest("DELETE", "appconfig");
+ } catch (Exception apiError) {
+ LOGGER.error(apiError.getMessage());
}
}
- protected void deleteProvisionedTenant(LinkedHashMap tenant) {
+ protected void deleteProvisionedTenant(Map tenant) {
// TODO we can parallelize to improve performance with lots of tenants
- Map detail = new HashMap<>();
- detail.put("resource", "tenants/" + tenant.get("id"));
- detail.put("method", "DELETE");
- String tenantId = (String) tenant.get("id");
- Map tenantIdOnly = new HashMap<>();
- tenantIdOnly.put("id", tenantId);
- detail.put("body", Utils.toJson(tenantIdOnly));
- Map systemApiRequest = new HashMap<>();
- systemApiRequest.put("detail", detail);
- final byte[] payload = Utils.toJson(systemApiRequest).getBytes();
+ LOGGER.info("Calling tenant service to delete tenant {}", tenant.get("id"));
try {
- LOGGER.info("Invoking delete tenant API");
- InvokeResponse response = lambda.invoke(request -> request
- .functionName("sb-" + this.envName + "-private-api-client")
- .invocationType(InvocationType.REQUEST_RESPONSE)
- .payload(SdkBytes.fromByteArray(payload))
- );
- if (response.sdkHttpResponse().isSuccessful()) {
- LOGGER.info("got response back: {}", response);
- // wait for tenant to reach deleted
- final String DELETED = "deleted";
- LocalDateTime timeout = LocalDateTime.now().plus(60, ChronoUnit.MINUTES);
- String tenantStatus = (String) getTenant(tenantId).get("onboardingStatus");
- boolean deleted = tenantStatus.equalsIgnoreCase(DELETED);
- while (!deleted) {
- if (LocalDateTime.now().compareTo(timeout) > 0) {
- // we've timed out retrying
- outputMessage("Timed out waiting for tenant " + tenantId + " to reach deleted state. "
- + "Please check CloudFormation in your AWS Console for more details.");
- // if a tenant delete fails, trying to delete the rest of the stack is guaranteed to fail
- // due to Tenant resources having cross-dependencies with other resources. stop here to let
- // the user figure out what went wrong
- throw new RuntimeException("Delete failed.");
- }
- outputMessage("Waiting 1 minute for tenant " + tenantId
- + " to reach deleted from " + tenantStatus);
- Thread.sleep(60 * 1000L); // 1 minute
- tenantStatus = (String) getTenant(tenantId).get("onboardingStatus");
- deleted = tenantStatus.equalsIgnoreCase(DELETED);
+ String tenantId = (String) tenant.get("id");
+ api().authorizedRequest("DELETE", "tenants/" + tenant.get("id"));
+ // wait for tenant to reach deleted
+ final String DELETED = "deleted";
+ LocalDateTime timeout = LocalDateTime.now().plus(60, ChronoUnit.MINUTES);
+ String tenantStatus = (String) getTenant(tenantId).get("onboardingStatus");
+ boolean deleted = tenantStatus.equalsIgnoreCase(DELETED);
+ while (!deleted) {
+ if (LocalDateTime.now().compareTo(timeout) > 0) {
+ // we've timed out retrying
+ outputMessage("Timed out waiting for tenant " + tenantId + " to reach deleted state. "
+ + "Please check CloudFormation in your AWS Console for more details.");
+ // if a tenant delete fails, trying to delete the rest of the stack is guaranteed to fail
+ // due to Tenant resources having cross-dependencies with other resources. stop here to let
+ // the user figure out what went wrong
+ throw new RuntimeException("Delete failed.");
}
- } else {
- LOGGER.warn("Private API client Lambda returned HTTP " + response.sdkHttpResponse().statusCode());
- throw new RuntimeException(response.sdkHttpResponse().statusText().get());
+ outputMessage("Waiting 1 minute for tenant " + tenantId
+ + " to reach deleted from " + tenantStatus);
+ Thread.sleep(60 * 1000L); // 1 minute
+ tenantStatus = (String) getTenant(tenantId).get("onboardingStatus");
+ deleted = tenantStatus.equalsIgnoreCase(DELETED);
}
- } catch (SdkServiceException lambdaError) {
- LOGGER.error("lambda:Invoke error", lambdaError);
- LOGGER.error(getFullStackTrace(lambdaError));
- throw lambdaError;
- } catch (InterruptedException ie) {
- LOGGER.error("Exception in waiting");
- LOGGER.error(getFullStackTrace(ie));
- throw new RuntimeException(ie);
+ } catch (Exception e) {
+ LOGGER.error(e.getMessage());
+ }
+ }
+
+ protected List getEcrRepositories() {
+ List repos = new ArrayList<>();
+ Map appConfig = getAppConfig();
+ Map services = (Map) appConfig.get("services");
+ for (String serviceName : services.keySet()) {
+ Map service = (Map) services.get(serviceName);
+ Map compute = (Map) service.get("compute");
+ repos.add((String) compute.get("containerRepo"));
}
+ return repos;
}
protected void deleteEcrImages(String ecrRepo) {
@@ -1171,7 +882,7 @@ protected void deleteEcrImages(String ecrRepo) {
token = response.nextToken();
} catch (SdkServiceException ecrError) {
LOGGER.error("ecr:ListImages error", ecrError);
- LOGGER.error(getFullStackTrace(ecrError));
+ LOGGER.error(Utils.getFullStackTrace(ecrError));
throw ecrError;
}
} while (token != null);
@@ -1189,142 +900,14 @@ protected void deleteEcrImages(String ecrRepo) {
}
} catch (SdkServiceException ecrError) {
LOGGER.error("ecr:batchDeleteImage error", ecrError);
- LOGGER.error(getFullStackTrace(ecrError));
+ LOGGER.error(Utils.getFullStackTrace(ecrError));
throw ecrError;
}
}
}
- protected Map getMetricStackOutputs(String stackName) {
- // Get the Redshift outputs from the metrics CloudFormation stack
- Map outputs = null;
- try {
- DescribeStacksResponse stacksResponse = cfn.describeStacks(DescribeStacksRequest.builder().stackName(stackName).build());
- outputs = stacksResponse.stacks().get(0).outputs().stream().collect(Collectors.toMap(Output::outputKey, Output::outputValue));
- for (String requiredOutput : Arrays.asList("RedshiftDatabaseName", "RedshiftEndpointAddress", "RedshiftCluster", "RedshiftEndpointPort", "MetricsBucket")) {
- if (outputs.get(requiredOutput) == null) {
- outputMessage("Error, CloudFormation stack: " + stackName + " missing required output: " + requiredOutput);
- outputMessage(("Aborting the installation due to error"));
- System.exit(2);
- }
- }
- } catch (SdkServiceException cloudFormationError) {
- LOGGER.error("cloudformation:DescribeStack error", cloudFormationError);
- LOGGER.error(getFullStackTrace(cloudFormationError));
- outputMessage("getMetricStackOutputs: Unable to load Metrics and Analytics CloudFormation stack: " + stackName);
- System.exit(2);
- }
- return outputs;
- }
-
- protected void setupQuickSight(String stackName, Map outputs, String dbPassword) {
- /* TODO Note that this entire QuickSight setup is not owned by CloudFormation like most everything
- * else and therefore won't be cleaned up properly when SaaS Boost is deleted/uninstalled.
- */
- LOGGER.info("User for QuickSight: " + this.quickSightUsername);
- LOGGER.info("Create data source in QuickSight for metrics Redshift table in Region: " + AWS_REGION.id());
- final CreateDataSourceResponse createDataSourceResponse = quickSight.createDataSource(CreateDataSourceRequest.builder()
- .dataSourceId("sb-" + this.envName + "-metrics-source")
- .name("sb-" + this.envName + "-metrics-source")
- .awsAccountId(accountId)
- .type(DataSourceType.REDSHIFT)
- .dataSourceParameters(DataSourceParameters.builder()
- .redshiftParameters(RedshiftParameters.builder()
- .database(outputs.get("RedshiftDatabaseName"))
- .host(outputs.get("RedshiftEndpointAddress"))
- .clusterId(outputs.get("RedshiftCluster"))
- .port(Integer.valueOf(outputs.get("RedshiftEndpointPort")))
- .build()
- )
- .build()
- )
- .credentials(DataSourceCredentials.builder()
- .credentialPair(CredentialPair.builder()
- .username("metricsadmin")
- .password(dbPassword)
- .build()
- )
- .build()
- )
- .permissions(ResourcePermission.builder()
- .principal(this.quickSightUserArn)
- .actions("quicksight:DescribeDataSource","quicksight:DescribeDataSourcePermissions",
- "quicksight:PassDataSource","quicksight:UpdateDataSource","quicksight:DeleteDataSource",
- "quicksight:UpdateDataSourcePermissions")
- .build()
- )
- .sslProperties(SslProperties.builder()
- .disableSsl(false)
- .build()
- )
- .tags(Tag.builder()
- .key("Name")
- .value(stackName)
- .build()
- )
- .build()
- );
-
- // Define the physical table for QuickSight
- List inputColumns = new ArrayList<>();
- Stream.of("type", "workload", "context", "tenant_id", "tenant_name", "tenant_tier", "metric_name", "metric_unit", "meta_data")
- .map(column -> InputColumn.builder()
- .name(column)
- .type(InputColumnDataType.STRING)
- .build()
- )
- .forEachOrdered(inputColumns::add);
- inputColumns.add(InputColumn.builder()
- .name("metric_value")
- .type(InputColumnDataType.INTEGER)
- .build()
- );
- inputColumns.add(InputColumn.builder()
- .name("timerecorded")
- .type(InputColumnDataType.DATETIME)
- .build()
- );
-
- PhysicalTable physicalTable = PhysicalTable.builder()
- .relationalTable(RelationalTable.builder()
- .dataSourceArn(createDataSourceResponse.arn())
- .schema("public")
- .name("sb_metrics")
- .inputColumns(inputColumns)
- .build()
- )
- .build();
-
- Map physicalTableMap = new HashMap<>();
- physicalTableMap.put("string", physicalTable);
-
- LOGGER.info("Create dataset for sb_metrics table in Quicksight in Region " + AWS_REGION.id());
- quickSight.createDataSet(CreateDataSetRequest.builder()
- .awsAccountId(accountId)
- .dataSetId("sb-" + this.envName + "-metrics")
- .name("sb-" + this.envName + "-metrics")
- .physicalTableMap(physicalTableMap)
- .importMode(DataSetImportMode.DIRECT_QUERY)
- .permissions(ResourcePermission.builder()
- .principal(this.quickSightUserArn)
- .actions("quicksight:DescribeDataSet","quicksight:DescribeDataSetPermissions",
- "quicksight:PassDataSet","quicksight:DescribeIngestion","quicksight:ListIngestions",
- "quicksight:UpdateDataSet","quicksight:DeleteDataSet","quicksight:CreateIngestion",
- "quicksight:CancelIngestion","quicksight:UpdateDataSetPermissions")
- .build()
- )
- .tags(Tag.builder()
- .key("Name")
- .value(stackName)
- .build()
- )
- .build()
- );
- }
-
- /*
- Create Service Roles necessary for Tenant Stack Deployment
- */
+ // TODO Technically these may only be necessary now if we're installing Keycloak
+ // Create Service Roles necessary for Tenant Stack Deployment
protected void setupAwsServiceRoles() {
/*
aws iam get-role --role-name "AWSServiceRoleForElasticLoadBalancing" || aws iam create-service-linked-role --aws-service-name "elasticloadbalancing.amazonaws.com"
@@ -1355,7 +938,7 @@ protected void setupAwsServiceRoles() {
iam.createServiceLinkedRole(request -> request.awsServiceName(serviceRole));
} catch (SdkServiceException iamError) {
LOGGER.error("iam:CreateServiceLinkedRole error", iamError);
- LOGGER.error(getFullStackTrace(iamError));
+ LOGGER.error(Utils.getFullStackTrace(iamError));
throw iamError;
}
}
@@ -1377,7 +960,7 @@ protected void copyResourcesToS3() {
}
} catch (IOException ioe) {
LOGGER.error("Error listing resources directory", ioe);
- LOGGER.error(getFullStackTrace(ioe));
+ LOGGER.error(Utils.getFullStackTrace(ioe));
throw new RuntimeException(ioe);
}
try (Stream stream = Files.walk(resourcesDir.resolve("keycloak"))) {
@@ -1389,7 +972,7 @@ protected void copyResourcesToS3() {
}
} catch (IOException ioe) {
LOGGER.error("Error walking keycloak directory", ioe);
- LOGGER.error(getFullStackTrace(ioe));
+ LOGGER.error(Utils.getFullStackTrace(ioe));
// TODO while this is an invalid state, maybe we only want to fail out if
// KEYCLOAK actually needs to be installed for this environment
throw new RuntimeException(ioe);
@@ -1404,7 +987,7 @@ protected static void printResults(Process process) {
}
} catch (IOException ioe) {
LOGGER.error("Error reading from runtime exec process", ioe);
- LOGGER.error(getFullStackTrace(ioe));
+ LOGGER.error(Utils.getFullStackTrace(ioe));
throw new RuntimeException(ioe);
}
}
@@ -1451,7 +1034,6 @@ protected void loadExistingSaaSBoostEnvironment() {
this.lambdaSourceFolder = environment.getLambdasFolderName();
this.stackName = environment.getBaseCloudFormationStackName();
this.baseStackDetails = environment.getBaseCloudFormationStackInfo();
- this.useAnalyticsModule = environment.isMetricsAnalyticsDeployed();
}
protected String getExistingSaaSBoostEnvironment() {
@@ -1467,7 +1049,8 @@ protected String getExistingSaaSBoostEnvironment() {
}
}
try {
- ssm.getParameter(GetParameterRequest.builder().name("/saas-boost/" + environment + "/SAAS_BOOST_ENVIRONMENT").build());
+ String envParamsExist = "/saas-boost/" + environment + "/STACK_NAME";
+ ssm.getParameter(request -> request.name(envParamsExist));
} catch (ParameterNotFoundException ssmError) {
outputMessage("Cannot find existing SaaS Boost environment " + environment
+ " in this AWS account and region.");
@@ -1476,10 +1059,10 @@ protected String getExistingSaaSBoostEnvironment() {
return environment;
}
- protected static boolean validateEmail(String emailAddress) {
+ protected static boolean validateEmail(String email) {
boolean valid = false;
- if (emailAddress != null) {
- valid = emailAddress.matches("^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$");
+ if (email != null) {
+ valid = email.matches("^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$");
}
return valid;
}
@@ -1506,6 +1089,14 @@ protected static boolean validateDomainName(String domain) {
return valid;
}
+ protected static boolean validateAwsAccountId(String accountId) {
+ boolean valid = false;
+ if (accountId != null) {
+ valid = accountId.replace("-", "").matches("^[0-9]{12}$");
+ }
+ return valid;
+ }
+
protected static List existingHostedZones(Route53Client route53, String domain) {
List hostedZones = new ArrayList<>();
String nextDnsName = null;
@@ -1596,8 +1187,11 @@ protected void processLambdas() {
// Now add the separate layers directories to the list so we can upload the lambda
// package to S3 below. Build utils before anything else.
sourceDirectories.add(workingDir.resolve(Path.of("layers", "utils")));
+ // TODO make this a list of everything in the layers folder that's not utils
sourceDirectories.add(workingDir.resolve(Path.of("layers", "apigw-helper")));
sourceDirectories.add(workingDir.resolve(Path.of("layers", "cloudformation-utils")));
+ sourceDirectories.add(workingDir.resolve(Path.of("layers", "keycloak-helper")));
+ sourceDirectories.add(workingDir.resolve(Path.of("layers", "saas-boost-api-client-helper")));
DirectoryStream functions = Files.newDirectoryStream(workingDir.resolve(Path.of("functions")), Files::isDirectory);
functions.forEach(sourceDirectories::add);
@@ -1608,14 +1202,19 @@ protected void processLambdas() {
DirectoryStream services = Files.newDirectoryStream(workingDir.resolve(Path.of("services")), Files::isDirectory);
services.forEach(sourceDirectories::add);
- sourceDirectories.add(workingDir.resolve(Path.of("metering-billing", "lambdas")));
-
final PathMatcher filter = FileSystems.getDefault().getPathMatcher("glob:**.zip");
outputMessage("Uploading " + sourceDirectories.size() + " Lambda functions to S3");
for (ListIterator iter = sourceDirectories.listIterator(); iter.hasNext();) {
int progress = iter.nextIndex();
Path sourceDirectory = iter.next();
- if (Files.exists(sourceDirectory.resolve("pom.xml"))) {
+ if (sourceDirectory.endsWith("saas-boost-api-client-helper")) {
+ executeCommand("sh build.sh", null, sourceDirectory.toFile());
+ Path zipFile = sourceDirectory.resolve("build/SaaSBoostApiClientHelper-lambda.zip");
+ LOGGER.info("Uploading Lambda source package to S3 " + zipFile.toString() + " -> " + this.lambdaSourceFolder + "/" + zipFile.getFileName().toString());
+ System.out.printf("%2d. %s%n", (progress + 1), zipFile.getFileName().toString());
+ saasBoostArtifactsBucket.putFile(s3, zipFile,
+ Path.of(this.lambdaSourceFolder, zipFile.getFileName().toString()));
+ } else if (Files.exists(sourceDirectory.resolve("pom.xml"))) {
executeCommand("mvn", null, sourceDirectory.toFile());
final Path targetDir = sourceDirectory.resolve("target");
try (Stream stream = Files.list(targetDir)) {
@@ -1630,12 +1229,12 @@ protected void processLambdas() {
}
}
} else {
- LOGGER.warn("No POM file found in {}", sourceDirectory.toString());
+ LOGGER.warn("No POM file found in {}", sourceDirectory);
}
}
} catch (IOException ioe) {
LOGGER.error("Error processing Lambda source folders", ioe);
- LOGGER.error(getFullStackTrace(ioe));
+ LOGGER.error(Utils.getFullStackTrace(ioe));
throw new RuntimeException(ioe);
}
}
@@ -1643,7 +1242,7 @@ protected void processLambdas() {
protected void createSaaSBoostStack(final String stackName, String adminEmail, String systemIdentityProvider,
String identityProviderCustomDomain, String identityProviderHostedZone,
String identityProviderCertificate, String adminWebAppCustomDomain,
- String adminWebAppHostedZone, String adminWebAppCertificate) {
+ String adminWebAppHostedZone, String adminWebAppCertificate, String appPlaneAccount) {
// Note - most params the default is used from the CloudFormation stack
List templateParameters = new ArrayList<>();
templateParameters.add(Parameter.builder().parameterKey("Environment").parameterValue(envName).build());
@@ -1660,7 +1259,8 @@ protected void createSaaSBoostStack(final String stackName, String adminEmail, S
//templateParameters.add(Parameter.builder().parameterKey("ApiDomain").parameterValue(Objects.toString(apiCustomDomaine, "")).build());
//templateParameters.add(Parameter.builder().parameterKey("ApiHostedZone").parameterValue(Objects.toString(apiHostedZone, "")).build());
//templateParameters.add(Parameter.builder().parameterKey("ApiCertificate").parameterValue(Objects.toString(apiCertificate, "")).build());
- templateParameters.add(Parameter.builder().parameterKey("CreateMacroResources").parameterValue(Boolean.toString(!doesCfnMacroResourceExist())).build());
+ //templateParameters.add(Parameter.builder().parameterKey("CreateMacroResources").parameterValue(Boolean.toString(!doesCfnMacroResourceExist())).build());
+ templateParameters.add(Parameter.builder().parameterKey("AppPlaneAccountId").parameterValue(appPlaneAccount).build());
LOGGER.info("createSaaSBoostStack::create stack " + stackName);
String stackId = null;
@@ -1668,8 +1268,6 @@ protected void createSaaSBoostStack(final String stackName, String adminEmail, S
CreateStackResponse cfnResponse = cfn.createStack(CreateStackRequest.builder()
.stackName(stackName)
.disableRollback(true)
- //.onFailure("DO_NOTHING") // TODO bug on roll back?
- //.timeoutInMinutes(90)
.capabilitiesWithStrings("CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND")
.templateURL(saasBoostArtifactsBucket.getBucketUrl() + "saas-boost.yaml")
.parameters(templateParameters)
@@ -1701,67 +1299,7 @@ protected void createSaaSBoostStack(final String stackName, String adminEmail, S
} while (!stackCompleted);
} catch (SdkServiceException cfnError) {
LOGGER.error("cloudformation error", cfnError);
- LOGGER.error(getFullStackTrace(cfnError));
- throw cfnError;
- }
- }
-
- protected void createMetricsStack(final String stackName, final String dbPasswordSsmParameter, final String databaseName) {
- LOGGER.info("Creating CloudFormation stack {} with database name {}", stackName, databaseName);
- List templateParameters = new ArrayList<>();
- templateParameters.add(Parameter.builder().parameterKey("Environment").parameterValue(this.envName).build());
- templateParameters.add(Parameter.builder().parameterKey("LambdaSourceFolder").parameterValue(this.lambdaSourceFolder).build());
- templateParameters.add(Parameter.builder().parameterKey("MetricUserPasswordSSMParameter").parameterValue(dbPasswordSsmParameter).build());
- templateParameters.add(Parameter.builder().parameterKey("SaaSBoostBucket").parameterValue(saasBoostArtifactsBucket.getBucketName()).build());
- templateParameters.add(Parameter.builder().parameterKey("LoggingBucket").parameterValue(baseStackDetails.get("LoggingBucket")).build());
- templateParameters.add(Parameter.builder().parameterKey("DatabaseName").parameterValue(databaseName).build());
- templateParameters.add(Parameter.builder().parameterKey("PublicSubnet1").parameterValue(baseStackDetails.get("PublicSubnet1")).build());
- templateParameters.add(Parameter.builder().parameterKey("PublicSubnet2").parameterValue(baseStackDetails.get("PublicSubnet2")).build());
- templateParameters.add(Parameter.builder().parameterKey("PrivateSubnet1").parameterValue(baseStackDetails.get("PrivateSubnet1")).build());
- templateParameters.add(Parameter.builder().parameterKey("PrivateSubnet2").parameterValue(baseStackDetails.get("PrivateSubnet2")).build());
- templateParameters.add(Parameter.builder().parameterKey("VPC").parameterValue(baseStackDetails.get("EgressVpc")).build());
-
- // Now run the stack to provision the infrastructure for Metrics and Analytics
- LOGGER.info("createMetricsStack::stack " + stackName);
-
- String stackId;
- try {
- CreateStackResponse cfnResponse = cfn.createStack(CreateStackRequest.builder()
- .stackName(stackName)
- //.onFailure("DO_NOTHING") // TODO bug on roll back?
- //.timeoutInMinutes(90)
- .capabilitiesWithStrings("CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND")
- .templateURL(saasBoostArtifactsBucket.getBucketUrl() + "saas-boost-metrics-analytics.yaml")
- .parameters(templateParameters)
- .build()
- );
- stackId = cfnResponse.stackId();
- LOGGER.info("createMetricsStack::stack id " + stackId);
-
- boolean stackCompleted = false;
- long sleepTime = 5L;
- do {
- DescribeStacksResponse response = cfn.describeStacks(request -> request.stackName(stackName));
- Stack stack = response.stacks().get(0);
- if ("CREATE_COMPLETE".equalsIgnoreCase(stack.stackStatusAsString())) {
- outputMessage("CloudFormation stack: " + stackName + " completed successfully.");
- stackCompleted = true;
- } else if ("CREATE_FAILED".equalsIgnoreCase(stack.stackStatusAsString())) {
- outputMessage("CloudFormation stack: " + stackName + " failed.");
- throw new RuntimeException("Error with CloudFormation stack " + stackName + ". Check the events in the AWS CloudFormation Console");
- } else {
- outputMessage("Awaiting CloudFormation Stack " + stackName + " to complete. Sleep " + sleepTime + " minute(s)...");
- try {
- Thread.sleep(sleepTime * 60 * 1000);
- } catch (Exception e) {
- LOGGER.error("Error with sleep");
- }
- sleepTime = 1L; //set to 1 minute after kick off of 5 minute
- }
- } while (!stackCompleted);
- } catch (SdkServiceException cfnError) {
- LOGGER.error("cloudformation error", cfnError);
- LOGGER.error(getFullStackTrace(cfnError));
+ LOGGER.error(Utils.getFullStackTrace(cfnError));
throw cfnError;
}
}
@@ -1778,7 +1316,7 @@ protected void deleteCloudFormationStack(final String stackName) {
}
} catch (SdkServiceException cfnError) {
LOGGER.error("cloudformation:DescribeStacks error", cfnError);
- LOGGER.error(getFullStackTrace(cfnError));
+ LOGGER.error(Utils.getFullStackTrace(cfnError));
throw cfnError;
}
try {
@@ -1815,14 +1353,14 @@ protected void deleteCloudFormationStack(final String stackName) {
} catch (SdkServiceException cfnError) {
if (!cfnError.getMessage().contains("does not exist")) {
LOGGER.error("cloudformation:DescribeStacks error", cfnError);
- LOGGER.error(getFullStackTrace(cfnError));
+ LOGGER.error(Utils.getFullStackTrace(cfnError));
throw cfnError;
}
}
}
} catch (SdkServiceException cfnError) {
LOGGER.error("cloudformation:DeleteStack error", cfnError);
- LOGGER.error(getFullStackTrace(cfnError));
+ LOGGER.error(Utils.getFullStackTrace(cfnError));
throw cfnError;
}
}
@@ -1836,7 +1374,7 @@ protected boolean checkCloudFormationStack(final String stackName) {
} catch (SdkServiceException cfnError) {
if (!cfnError.getMessage().contains("does not exist")) {
LOGGER.error("cloudformation:DescribeStacks error", cfnError);
- LOGGER.error(getFullStackTrace(cfnError));
+ LOGGER.error(Utils.getFullStackTrace(cfnError));
throw cfnError;
}
}
@@ -1851,7 +1389,7 @@ public static void copyAdminWebAppSourceToS3(Path workingDir, String artifactsBu
}
// Sync files to the web bucket
- outputMessage("Synchronizing AWS SaaS Boost web application files to s3 web bucket");
+ outputMessage("Synchronizing AWS SaaS Boost web application files to s3");
List filesToUpload;
try (Stream stream = Files.walk(webDir)) {
filesToUpload = stream
@@ -1892,7 +1430,7 @@ public static void copyAdminWebAppSourceToS3(Path workingDir, String artifactsBu
);
} catch (SdkServiceException s3Error) {
LOGGER.error("s3:PutObject error", s3Error);
- LOGGER.error(getFullStackTrace(s3Error));
+ LOGGER.error(Utils.getFullStackTrace(s3Error));
throw s3Error;
}
} catch (IOException ioe) {
@@ -1901,7 +1439,77 @@ public static void copyAdminWebAppSourceToS3(Path workingDir, String artifactsBu
}
} catch (IOException ioe) {
LOGGER.error("Error walking client/web directory", ioe);
- LOGGER.error(getFullStackTrace(ioe));
+ LOGGER.error(Utils.getFullStackTrace(ioe));
+ throw new RuntimeException(ioe);
+ }
+ }
+
+ public static void copyApiDocsSourceToS3(Path workingDir, String artifactsBucket, S3Client s3) {
+ Path webDir = workingDir.resolve(Path.of("resources", "api-docs"));
+ if (!Files.isDirectory(webDir)) {
+ outputMessage("Error, can't find resources/api-docs directory at " + webDir.toAbsolutePath().toString());
+ System.exit(2);
+ }
+
+ // Sync files to the web bucket
+ outputMessage("Synchronizing AWS SaaS Boost API Docs (Swagger) files to s3");
+ List filesToUpload;
+ try (Stream stream = Files.walk(webDir)) {
+ filesToUpload = stream
+ .filter(file ->
+ Files.isRegularFile(file) && (
+ file.startsWith("resources/api-docs/app.js")
+ || file.startsWith("resources/api-docs/package.json")
+ || file.startsWith("resources/api-docs/package-lock.json")
+ || file.startsWith("resources/api-docs/buildspec_no_post_build.yaml")
+ || file.startsWith("resources/api-docs/update.sh"))
+ )
+ .collect(Collectors.toList());
+ outputMessage("Uploading " + filesToUpload.size() + " files to S3");
+
+ // Create a ZIP archive of the source files so we only call s3 put object once
+ // and so we can trigger the CodeBuild project off of that single s3 event
+ // (instead of triggering CodeBuild 180+ times -- once for each file put to s3).
+ try {
+ ByteArrayOutputStream src = new ByteArrayOutputStream();
+ ZipOutputStream zip = new ZipOutputStream(src);
+ for (Path fileToUpload : filesToUpload) {
+ // java.nio.file.Path will use OS dependent file separators
+ String fileName = fileToUpload.toFile().toString().replace('\\', '/');
+ ZipEntry entry = new ZipEntry(fileName);
+ zip.putNextEntry(entry);
+ zip.write(Files.readAllBytes(fileToUpload)); // all of our files are very small
+ zip.closeEntry();
+ }
+ zip.close();
+ try {
+ // Now copy the Swagger source files up to the artifacts bucket
+ // This will trigger a CodeBuild project to build and deploy the app
+ // if done after the initial install of SaaS Boost
+ s3.putObject(PutObjectRequest.builder()
+ .bucket(artifactsBucket)
+ .key("api-docs/src.zip")
+ .build(), RequestBody.fromBytes(src.toByteArray())
+ );
+ // Copy the swagger definition file up to the artifacts bucket separately
+ // because we use it as an event source to trigger future builds of the
+ // api docs site
+ s3.putObject(PutObjectRequest.builder()
+ .bucket(artifactsBucket)
+ .key("api-docs/swagger.json")
+ .build(), workingDir.resolve(Path.of("resources", "api-docs", "swagger.json")));
+ } catch (SdkServiceException s3Error) {
+ LOGGER.error("s3:PutObject error", s3Error);
+ LOGGER.error(Utils.getFullStackTrace(s3Error));
+ throw s3Error;
+ }
+ } catch (IOException ioe) {
+ LOGGER.error("ZIP archive generation failed");
+ throw new RuntimeException(Utils.getFullStackTrace(ioe));
+ }
+ } catch (IOException ioe) {
+ LOGGER.error("Error walking resources/api-docs directory", ioe);
+ LOGGER.error(Utils.getFullStackTrace(ioe));
throw new RuntimeException(ioe);
}
}
@@ -1923,7 +1531,7 @@ public static void executeCommand(String command, String[] environment, File dir
printResults(process);
} catch (Exception e) {
LOGGER.error("Error running command: " + command);
- LOGGER.error(getFullStackTrace(e));
+ LOGGER.error(Utils.getFullStackTrace(e));
throw new RuntimeException("Error running command: " + command);
}
@@ -1944,38 +1552,6 @@ public static void executeCommand(String command, String[] environment, File dir
process.destroy();
}
- protected LinkedHashMap getQuickSightUsers() {
- LOGGER.info("Load Quicksight users");
- LinkedHashMap users = new LinkedHashMap<>();
- try {
- String nextToken = null;
- do {
- ListUsersResponse response = quickSight.listUsers(ListUsersRequest.builder()
- .awsAccountId(accountId)
- .namespace("default")
- .nextToken(nextToken)
- .build()
- );
- if (response.hasUserList()) {
- for (User quickSightUser : response.userList()) {
- users.put(quickSightUser.userName(), quickSightUser);
- }
- }
- nextToken = response.nextToken();
- } while (nextToken != null);
- } catch (SdkServiceException quickSightError) {
- LOGGER.error("quickSight:ListUsers error {}", quickSightError.getMessage());
- LOGGER.error(getFullStackTrace(quickSightError));
- throw quickSightError;
- }
- LOGGER.info("Completed load of QuickSight users");
- return users;
- }
-
- protected String analyticsStackName() {
- return this.stackName + "-analytics";
- }
-
protected static void cleanUpS3(S3Client s3, String bucket, String prefix) {
// The list of objects in the bucket to delete
List toDelete = new ArrayList<>();
@@ -1983,7 +1559,8 @@ protected static void cleanUpS3(S3Client s3, String bucket, String prefix) {
prefix = prefix + "/";
}
GetBucketVersioningResponse versioningResponse = s3.getBucketVersioning(request -> request.bucket(bucket));
- if (BucketVersioningStatus.ENABLED == versioningResponse.status() || BucketVersioningStatus.SUSPENDED == versioningResponse.status()) {
+ if (BucketVersioningStatus.ENABLED == versioningResponse.status()
+ || BucketVersioningStatus.SUSPENDED == versioningResponse.status()) {
LOGGER.info("Bucket " + bucket + " is versioned (" + versioningResponse.status() + ")");
ListObjectVersionsResponse listObjectResponse;
String keyMarker = null;
@@ -2021,6 +1598,15 @@ protected static void cleanUpS3(S3Client s3, String bucket, String prefix) {
.build()
)
.forEachOrdered(toDelete::add);
+ listObjectResponse.deleteMarkers()
+ .stream()
+ .map(marker ->
+ ObjectIdentifier.builder()
+ .key(marker.key())
+ .versionId(marker.versionId())
+ .build()
+ )
+ .forEachOrdered(toDelete::add);
} while (listObjectResponse.isTruncated());
} else {
LOGGER.info("Bucket " + bucket + " is not versioned (" + versioningResponse.status() + ")");
@@ -2078,50 +1664,6 @@ protected static void cleanUpS3(S3Client s3, String bucket, String prefix) {
}
}
- private boolean doesCfnMacroResourceExist() {
- // this assumes that the macro resource exists in CloudFormation if and only if all requisite resources also
- // exist, i.e. the macro Lambda function, execution role, and log group. this should always be true, since the
- // macro resource will never be deleted unless each of the others are deleted thanks to CloudFormation
- // dependency analysis
- List stackNamesToCheck = new ArrayList<>();
- String paginationToken = null;
- do {
- ListStacksResponse listStacksResponse = cfn.listStacks(
- ListStacksRequest.builder().nextToken(paginationToken).build());
- stackNamesToCheck.addAll(listStacksResponse.stackSummaries().stream()
- .filter(summary -> summary.stackStatus() != StackStatus.DELETE_COMPLETE
- && summary.stackStatus() != StackStatus.DELETE_IN_PROGRESS)
- .map(StackSummary::stackName)
- .collect(Collectors.toList()));
- paginationToken = listStacksResponse.nextToken();
- } while (paginationToken != null);
- // for each stack, look for Macro Resource (either by listing all or getResource by logical id)
- for (String stackName : stackNamesToCheck) {
- try {
- StackResourceDetail stackResourceDetail = cfn.describeStackResource(request -> request
- .stackName(stackName)
- .logicalResourceId("ApplicationServicesMacro")).stackResourceDetail();
- if (stackResourceDetail.resourceStatus() != ResourceStatus.DELETE_COMPLETE) {
- LOGGER.info("Found the ApplicationServicesMacro resource in {}", stackName);
- return true;
- }
- } catch (CloudFormationException cfne) {
- if (cfne.getMessage().contains("Stack '" + stackName + "' does not exist")) {
- // if stacks are being deleted
- }
- }
- }
- LOGGER.info("Could not find any ApplicationServicesMacro resource");
- return false;
- }
-
- public static String getFullStackTrace(Exception e) {
- final StringWriter sw = new StringWriter();
- final PrintWriter pw = new PrintWriter(sw, true);
- e.printStackTrace(pw);
- return sw.getBuffer().toString();
- }
-
public static boolean isWindows() {
return (OS.contains("win"));
}
@@ -2130,32 +1672,65 @@ public static boolean isMac() {
return (OS.contains("mac"));
}
- /**
- * Generate a random password that matches the password policy of the Cognito user pool
- * @return a random password that matches the password policy of the Cognito user pool
- */
- public static String generatePassword(int passwordLength) {
- if (passwordLength < 8) {
- throw new IllegalArgumentException("Invalid password length. Minimum of 8 characters is required.");
- }
-
- // Split the classes of characters into separate buckets so we can be sure to use
- // the correct amount of each type
- final char[][] requiredCharacterBuckets = {
- {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'},
- {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'},
- {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}
- };
-
- Random random = new Random();
- StringBuilder password = new StringBuilder(passwordLength);
+ protected String quickCreateLink() {
+ StringBuilder quickCreateLink = new StringBuilder();
+ quickCreateLink.append("https://");
+ quickCreateLink.append(AWS_REGION);
+ quickCreateLink.append(".console.aws.amazon.com/cloudformation/home?region=");
+ quickCreateLink.append(AWS_REGION);
+ quickCreateLink.append("#/stacks/create/review?");
+ quickCreateLink.append("templateURL=");
+ quickCreateLink.append(saasBoostArtifactsBucket.getBucketUrl());
+ quickCreateLink.append("saas-boost-app-integration.yaml");
+ quickCreateLink.append("&stackName=");
+ quickCreateLink.append("sb-");
+ quickCreateLink.append(envName);
+ quickCreateLink.append("-integration");
+ quickCreateLink.append("¶m_Environment=");
+ quickCreateLink.append(envName);
+ Map params = getQuickCreateLinkParameters();
+ quickCreateLink.append("¶m_EventBusArn=");
+ quickCreateLink.append(params.get("EVENT_BUS"));
+ quickCreateLink.append("¶m_ApiAppClientSecretArn=");
+ quickCreateLink.append(params.get("API_APP_CLIENT_SECRET"));
+ quickCreateLink.append("¶m_EncryptionKeyArn=");
+ quickCreateLink.append(params.get("API_APP_CLIENT_KEY"));
+ quickCreateLink.append("¶m_UtilsLayerArn=");
+ quickCreateLink.append(params.get("UTILS_LAYER"));
+ quickCreateLink.append("¶m_CloudFormationUtilsLayerArn=");
+ quickCreateLink.append(params.get("CFN_UTILS_LAYER"));
+ quickCreateLink.append("¶m_ApiHelperLayerArn=");
+ quickCreateLink.append(params.get("API_CLIENT_HELPER_LAYER"));
+ return quickCreateLink.toString();
+ }
- // Randomly select one character from each of the required character types
- for (char[] requiredCharacterBucket : requiredCharacterBuckets) {
- password.append(requiredCharacterBucket[random.nextInt(requiredCharacterBucket.length)]);
+ protected Map getQuickCreateLinkParameters() {
+ Map params = new HashMap<>();
+ try {
+ GetParametersResponse response = ssm.getParameters(request -> request
+ .names(List.of(
+ "/saas-boost/" + envName + "/EVENT_BUS",
+ "/saas-boost/" + envName + "/API_APP_CLIENT_SECRET",
+ "/saas-boost/" + envName + "/API_APP_CLIENT_KEY",
+ "/saas-boost/" + envName + "/UTILS_LAYER",
+ "/saas-boost/" + envName + "/CFN_UTILS_LAYER",
+ "/saas-boost/" + envName + "/API_CLIENT_HELPER_LAYER"
+ ))
+ );
+ response.parameters()
+ .stream()
+ .forEach(parameter -> params.put(
+ parameter.name().substring(parameter.name().lastIndexOf("/") + 1), parameter.value()));
+ // ParameterStore only has the name of the event bus, but we need the whole ARN
+ params.put("EVENT_BUS",
+ "arn:" + AWS_REGION.metadata().partition().id() + ":events:" + AWS_REGION.id()
+ + ":" + accountId + ":event-bus/" + params.get("EVENT_BUS"));
+ } catch (SdkServiceException ssmError) {
+ LOGGER.error("ssm getParameters failed", ssmError);
+ LOGGER.error(Utils.getFullStackTrace(ssmError));
+ throw ssmError;
}
-
- // build the remaining password using Utils.randomString
- return password.append(Utils.randomString(passwordLength - requiredCharacterBuckets.length)).toString();
+ return params;
}
+
}
diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactory.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactory.java
deleted file mode 100644
index 98fc477b..00000000
--- a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactory.java
+++ /dev/null
@@ -1,247 +0,0 @@
-/**
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.amazon.aws.partners.saasfactory.saasboost.clients;
-
-import com.amazon.aws.partners.saasfactory.saasboost.Utils;
-import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
-import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder;
-import software.amazon.awssdk.awscore.retry.AwsRetryPolicy;
-import software.amazon.awssdk.core.SdkClient;
-import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
-import software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting;
-import software.amazon.awssdk.core.retry.RetryPolicy;
-import software.amazon.awssdk.core.retry.backoff.BackoffStrategy;
-import software.amazon.awssdk.core.retry.conditions.RetryCondition;
-import software.amazon.awssdk.regions.Region;
-import software.amazon.awssdk.services.acm.AcmClient;
-import software.amazon.awssdk.services.acm.AcmClientBuilder;
-import software.amazon.awssdk.services.apigateway.ApiGatewayClient;
-import software.amazon.awssdk.services.apigateway.ApiGatewayClientBuilder;
-import software.amazon.awssdk.services.apigateway.model.CreateDeploymentRequest;
-import software.amazon.awssdk.services.cloudformation.CloudFormationClient;
-import software.amazon.awssdk.services.cloudformation.CloudFormationClientBuilder;
-import software.amazon.awssdk.services.ecr.EcrClient;
-import software.amazon.awssdk.services.ecr.EcrClientBuilder;
-import software.amazon.awssdk.services.iam.IamClient;
-import software.amazon.awssdk.services.iam.IamClientBuilder;
-import software.amazon.awssdk.services.lambda.LambdaClient;
-import software.amazon.awssdk.services.lambda.LambdaClientBuilder;
-import software.amazon.awssdk.services.quicksight.QuickSightClient;
-import software.amazon.awssdk.services.quicksight.QuickSightClientBuilder;
-import software.amazon.awssdk.services.route53.Route53Client;
-import software.amazon.awssdk.services.route53.Route53ClientBuilder;
-import software.amazon.awssdk.services.s3.S3Client;
-import software.amazon.awssdk.services.s3.S3ClientBuilder;
-import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
-import software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder;
-import software.amazon.awssdk.services.ssm.SsmClient;
-import software.amazon.awssdk.services.ssm.SsmClientBuilder;
-import software.amazon.awssdk.services.sts.StsClient;
-import software.amazon.awssdk.services.sts.StsClientBuilder;
-
-import java.net.URI;
-import java.time.Duration;
-
-public class AwsClientBuilderFactory {
-
- private static final AwsCredentialsProvider DEFAULT_CREDENTIALS_PROVIDER =
- RefreshingProfileDefaultCredentialsProvider.builder().build();
-
- private final Region awsRegion;
- private final AwsCredentialsProvider credentialsProvider;
-
- private ApiGatewayClientBuilder cachedApiGatewayBuilder;
- private CloudFormationClientBuilder cachedCloudFormationBuilder;
- private EcrClientBuilder cachedEcrBuilder;
- private IamClientBuilder cachedIamBuilder;
- private LambdaClientBuilder cachedLambdaBuilder;
- private QuickSightClientBuilder cachedQuickSightBuilder;
- private S3ClientBuilder cachedS3Builder;
- private SsmClientBuilder cachedSsmBuilder;
- private StsClientBuilder cachedStsBuilder;
- private SecretsManagerClientBuilder cachedSecretsManagerClientBuilder;
- private Route53ClientBuilder cachedRoute53ClientBuilder;
- private AcmClientBuilder cachedAcmClientBuilder;
-
- AwsClientBuilderFactory() {
- // for testing
- this.awsRegion = null;
- this.credentialsProvider = null;
- }
-
- private AwsClientBuilderFactory(Builder builder) {
- // passing no region or a null region to any of the AWS Client Builders
- // leads to the default region from the configured profile being used
- this.awsRegion = builder.defaultRegion;
- this.credentialsProvider = builder.awsCredentialsProvider != null
- ? builder.awsCredentialsProvider
- : DEFAULT_CREDENTIALS_PROVIDER;
- }
-
- // VisibleForTesting
- > B decorateBuilderWithDefaults(B builder) {
- return builder
- .credentialsProvider(credentialsProvider)
- .region(awsRegion);
- }
-
- public ApiGatewayClientBuilder apiGatewayBuilder() {
- if (cachedApiGatewayBuilder == null) {
- // override throttling policy to wait 5 seconds if we're throttled on CreateDeployment
- // https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html
- cachedApiGatewayBuilder = decorateBuilderWithDefaults(ApiGatewayClient.builder())
- .overrideConfiguration(config -> config.retryPolicy(AwsRetryPolicy.addRetryConditions(
- RetryPolicy.builder().throttlingBackoffStrategy(retryPolicyContext -> {
- if (retryPolicyContext.originalRequest() instanceof CreateDeploymentRequest) {
- return Duration.ofSeconds(5);
- }
- return null;
- }).build())));
- }
-
- return cachedApiGatewayBuilder;
- }
-
- public CloudFormationClientBuilder cloudFormationBuilder() {
- if (cachedCloudFormationBuilder == null) {
- cachedCloudFormationBuilder = decorateBuilderWithDefaults(CloudFormationClient.builder());
- }
- return cachedCloudFormationBuilder;
- }
-
- public EcrClientBuilder ecrBuilder() {
- if (cachedEcrBuilder == null) {
- cachedEcrBuilder = decorateBuilderWithDefaults(EcrClient.builder());
- }
- return cachedEcrBuilder;
- }
-
- public IamClientBuilder iamBuilder() {
- if (cachedIamBuilder == null) {
- Region region = Region.of(System.getenv("AWS_REGION"));
- if (Utils.isChinaRegion(region)) {
- // China's IAM endpoints point to Beijing region
- // See https://docs.amazonaws.cn/en_us/aws/latest/userguide/iam.html
- cachedIamBuilder = decorateBuilderWithDefaults(IamClient.builder()).region(Region.AWS_CN_GLOBAL);
- } else {
- // IAM in the commercial regions use the AWS_GLOBAL
- // ref: https://docs.aws.amazon.com/general/latest/gr/iam-service.html
- cachedIamBuilder = decorateBuilderWithDefaults(IamClient.builder()).region(Region.AWS_GLOBAL);
- }
- }
- return cachedIamBuilder;
- }
-
- public LambdaClientBuilder lambdaBuilder() {
- if (cachedLambdaBuilder == null) {
- cachedLambdaBuilder = decorateBuilderWithDefaults(LambdaClient.builder());
- }
- return cachedLambdaBuilder;
- }
-
- public QuickSightClientBuilder quickSightBuilder() {
- if (cachedQuickSightBuilder == null) {
- cachedQuickSightBuilder = decorateBuilderWithDefaults(QuickSightClient.builder());
- }
- return cachedQuickSightBuilder;
- }
-
- public S3ClientBuilder s3Builder() {
- if (cachedS3Builder == null) {
- cachedS3Builder = decorateBuilderWithDefaults(S3Client.builder());
- }
- return cachedS3Builder;
- }
-
- public SsmClientBuilder ssmBuilder() {
- if (cachedSsmBuilder == null) {
- cachedSsmBuilder = decorateBuilderWithDefaults(SsmClient.builder());
- }
- return cachedSsmBuilder;
- }
-
- public StsClientBuilder stsBuilder() {
- if (cachedStsBuilder == null) {
- cachedStsBuilder = decorateBuilderWithDefaults(StsClient.builder());
- }
- return cachedStsBuilder;
- }
-
- public SecretsManagerClientBuilder secretsManagerBuilder() {
- if (cachedSecretsManagerClientBuilder == null) {
- cachedSecretsManagerClientBuilder = decorateBuilderWithDefaults(SecretsManagerClient.builder());
- }
- return cachedSecretsManagerClientBuilder;
- }
-
- public Route53ClientBuilder route53Builder() {
- if (cachedRoute53ClientBuilder == null) {
- // Route53 is a global service and uses a different region setting than the default
- Region region;
- String endpoint;
- Builder factory = builder()
- .region(Region.of(System.getenv("AWS_REGION")))
- .credentialsProvider(DEFAULT_CREDENTIALS_PROVIDER);
- if (!Utils.isChinaRegion(factory.defaultRegion)) {
- region = Region.US_EAST_1;
- endpoint = "https://route53.amazonaws.com";
- } else {
- region = Region.CN_NORTHWEST_1;
- endpoint = "https://route53.amazonaws.com.cn";
- }
- cachedRoute53ClientBuilder = Route53Client.builder()
- .region(region)
- .endpointOverride(URI.create(endpoint))
- .credentialsProvider(factory.awsCredentialsProvider);
- }
- return cachedRoute53ClientBuilder;
- }
-
- public AcmClientBuilder acmBuilder() {
- if (cachedAcmClientBuilder == null) {
- cachedAcmClientBuilder = decorateBuilderWithDefaults(AcmClient.builder());
- }
- return cachedAcmClientBuilder;
- }
-
- public static Builder builder() {
- return new Builder();
- }
-
- public static class Builder {
- private Region defaultRegion;
- private AwsCredentialsProvider awsCredentialsProvider;
-
- private Builder() {
-
- }
-
- public Builder region(Region defaultRegion) {
- this.defaultRegion = defaultRegion;
- return this;
- }
-
- public Builder credentialsProvider(AwsCredentialsProvider awsCredentialsProvider) {
- this.awsCredentialsProvider = awsCredentialsProvider;
- return this;
- }
-
- public AwsClientBuilderFactory build() {
- return new AwsClientBuilderFactory(this);
- }
- }
-}
diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/clients/RefreshingProfileDefaultCredentialsProvider.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/clients/RefreshingProfileDefaultCredentialsProvider.java
deleted file mode 100644
index 82e835ed..00000000
--- a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/clients/RefreshingProfileDefaultCredentialsProvider.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.amazon.aws.partners.saasfactory.saasboost.clients;
-
-import software.amazon.awssdk.auth.credentials.AwsCredentials;
-import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
-import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
-import software.amazon.awssdk.profiles.ProfileFile;
-
-import java.io.File;
-import java.nio.file.Path;
-
-/**
- * This class provides the exact same functionality as the {@link DefaultCredentialsProvider} but without any
- * caching to support profile files that refresh from outside the JVM. To be explicit, this
- * {@link RefreshingProfileDefaultCredentialsProvider} creates a new {@link DefaultCredentialsProvider} from scratch
- * each time {@link RefreshingProfileDefaultCredentialsProvider#resolveCredentials()} is called.
- *
- * In some cases (such as in Cloud9, see #137) the
- * credentials being returned by the credentials provider will expire and will not be able to be refreshed. For example,
- * if the credentials being used are coming from the default
.aws/credentials
file and are updated during the
- * lifetime of this process, the {@link software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider} will return
- * expired credentials, leading to SaaS Boost erroring out.
- *
- * This class addresses this case by creating a new {@link DefaultCredentialsProvider} (note, explicitly using the
- * builder, because the .create() function returns a static singleton) each time resolve credentials is installed.
- * This ensures that any new credentials added to the configured profile will be picked up for any resolve credentials
- * call.
- *
- * This obviously comes with a performance hit: we're creating a new object each time resolveCredentials is called
- * rather than relying on in-memory values. In experimentation this equates to a roughly 100x difference in
- * performance: the refreshingCredentialsProvider will average ~0.1-0.2 ms per resolveCredentials call vs the
- * Default in-memory's ~0.0001ms runtime (YMMV based on CPU clock speed). We have considered this performance change
- * to be acceptable: resolveCredentials should only be called once per SDK call, and so this is equivalent to a
- * linear difference in runtime performance of the installer, likely adding significantly less than one second to
- * an already very long-running process (on the order of minutes to upload Lambda function artifacts to S3 and wait
- * for CloudFormation templates to finish).
- *
- * @see SaaS Boost Issue #137
- * @see AWS Java SDK v2 Issue #1754
- */
-public class RefreshingProfileDefaultCredentialsProvider implements AwsCredentialsProvider {
- private final String profileFilename;
- private final DefaultCredentialsProvider.Builder curriedBuilder;
-
- private RefreshingProfileDefaultCredentialsProvider(RefreshingProfileDefaultCredentialsProvider.Builder builder) {
- curriedBuilder = DefaultCredentialsProvider.builder();
- curriedBuilder.reuseLastProviderEnabled(builder.reuseLastProviderEnabled);
- curriedBuilder.asyncCredentialUpdateEnabled(builder.asyncCredentialUpdateEnabled);
- curriedBuilder.profileName(builder.profileName);
- profileFilename = builder.profileFilename;
- }
-
- /**
- * @see AwsCredentialsProvider#resolveCredentials()
- */
- @Override
- public AwsCredentials resolveCredentials() {
- if (profileFilename == null) {
- return curriedBuilder.build().resolveCredentials();
- }
- curriedBuilder.profileFile(ProfileFile.builder()
- .type(ProfileFile.Type.CREDENTIALS)
- .content(Path.of(new File(profileFilename).toURI()))
- .build());
- return curriedBuilder.build().resolveCredentials();
- }
-
- public static Builder builder() {
- return new Builder();
- }
-
- /**
- * @see DefaultCredentialsProvider.Builder
- */
- public static class Builder {
- private String profileFilename;
- private String profileName;
- private boolean reuseLastProviderEnabled;
- private boolean asyncCredentialUpdateEnabled;
-
- private Builder() {
-
- }
-
- public RefreshingProfileDefaultCredentialsProvider.Builder profileFilename(String profileFilename) {
- this.profileFilename = profileFilename;
- return this;
- }
-
- public RefreshingProfileDefaultCredentialsProvider.Builder profileName(String profileName) {
- this.profileName = profileName;
- return this;
- }
-
- public RefreshingProfileDefaultCredentialsProvider.Builder reuseLastProviderEnabled(
- Boolean reuseLastProviderEnabled) {
- this.reuseLastProviderEnabled = reuseLastProviderEnabled;
- return this;
- }
-
- public RefreshingProfileDefaultCredentialsProvider.Builder asyncCredentialUpdateEnabled(
- Boolean asyncCredentialUpdateEnabled) {
- this.asyncCredentialUpdateEnabled = asyncCredentialUpdateEnabled;
- return this;
- }
-
- public RefreshingProfileDefaultCredentialsProvider build() {
- return new RefreshingProfileDefaultCredentialsProvider(this);
- }
- }
-}
diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/Environment.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/Environment.java
index a8c81db1..8391d745 100644
--- a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/Environment.java
+++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/Environment.java
@@ -35,7 +35,6 @@ public final class Environment {
private String lambdasFolderName;
private String baseCloudFormationStackName;
private Map baseCloudFormationStackInfo;
- private boolean metricsAnalyticsDeployed;
private Environment(Builder b) {
this.name = b.name;
@@ -44,7 +43,6 @@ private Environment(Builder b) {
this.lambdasFolderName = b.lambdasFolderName;
this.baseCloudFormationStackName = b.baseCloudFormationStackName;
this.baseCloudFormationStackInfo = b.baseCloudFormationStackInfo;
- this.metricsAnalyticsDeployed = b.metricsAnalyticsDeployed;
}
public String getName() {
@@ -95,14 +93,6 @@ public void setBaseCloudFormationStackInfo(Map baseCloudFormatio
this.baseCloudFormationStackInfo = baseCloudFormationStackInfo;
}
- public boolean isMetricsAnalyticsDeployed() {
- return this.metricsAnalyticsDeployed;
- }
-
- public void setMetricsAnalyticsDeployed(boolean metricsAnalyticsDeployed) {
- this.metricsAnalyticsDeployed = metricsAnalyticsDeployed;
- }
-
/**
* Retrieves a new empty instance of the {@link Environment.Builder}.
*
@@ -139,7 +129,6 @@ public static final class Builder {
private String lambdasFolderName;
private String baseCloudFormationStackName;
private Map baseCloudFormationStackInfo;
- private boolean metricsAnalyticsDeployed;
private Builder() {
@@ -175,11 +164,6 @@ public Builder baseCloudFormationStackInfo(Map baseCloudFormatio
return this;
}
- public Builder metricsAnalyticsDeployed(boolean metricsAnalyticsDeployed) {
- this.metricsAnalyticsDeployed = metricsAnalyticsDeployed;
- return this;
- }
-
public Environment build() {
return new Environment(this);
}
diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/ExistingEnvironmentFactory.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/ExistingEnvironmentFactory.java
index af9d4165..c102e6b3 100644
--- a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/ExistingEnvironmentFactory.java
+++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/model/ExistingEnvironmentFactory.java
@@ -40,11 +40,8 @@
public final class ExistingEnvironmentFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(ExistingEnvironmentFactory.class);
- public static Environment findExistingEnvironment(
- SsmClient ssm,
- CloudFormationClient cfn,
- String environmentName,
- String accountId) {
+ public static Environment findExistingEnvironment(SsmClient ssm, CloudFormationClient cfn, String environmentName,
+ String accountId) {
if (Utils.isBlank(environmentName)) {
throw new EnvironmentLoadException("EnvironmentName cannot be blank.");
}
@@ -56,17 +53,14 @@ public static Environment findExistingEnvironment(
.baseCloudFormationStackName(baseCloudFormationStackName)
.baseCloudFormationStackInfo(getExistingSaaSBoostStackDetails(cfn, baseCloudFormationStackName))
.lambdasFolderName(getExistingSaaSBoostLambdasFolder(ssm, environmentName))
- .metricsAnalyticsDeployed(getExistingSaaSBoostAnalyticsDeployed(ssm, environmentName))
.name(environmentName)
.accountId(accountId)
.build();
}
// VisibleForTesting
- static SaaSBoostArtifactsBucket getExistingSaaSBoostArtifactBucket(
- SsmClient ssm,
- String environmentName,
- Region region) {
+ static SaaSBoostArtifactsBucket getExistingSaaSBoostArtifactBucket(SsmClient ssm, String environmentName,
+ Region region) {
LOGGER.debug("Getting existing SaaS Boost artifact bucket name from Parameter Store");
String artifactsBucket = null;
try {
@@ -95,7 +89,7 @@ static String getExistingSaaSBoostStackName(SsmClient ssm, String environmentNam
String stackName = null;
try {
GetParameterResponse response = ssm.getParameter(request -> request
- .name("/saas-boost/" + environmentName + "/SAAS_BOOST_STACK")
+ .name("/saas-boost/" + environmentName + "/STACK_NAME")
);
stackName = response.parameter().value();
} catch (ParameterNotFoundException paramStoreError) {
@@ -112,13 +106,12 @@ static String getExistingSaaSBoostStackName(SsmClient ssm, String environmentNam
}
// VisibleForTesting
- static Map getExistingSaaSBoostStackDetails(
- CloudFormationClient cfn,
- String baseCloudFormationStackName) {
+ static Map getExistingSaaSBoostStackDetails(CloudFormationClient cfn,
+ String baseCloudFormationStackName) {
LOGGER.debug("Getting CloudFormation stack details for SaaS Boost stack {}", baseCloudFormationStackName);
Map details = new HashMap<>();
- List requiredOutputs = List.of("PublicSubnet1", "PublicSubnet2", "PrivateSubnet1",
- "PrivateSubnet2", "EgressVpc", "LoggingBucket");
+ // TODO not sure we need this
+ List requiredOutputs = List.of("LoggingBucket");
try {
DescribeStacksResponse response = cfn.describeStacks(
request -> request.stackName(baseCloudFormationStackName));
@@ -153,12 +146,12 @@ static String getExistingSaaSBoostLambdasFolder(SsmClient ssm, String environmen
String lambdasFolder = null;
try {
GetParameterResponse response = ssm.getParameter(request -> request
- .name("/saas-boost/" + environmentName + "/SAAS_BOOST_LAMBDAS_FOLDER")
+ .name("/saas-boost/" + environmentName + "/LAMBDAS_FOLDER")
);
lambdasFolder = response.parameter().value();
} catch (ParameterNotFoundException paramStoreError) {
LOGGER.warn("Parameter /saas-boost/" + environmentName
- + "/SAAS_BOOST_LAMBDAS_FOLDER not found setting to default 'lambdas'");
+ + "/LAMBDAS_FOLDER not found setting to default 'lambdas'");
lambdasFolder = "lambdas";
} catch (SdkServiceException ssmError) {
LOGGER.error("ssm:GetParameter error {}", ssmError.getMessage());
@@ -169,24 +162,4 @@ static String getExistingSaaSBoostLambdasFolder(SsmClient ssm, String environmen
return lambdasFolder;
}
- // VisibleForTesting
- static boolean getExistingSaaSBoostAnalyticsDeployed(SsmClient ssm, String environmentName) {
- LOGGER.debug("Getting existing SaaS Boost Analytics module deployed from Parameter Store");
- boolean analyticsDeployed = false;
- try {
- GetParameterResponse response = ssm.getParameter(request -> request
- .name("/saas-boost/" + environmentName + "/METRICS_ANALYTICS_DEPLOYED")
- );
- analyticsDeployed = Boolean.parseBoolean(response.parameter().value());
- } catch (ParameterNotFoundException paramStoreError) {
- // this means the parameter doesn't exist, so ignore
- } catch (SdkServiceException ssmError) {
- // TODO CloudFormation should own this parameter, not the installer...
- LOGGER.error("ssm:GetParameter error {}", ssmError.getMessage());
- LOGGER.error(Utils.getFullStackTrace(ssmError));
- throw ssmError;
- }
- LOGGER.info("Loaded analytics deployed {}", analyticsDeployed);
- return analyticsDeployed;
- }
}
diff --git a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflow.java b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflow.java
index aa5802a5..40efd587 100644
--- a/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflow.java
+++ b/installer/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflow.java
@@ -21,7 +21,6 @@
import com.amazon.aws.partners.saasfactory.saasboost.Keyboard;
import com.amazon.aws.partners.saasfactory.saasboost.SaaSBoostInstall;
import com.amazon.aws.partners.saasfactory.saasboost.Utils;
-import com.amazon.aws.partners.saasfactory.saasboost.clients.AwsClientBuilderFactory;
import com.amazon.aws.partners.saasfactory.saasboost.model.Environment;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
@@ -38,6 +37,7 @@
import software.amazon.awssdk.services.cloudformation.model.StackStatus;
import software.amazon.awssdk.services.cloudformation.model.UpdateStackRequest;
import software.amazon.awssdk.services.cloudformation.model.UpdateStackResponse;
+import software.amazon.awssdk.services.s3.S3Client;
import java.io.BufferedReader;
import java.io.File;
@@ -63,18 +63,16 @@ public class UpdateWorkflow extends AbstractWorkflow {
private final Environment environment;
private final Path workingDir;
- private final AwsClientBuilderFactory clientBuilderFactory;
- private final boolean doesCfnMacroResourceExist;
-
- public UpdateWorkflow(
- Path workingDir,
- Environment environment,
- AwsClientBuilderFactory clientBuilderFactory,
- boolean doesCfnMacroResourceExist) {
+ private final S3Client s3;
+ private final CloudFormationClient cfn;
+ private final ApiGatewayClient apigw;
+
+ public UpdateWorkflow(Path workingDir, Environment environment, S3Client s3, CloudFormationClient cfn, ApiGatewayClient apigw) {
this.environment = environment;
this.workingDir = workingDir;
- this.clientBuilderFactory = clientBuilderFactory;
- this.doesCfnMacroResourceExist = doesCfnMacroResourceExist;
+ this.s3 = s3;
+ this.cfn = cfn;
+ this.apigw = apigw;
}
private boolean confirm() {
@@ -115,7 +113,7 @@ public void run() {
outputMessage("Updating admin web application...");
SaaSBoostInstall.copyAdminWebAppSourceToS3(workingDir,
environment.getArtifactsBucket().getBucketName(),
- clientBuilderFactory.s3Builder().build());
+ s3);
break;
}
case CUSTOM_RESOURCES:
@@ -127,56 +125,35 @@ public void run() {
for (String target : action.getTargets()) {
// TODO update this logic for windows
File updatedDirectory = new File(action.getDirectoryName(), target);
- outputMessage("Updating " + updatedDirectory + " using "
- + new File(updatedDirectory, "update.sh"));
- // if this fails because update.sh does not exist, does not have the proper
- // permissions or any other reason, a runtimeException will be thrown, exiting
- // the run() execution
- SaaSBoostInstall.executeCommand(
- "./update.sh " + environment.getName(), // command to execute
- null, // environment to use
- updatedDirectory.getAbsoluteFile()); // directory to execute from
+ if (updatedDirectory.exists()) {
+ outputMessage("Updating " + updatedDirectory + " using "
+ + new File(updatedDirectory, "update.sh"));
+ // if this fails because update.sh does not exist, does not have the proper
+ // permissions or any other reason, a runtimeException will be thrown, exiting
+ // the run() execution
+ SaaSBoostInstall.executeCommand(
+ "./update.sh " + environment.getName(), // command to execute
+ null, // environment to use
+ updatedDirectory.getAbsoluteFile()); // directory to execute from
+ } else {
+ outputMessage("Warning! Directory to update "
+ + action.getDirectoryName() + " does not exist!");
+ }
}
break;
}
case RESOURCES: {
// upload the template to the Boost Artifacts bucket
for (String target : action.getTargets()) {
- outputMessage("Updating CloudFormation template: " + target);
- environment.getArtifactsBucket().putFile(
- clientBuilderFactory.s3Builder().build(), // s3 client
- Path.of(action.getDirectoryName(), target), // local path
- Path.of(target)); // remote path
- if (target.equals("saas-boost-metrics-analytics.yaml")
- && environment.isMetricsAnalyticsDeployed()) {
- // the metrics-analytics stack is not a child stack of the base stack,
- // so just updating the base stack won't update. update it manually.
- String analyticsStackName = environment.getBaseCloudFormationStackName() + "-analytics";
- // Load up the existing parameters from CloudFormation
- Map stackParamsMap = new LinkedHashMap<>();
- try {
- DescribeStacksResponse response = clientBuilderFactory.cloudFormationBuilder().build()
- .describeStacks(request -> request.stackName(analyticsStackName));
- if (response.hasStacks() && !response.stacks().isEmpty()) {
- Stack stack = response.stacks().get(0);
- stackParamsMap = stack.parameters().stream()
- .collect(Collectors.toMap(
- Parameter::parameterKey, Parameter::parameterValue));
- }
- } catch (SdkServiceException cfnError) {
- if (cfnError.getMessage().contains("does not exist")) {
- outputMessage("Analytics module CloudFormation stack "
- + analyticsStackName + " not found.");
- System.exit(2);
- }
- LOGGER.error("cloudformation:DescribeStacks error", cfnError);
- LOGGER.error(Utils.getFullStackTrace(cfnError));
- throw cfnError;
- }
- Map paramsMap = getCloudFormationParameterMap(
- workingDir.resolve(Path.of("resources", "saas-boost-metrics-analytics.yaml")),
- stackParamsMap);
- updateCloudFormationStack(analyticsStackName, paramsMap, target);
+ if (Path.of(action.getDirectoryName(), target).toFile().exists()) {
+ outputMessage("Updating CloudFormation template: " + target);
+ environment.getArtifactsBucket().putFile(
+ s3, // s3 client
+ Path.of(action.getDirectoryName(), target), // local path
+ Path.of(target)); // remote path
+ } else {
+ outputMessage("Warning! Resource to update "
+ + Path.of(action.getDirectoryName(), target).toFile() + " does not exist!");
}
}
break;
@@ -193,12 +170,6 @@ public void run() {
outputMessage("Updating Version parameter to " + Constants.VERSION);
cloudFormationParamMap.put("Version", Constants.VERSION);
- // If CloudFormation macro resources do not exist, that means that another environment that had previously
- // owned those resources was deleted. In this case we should make sure to create them.
- if (!doesCfnMacroResourceExist) {
- cloudFormationParamMap.put("CreateMacroResources", Boolean.TRUE.toString());
- }
-
// Always call update stack
outputMessage("Executing CloudFormation update stack on: " + environment.getBaseCloudFormationStackName());
updateCloudFormationStack(
@@ -481,7 +452,6 @@ private void updateCloudFormationStack(String stackName, Map par
.map(entry -> Parameter.builder().parameterKey(entry.getKey()).parameterValue(entry.getValue()).build())
.collect(Collectors.toList());
- CloudFormationClient cfn = clientBuilderFactory.cloudFormationBuilder().build();
LOGGER.info("Executing CloudFormation update stack for " + stackName);
try {
UpdateStackResponse updateStackResponse = cfn.updateStack(UpdateStackRequest.builder()
@@ -510,6 +480,7 @@ private void updateCloudFormationStack(String stackName, Map par
StackStatus.UPDATE_ROLLBACK_FAILED);
if (stackStatus == StackStatus.UPDATE_COMPLETE) {
outputMessage("CloudFormation stack: " + stackName + " updated successfully.");
+ outputMessage("You may need to update the CloudFormation Integration Stack in the Application Plane AWS Account.");
break;
} else if (failureStatuses.contains(stackStatus)) {
outputMessage("CloudFormation stack: " + stackName + " update failed.");
@@ -543,23 +514,18 @@ protected void runApiGatewayDeployment(Map cloudFormationParamMa
// CloudFormation will not redeploy an API Gateway stage on update
outputMessage("Updating API Gateway deployment for stages");
try {
- String publicApiName = "sb-" + environment.getName() + "-public-api";
- String privateApiName = "sb-" + environment.getName() + "-private-api";
- ApiGatewayClient apigw = clientBuilderFactory.apiGatewayBuilder().build();
+ String apiName = "sb-" + environment.getName() + "-api";
GetRestApisResponse response = apigw.getRestApis();
if (response.hasItems()) {
for (RestApi api : response.items()) {
- String apiName = api.name();
- boolean isPublicApi = publicApiName.equals(apiName);
- boolean isPrivateApi = privateApiName.equals(apiName);
- if (isPublicApi || isPrivateApi) {
- String stage = isPublicApi ? cloudFormationParamMap.get("PublicApiStage")
- : cloudFormationParamMap.get("PrivateApiStage");
+ if (apiName.equals(api.name())) {
+ String stage = cloudFormationParamMap.get("ApiStage");
outputMessage("Updating API Gateway deployment for " + apiName + " to stage: " + stage);
apigw.createDeployment(request -> request
.restApiId(api.id())
.stageName(stage)
);
+ break;
}
}
}
diff --git a/installer/src/main/resources/log4j2.xml b/installer/src/main/resources/log4j2.xml
index 4de39b98..5b08184a 100644
--- a/installer/src/main/resources/log4j2.xml
+++ b/installer/src/main/resources/log4j2.xml
@@ -17,16 +17,15 @@ limitations under the License.
-
+
-
-
-
+
+
-
+
diff --git a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostArtifactsBucketTest.java b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostArtifactsBucketTest.java
index e01a9eb6..f210a090 100644
--- a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostArtifactsBucketTest.java
+++ b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostArtifactsBucketTest.java
@@ -16,31 +16,33 @@
package com.amazon.aws.partners.saasfactory.saasboost;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
-import org.mockito.junit.MockitoJUnitRunner;
+import org.mockito.junit.jupiter.MockitoExtension;
import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.policybuilder.iam.IamPolicy;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.*;
import java.nio.file.Path;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
-@RunWith(MockitoJUnitRunner.class)
+@ExtendWith(MockitoExtension.class)
public class SaaSBoostArtifactsBucketTest {
private static final String ENV_NAME = "env-name";
+ private static final String APP_PLANE_ACCOUNT = "012345678901";
@Mock
S3Client mockS3;
- @Before
+ @BeforeEach
public void reset() {
Mockito.reset(mockS3);
}
@@ -52,17 +54,18 @@ public void putFileTest() throws Exception {
ArgumentCaptor requestBodyArgumentCaptor = ArgumentCaptor.forClass(RequestBody.class);
SaaSBoostArtifactsBucket testBucket =
- SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_EAST_1);
+ SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_EAST_1, APP_PLANE_ACCOUNT);
Path localPathToTestPut = Path.of(this.getClass().getClassLoader().getResource("template.yaml").toURI());
Path exampleRemotePath = Path.of("dir", "dir2");
testBucket.putFile(mockS3, localPathToTestPut, exampleRemotePath);
Mockito.verify(mockS3).putObject(putObjectRequestArgumentCaptor.capture(), requestBodyArgumentCaptor.capture());
- assertEquals("Put object to the wrong bucket.",
- testBucket.getBucketName(), putObjectRequestArgumentCaptor.getValue().bucket());
- assertEquals("Put object to the wrong location.",
- exampleRemotePath.toString().replace('\\', '/'), putObjectRequestArgumentCaptor.getValue().key());
- assertEquals("Put different length object to remote location. Wrong file?",
- localPathToTestPut.toFile().length(), requestBodyArgumentCaptor.getValue().contentLength());
+ assertEquals(testBucket.getBucketName(), putObjectRequestArgumentCaptor.getValue().bucket(),
+ "Put object to the wrong bucket.");
+ assertEquals(exampleRemotePath.toString().replace('\\', '/'),
+ putObjectRequestArgumentCaptor.getValue().key(),
+ "Put object to the wrong location.");
+ assertEquals(localPathToTestPut.toFile().length(), requestBodyArgumentCaptor.getValue().contentLength(),
+ "Put different length object to remote location. Wrong file?");
}
@Test
@@ -70,23 +73,23 @@ public void createBucketLocationConstraintTest() {
ArgumentCaptor createBucketRequestArgumentCaptor =
ArgumentCaptor.forClass(CreateBucketRequest.class);
- SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_EAST_1);
+ SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_EAST_1, APP_PLANE_ACCOUNT);
Mockito.verify(mockS3).createBucket(createBucketRequestArgumentCaptor.capture());
// expected, actual
CreateBucketRequest capturedCreateBucketRequest = createBucketRequestArgumentCaptor.getValue();
if (capturedCreateBucketRequest.createBucketConfiguration() != null) {
// if no createBucketConfiguration is passed in the createBucketRequest,
// there is implicitly no location constraint (because constraint must be part of the config)
- assertNull("No location constraint should be provided for buckets in us-east-1",
- createBucketRequestArgumentCaptor.getValue().createBucketConfiguration().locationConstraint());
+ assertNull(createBucketRequestArgumentCaptor.getValue().createBucketConfiguration().locationConstraint(),
+ "No location constraint should be provided for buckets in us-east-1");
}
Mockito.reset(mockS3);
- SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_WEST_2);
+ SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_WEST_2, APP_PLANE_ACCOUNT);
Mockito.verify(mockS3).createBucket(createBucketRequestArgumentCaptor.capture());
- assertEquals("Location constraint should be provided for buckets in us-west-2",
- BucketLocationConstraint.US_WEST_2,
- createBucketRequestArgumentCaptor.getValue().createBucketConfiguration().locationConstraint());
+ assertEquals(BucketLocationConstraint.US_WEST_2,
+ createBucketRequestArgumentCaptor.getValue().createBucketConfiguration().locationConstraint(),
+ "Location constraint should be provided for buckets in us-west-2");
}
@Test
@@ -94,12 +97,12 @@ public void createBucketServerSideEncryptionTest() {
ArgumentCaptor putBucketEncryptionRequestArgumentCaptor =
ArgumentCaptor.forClass(PutBucketEncryptionRequest.class);
SaaSBoostArtifactsBucket createdBucket =
- SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_EAST_1);
+ SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_EAST_1, APP_PLANE_ACCOUNT);
Mockito.verify(mockS3).putBucketEncryption(putBucketEncryptionRequestArgumentCaptor.capture());
PutBucketEncryptionRequest capturedPutBucketEncryptionRequest =
putBucketEncryptionRequestArgumentCaptor.getValue();
- assertEquals("Put encryption to the wrong bucket.",
- createdBucket.getBucketName(), capturedPutBucketEncryptionRequest.bucket());
+ assertEquals(createdBucket.getBucketName(), capturedPutBucketEncryptionRequest.bucket(),
+ "Put encryption to the wrong bucket.");
assertNotNull(capturedPutBucketEncryptionRequest.serverSideEncryptionConfiguration());
assertNotNull(capturedPutBucketEncryptionRequest.serverSideEncryptionConfiguration().rules());
assertTrue(capturedPutBucketEncryptionRequest.serverSideEncryptionConfiguration().rules().contains(
@@ -113,32 +116,44 @@ public void createBucketBucketPolicyTest() {
ArgumentCaptor putBucketPolicyArgumentCaptor =
ArgumentCaptor.forClass(PutBucketPolicyRequest.class);
SaaSBoostArtifactsBucket createdBucket =
- SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_EAST_1);
+ SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.US_EAST_1, APP_PLANE_ACCOUNT);
Mockito.verify(mockS3).putBucketPolicy(putBucketPolicyArgumentCaptor.capture());
PutBucketPolicyRequest capturedPutBucketPolicyRequest = putBucketPolicyArgumentCaptor.getValue();
- assertEquals("Put bucket policy to the wrong bucket.",
- createdBucket.getBucketName(), capturedPutBucketPolicyRequest.bucket());
+ assertEquals(createdBucket.getBucketName(), capturedPutBucketPolicyRequest.bucket(),
+ "Put bucket policy to the wrong bucket.");
assertNotNull(capturedPutBucketPolicyRequest.policy());
- assertEquals("{\n" +
- " \"Version\": \"2012-10-17\",\n" +
- " \"Statement\": [\n" +
- " {\n" +
- " \"Sid\": \"DenyNonHttps\",\n" +
- " \"Effect\": \"Deny\",\n" +
- " \"Principal\": \"*\",\n" +
- " \"Action\": \"s3:*\",\n" +
- " \"Resource\": [\n" +
- " \"arn:aws:s3:::" + createdBucket.getBucketName() + "/*\",\n" +
- " \"arn:aws:s3:::" + createdBucket.getBucketName() + "\"\n" +
- " ],\n" +
- " \"Condition\": {\n" +
- " \"Bool\": {\n" +
- " \"aws:SecureTransport\": \"false\"\n" +
- " }\n" +
- " }\n" +
- " }\n" +
- " ]\n" +
- "}", capturedPutBucketPolicyRequest.policy());
+ String partition = createdBucket.getRegion().metadata().partition().id();
+ IamPolicy createdPolicy = IamPolicy.fromJson(capturedPutBucketPolicyRequest.policy());
+ IamPolicy expectedPolicy = IamPolicy.fromJson("{\n"
+ + " \"Version\": \"2012-10-17\",\n"
+ + " \"Statement\": [\n"
+ + " {\n"
+ + " \"Sid\": \"DenyNonHttps\",\n"
+ + " \"Effect\": \"Deny\",\n"
+ + " \"Principal\": \"*\",\n"
+ + " \"Action\": \"s3:*\",\n"
+ + " \"Resource\": [\n"
+ + " \"arn:" + partition + ":s3:::" + createdBucket.getBucketName() + "/*\",\n"
+ + " \"arn:" + partition + ":s3:::" + createdBucket.getBucketName() + "\"\n"
+ + " ],\n"
+ + " \"Condition\": {\n"
+ + " \"Bool\": {\n"
+ + " \"aws:SecureTransport\": \"false\"\n"
+ + " }\n"
+ + " }\n"
+ + " },\n"
+ + " {\n"
+ + " \"Sid\": \"AppPlaneAccountQuickLink\",\n"
+ + " \"Effect\": \"Allow\",\n"
+ + " \"Principal\": {\n"
+ + " \"AWS\": \"arn:aws:iam::" + createdBucket.getAppPlaneAccountId() + ":root\"\n"
+ + " },\n"
+ + " \"Action\": \"s3:GetObject\",\n"
+ + " \"Resource\": \"arn:" + partition + ":s3:::" + createdBucket.getBucketName() + "/saas-boost-app-integration.yaml\"\n"
+ + " }\n"
+ + " ]\n"
+ + "}");
+ assertEquals(expectedPolicy, createdPolicy);
}
@Test
@@ -146,31 +161,43 @@ public void createBucketBucketPolicyTest_china() {
ArgumentCaptor putBucketPolicyArgumentCaptor =
ArgumentCaptor.forClass(PutBucketPolicyRequest.class);
SaaSBoostArtifactsBucket createdBucket =
- SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.CN_NORTHWEST_1);
+ SaaSBoostArtifactsBucket.createS3ArtifactBucket(mockS3, ENV_NAME, Region.CN_NORTHWEST_1, APP_PLANE_ACCOUNT);
Mockito.verify(mockS3).putBucketPolicy(putBucketPolicyArgumentCaptor.capture());
PutBucketPolicyRequest capturedPutBucketPolicyRequest = putBucketPolicyArgumentCaptor.getValue();
- assertEquals("Put bucket policy to the wrong bucket.",
- createdBucket.getBucketName(), capturedPutBucketPolicyRequest.bucket());
+ assertEquals(createdBucket.getBucketName(), capturedPutBucketPolicyRequest.bucket(),
+ "Put bucket policy to the wrong bucket.");
assertNotNull(capturedPutBucketPolicyRequest.policy());
- assertEquals("{\n" +
- " \"Version\": \"2012-10-17\",\n" +
- " \"Statement\": [\n" +
- " {\n" +
- " \"Sid\": \"DenyNonHttps\",\n" +
- " \"Effect\": \"Deny\",\n" +
- " \"Principal\": \"*\",\n" +
- " \"Action\": \"s3:*\",\n" +
- " \"Resource\": [\n" +
- " \"arn:aws-cn:s3:::" + createdBucket.getBucketName() + "/*\",\n" +
- " \"arn:aws-cn:s3:::" + createdBucket.getBucketName() + "\"\n" +
- " ],\n" +
- " \"Condition\": {\n" +
- " \"Bool\": {\n" +
- " \"aws:SecureTransport\": \"false\"\n" +
- " }\n" +
- " }\n" +
- " }\n" +
- " ]\n" +
- "}", capturedPutBucketPolicyRequest.policy());
+ String partition = createdBucket.getRegion().metadata().partition().id();
+ IamPolicy createdPolicy = IamPolicy.fromJson(capturedPutBucketPolicyRequest.policy());
+ IamPolicy expectedPolicy = IamPolicy.fromJson("{\n"
+ + " \"Version\": \"2012-10-17\",\n"
+ + " \"Statement\": [\n"
+ + " {\n"
+ + " \"Sid\": \"DenyNonHttps\",\n"
+ + " \"Effect\": \"Deny\",\n"
+ + " \"Principal\": \"*\",\n"
+ + " \"Action\": \"s3:*\",\n"
+ + " \"Resource\": [\n"
+ + " \"arn:" + partition + ":s3:::" + createdBucket.getBucketName() + "/*\",\n"
+ + " \"arn:" + partition + ":s3:::" + createdBucket.getBucketName() + "\"\n"
+ + " ],\n"
+ + " \"Condition\": {\n"
+ + " \"Bool\": {\n"
+ + " \"aws:SecureTransport\": \"false\"\n"
+ + " }\n"
+ + " }\n"
+ + " },\n"
+ + " {\n"
+ + " \"Sid\": \"AppPlaneAccountQuickLink\",\n"
+ + " \"Effect\": \"Allow\",\n"
+ + " \"Principal\": {\n"
+ + " \"AWS\": \"arn:aws:iam::" + createdBucket.getAppPlaneAccountId() + ":root\"\n"
+ + " },\n"
+ + " \"Action\": \"s3:GetObject\",\n"
+ + " \"Resource\": \"arn:" + partition + ":s3:::" + createdBucket.getBucketName() + "/saas-boost-app-integration.yaml\"\n"
+ + " }\n"
+ + " ]\n"
+ + "}");
+ assertEquals(expectedPolicy, createdPolicy);
}
}
diff --git a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstallTest.java b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstallTest.java
index 0a922bf8..8db37b46 100644
--- a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstallTest.java
+++ b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostInstallTest.java
@@ -15,16 +15,14 @@
*/
package com.amazon.aws.partners.saasfactory.saasboost;
-import org.junit.After;
-import org.junit.BeforeClass;
-import org.junit.Test;
+import org.junit.jupiter.api.AfterEach;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.*;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
// Note that these tests will only work if you run them from Maven or if you add
// the AWS_REGION environment variable to your IDE's configuration settings
@@ -76,7 +74,7 @@ public static void initTemplate() {
}
*/
- @After
+ @AfterEach
public void resetStdIn() {
System.setIn(System.in);
}
diff --git a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactoryTest.java b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactoryTest.java
deleted file mode 100644
index 096fde30..00000000
--- a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/AwsClientBuilderFactoryTest.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/**
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.amazon.aws.partners.saasfactory.saasboost.clients;
-
-import org.junit.After;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
-import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
-import software.amazon.awssdk.awscore.client.builder.AwsSyncClientBuilder;
-import software.amazon.awssdk.regions.Region;
-import software.amazon.awssdk.services.quicksight.QuickSightClientBuilder;
-
-import java.lang.reflect.Method;
-import java.util.List;
-
-import static org.junit.Assert.*;
-import static org.mockito.Mockito.*;
-
-public class AwsClientBuilderFactoryTest {
-
- private static final Region DEFAULT_EXPECTED_REGION = null;
- private static final Class extends AwsCredentialsProvider> DEFAULT_EXPECTED_CREDENTIALS_PROVIDER_CLASS =
- RefreshingProfileDefaultCredentialsProvider.class;
-
- private static QuickSightClientBuilder mockBuilder;
-
- @BeforeClass
- public static void createMockBuilder() {
- mockBuilder = mock(QuickSightClientBuilder.class);
- when(mockBuilder.credentialsProvider(any())).thenReturn(mockBuilder);
- when(mockBuilder.region(any())).thenReturn(mockBuilder);
- }
-
- @After
- public void resetMockBuilder() {
- clearInvocations(mockBuilder);
- }
-
- @Test
- public void buildFactoryWithNoRegion() {
- // this test verifies that a null region is automatically filled with the default profile region
- // in the SDK. this is assumed by the BoostAwsClientBuilderFactory and will fail should that behavior change
- AwsClientBuilderFactory.builder().build().quickSightBuilder().build();
- }
-
- @Test
- public void verifyBuildersHaveDefaults() {
- // for each builder, verify it has region and credentials provider as expected
- runBoostAwsClientBuilderFactoryTest(AwsClientBuilderFactory.builder().build(),
- DEFAULT_EXPECTED_REGION, DEFAULT_EXPECTED_CREDENTIALS_PROVIDER_CLASS);
- }
-
- @Test
- public void verifyBuilderRegionOverridden() {
- Region expectedRegion = Region.AF_SOUTH_1;
- runBoostAwsClientBuilderFactoryTest(AwsClientBuilderFactory.builder().region(expectedRegion).build(),
- expectedRegion, DEFAULT_EXPECTED_CREDENTIALS_PROVIDER_CLASS);
- }
-
- @Test
- public void verifyBuilderCredentialProviderOverridden() {
- Class extends AwsCredentialsProvider> expectedCredentialsProviderClass = DefaultCredentialsProvider.class;
- runBoostAwsClientBuilderFactoryTest(
- AwsClientBuilderFactory.builder()
- .credentialsProvider(DefaultCredentialsProvider.create())
- .build(),
- DEFAULT_EXPECTED_REGION, expectedCredentialsProviderClass);
- }
-
- @Test
- public void verifyFactoryCachingForAllBuilders() {
- AwsClientBuilderFactory factory = AwsClientBuilderFactory.builder().build();
- // for each method that returns a builder..
- for (Method m : factory.getClass().getMethods()) {
- // checking if the return type implements AwsSyncClientBuilder
- if (List.of(m.getReturnType().getInterfaces()).contains(AwsSyncClientBuilder.class)) {
- try {
- AwsSyncClientBuilder b = (AwsSyncClientBuilder) m.invoke(factory);
- // invoking the builder function again should not create a new builder
- assertEquals(b, m.invoke(factory));
- } catch (Exception e) {
- throw new RuntimeException("test failed", e);
- }
- }
- }
- }
-
- private void runBoostAwsClientBuilderFactoryTest(
- AwsClientBuilderFactory factory,
- Region expectedRegion,
- Class extends AwsCredentialsProvider> expectedCredentialsProviderClass) {
- ArgumentCaptor credentialsProviderArgumentCaptor =
- ArgumentCaptor.forClass(AwsCredentialsProvider.class);
- factory.decorateBuilderWithDefaults(mockBuilder);
-
- verify(mockBuilder).region(expectedRegion);
- verify(mockBuilder).credentialsProvider(credentialsProviderArgumentCaptor.capture());
- assertEquals("Factory instantiated the wrong credentials provider",
- expectedCredentialsProviderClass,
- credentialsProviderArgumentCaptor.getValue().getClass());
- }
-}
diff --git a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/MockAwsClientBuilderFactory.java b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/MockAwsClientBuilderFactory.java
deleted file mode 100644
index a585618d..00000000
--- a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/MockAwsClientBuilderFactory.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/**
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
- package com.amazon.aws.partners.saasfactory.saasboost.clients;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import java.net.URI;
-
-import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
-import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
-import software.amazon.awssdk.http.SdkHttpClient;
-import software.amazon.awssdk.regions.Region;
-import software.amazon.awssdk.services.cloudformation.CloudFormationClient;
-import software.amazon.awssdk.services.cloudformation.CloudFormationClientBuilder;
-
-public class MockAwsClientBuilderFactory extends AwsClientBuilderFactory {
- private final AwsClientBuilderFactory factory = mock(AwsClientBuilderFactory.class);
-
- public MockAwsClientBuilderFactory() {
-
- }
-
- public void mockCfn(CloudFormationClient cfn) {
- when(factory.cloudFormationBuilder()).thenReturn(new CloudFormationClientBuilder() {
-
- @Override
- public CloudFormationClientBuilder httpClient(SdkHttpClient httpClient) {
- // TODO Auto-generated method stub
- return null;
- }
-
- @Override
- public CloudFormationClientBuilder httpClientBuilder(
- software.amazon.awssdk.http.SdkHttpClient.Builder httpClientBuilder) {
- // TODO Auto-generated method stub
- return null;
- }
-
- @Override
- public CloudFormationClientBuilder credentialsProvider(AwsCredentialsProvider credentialsProvider) {
- // TODO Auto-generated method stub
- return null;
- }
-
- @Override
- public CloudFormationClientBuilder region(Region region) {
- // TODO Auto-generated method stub
- return null;
- }
-
- @Override
- public CloudFormationClientBuilder dualstackEnabled(Boolean dualstackEndpointEnabled) {
- // TODO Auto-generated method stub
- return null;
- }
-
- @Override
- public CloudFormationClientBuilder fipsEnabled(Boolean fipsEndpointEnabled) {
- // TODO Auto-generated method stub
- return null;
- }
-
- @Override
- public CloudFormationClientBuilder overrideConfiguration(
- ClientOverrideConfiguration overrideConfiguration) {
- // TODO Auto-generated method stub
- return null;
- }
-
- @Override
- public CloudFormationClientBuilder endpointOverride(URI endpointOverride) {
- // TODO Auto-generated method stub
- return null;
- }
-
- @Override
- public CloudFormationClient build() {
- // TODO Auto-generated method stub
- return cfn;
- }
-
- });
- }
-}
diff --git a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/ProfileUtils.java b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/ProfileUtils.java
deleted file mode 100644
index 2daa46c5..00000000
--- a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/ProfileUtils.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package com.amazon.aws.partners.saasfactory.saasboost.clients;
-
-import software.amazon.awssdk.auth.credentials.AwsCredentials;
-import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
-import software.amazon.awssdk.core.SdkSystemSetting;
-import software.amazon.awssdk.profiles.Profile;
-
-import java.io.*;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-
-public class ProfileUtils {
- public static void updateOrCreateProfile(String filename,
- String profile,
- AwsCredentials newCredentials)
- throws IOException {
- // this will automatically create the file if it does not already exist
- try (FileWriter updatingOutputStream = new FileWriter(filename, false)) {
- writeProfileToFileWriter(
- Profile.builder()
- .name(profile)
- .properties(propertiesFromCredentials(newCredentials))
- .build(),
- updatingOutputStream);
- }
- }
-
- private static void writeProfileToFileWriter(Profile profile, FileWriter fileWriter) throws IOException {
- fileWriter.write("[" + profile.name() + "]\n");
- for (Map.Entry property : profile.properties().entrySet()) {
- fileWriter.write(property.getKey() + " = " + property.getValue() + "\n");
- }
- fileWriter.write("\n");
- }
-
- private static Map propertiesFromCredentials(AwsCredentials awsCredentials) {
- Map properties = new HashMap<>();
- properties.put(
- SdkSystemSetting.AWS_ACCESS_KEY_ID.environmentVariable().toLowerCase(),
- awsCredentials.accessKeyId());
- properties.put(
- SdkSystemSetting.AWS_SECRET_ACCESS_KEY.environmentVariable().toLowerCase(),
- awsCredentials.secretAccessKey());
- if (awsCredentials instanceof AwsSessionCredentials) {
- properties.put(
- SdkSystemSetting.AWS_SESSION_TOKEN.environmentVariable().toLowerCase(),
- ((AwsSessionCredentials) awsCredentials).sessionToken());
- }
- return properties;
- }
-}
diff --git a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/RefreshingProfileDefaultCredentialsProviderTest.java b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/RefreshingProfileDefaultCredentialsProviderTest.java
deleted file mode 100644
index 68f7637e..00000000
--- a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/clients/RefreshingProfileDefaultCredentialsProviderTest.java
+++ /dev/null
@@ -1,239 +0,0 @@
-/**
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.amazon.aws.partners.saasfactory.saasboost.clients;
-
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import software.amazon.awssdk.auth.credentials.*;
-import software.amazon.awssdk.core.SdkSystemSetting;
-import software.amazon.awssdk.profiles.ProfileFileSystemSetting;
-
-import java.io.Closeable;
-import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-
-import static org.junit.Assert.*;
-import static org.junit.Assume.assumeFalse;
-
-public class RefreshingProfileDefaultCredentialsProviderTest {
- private static final Logger LOGGER = LoggerFactory.getLogger(RefreshingProfileDefaultCredentialsProviderTest.class);
-
- private static final String AWS_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID";
- private static final String AWS_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY";
-
- private static final String BEFORE_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE";
- private static final String AFTER_ACCESS_KEY = "AKIAI44QH8DHBEXAMPLE";
- private static final String BEFORE_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
- private static final String AFTER_SECRET_KEY = "je7MtGbClwBF/2Zp9Utk/h3yCo8nvbEXAMPLEKEY";
- private static final String BEFORE_SESSION_TOKEN = "fakesessiontoken";
- private static final String AFTER_SESSION_TOKEN = "adifferentfakesessiontoken";
- private static final AwsCredentials BEFORE_PERMANENT =
- AwsBasicCredentials.create(BEFORE_ACCESS_KEY, BEFORE_SECRET_KEY);
- private static final AwsCredentials AFTER_PERMANENT =
- AwsBasicCredentials.create(AFTER_ACCESS_KEY, AFTER_SESSION_TOKEN);
- private static final AwsCredentials BEFORE_TEMPORARY =
- AwsSessionCredentials.create(BEFORE_ACCESS_KEY, BEFORE_SECRET_KEY, BEFORE_SESSION_TOKEN);
- private static final AwsCredentials AFTER_TEMPORARY =
- AwsSessionCredentials.create(AFTER_ACCESS_KEY, AFTER_SECRET_KEY, AFTER_SESSION_TOKEN);
- private static final AwsCredentials[] TEST_CREDENTIALS = new AwsCredentials[] {
- BEFORE_PERMANENT, AFTER_PERMANENT, BEFORE_TEMPORARY, AFTER_TEMPORARY
- };
-
- private static final String RESOURCES_LOCATION = "src/test/resources/";
-
- // this is created to hide all AWS credentials from the test environment that are not profile credentials
- // so that the RefreshingProfileDefaultCredentialsProvider can be tested directly.
- private static NoSystemPropertyCredentialsTestEnvironment cachedSystemProperties;
-
- private String fakeProfileFilename;
-
- public RefreshingProfileDefaultCredentialsProviderTest() {
-
- }
-
- /**
- * Returns whether to skip these tests based on AWS Credentials Environment Variable configuration.
- *
- * As stated in the documentation for the NoSystemPropertyCredentialsTestEnvironment, if environment
- * variables are configured for this test they will not be hidden and this test cannot verify the
- * functionality of the RefreshingProfileDefaultCredentialsProvider.
- *
- * In most cases it is acceptable to skip these tests, since if users are running these tests with
- * the EnvironmentVariableCredentialsProvider that means they will likely be running the Installer
- * itself with the same configuration, so the the RefreshingProfileDefaultCredentialsProvider will
- * not be used at all.
- *
- * The only caveat is that any change to the RefreshingProfileDefaultCredentialsProviderTest must
- * guarantee that this test is run before being accepted for contribution.
- *
- * @return true if AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY is configured in environment
- */
- private static boolean shouldSkipTests() {
- Set environmentKeys = System.getenv().keySet();
- return environmentKeys.contains(AWS_ACCESS_KEY_ID) || environmentKeys.contains(AWS_SECRET_ACCESS_KEY);
- }
-
- @BeforeClass
- public static void cacheAndHideSystemProperties() {
- cachedSystemProperties = new NoSystemPropertyCredentialsTestEnvironment();
- }
-
- @AfterClass
- public static void resetSystemProperties() {
- cachedSystemProperties.close();
- }
-
- @After
- public void cleanUp() {
- if (!new File(fakeProfileFilename).delete()) {
- LOGGER.error("Failed to delete {} due to an underlying FileSystem error", fakeProfileFilename);
- }
- }
-
- /**
- * Tests whether the RefreshingDefaultCredentialsProviderTest is still necessary.
- *
- * For more information, see the class javadoc for {@link RefreshingProfileDefaultCredentialsProvider}.
- *
- * @throws IOException in case of any errors in File management
- */
- @Test
- public void defaultCredentialsProviderBugStillExists() throws IOException {
- // if we change the profile from under the DefaultCredentials provider, does it return the old credentials?
- fakeProfileFilename = getAbsoluteFakeProfileFilename("fake-credentials-default");
- ProfileUtils.updateOrCreateProfile(
- fakeProfileFilename,
- ProfileFileSystemSetting.AWS_PROFILE.defaultValue(),
- TEST_CREDENTIALS[0]);
- System.setProperty(
- ProfileFileSystemSetting.AWS_SHARED_CREDENTIALS_FILE.property(),
- fakeProfileFilename);
- System.setProperty(
- ProfileFileSystemSetting.AWS_PROFILE.property(),
- ProfileFileSystemSetting.AWS_PROFILE.defaultValue());
- DefaultCredentialsProvider defaultCredentialsProvider = DefaultCredentialsProvider.create();
- runUpdatingCredentialsProviderTest(defaultCredentialsProvider, false);
- }
-
- @Test
- public void refreshingCredentialsProviderFindsNewCredentials() throws IOException {
- fakeProfileFilename = getAbsoluteFakeProfileFilename("fake-credentials-refreshing");
- ProfileUtils.updateOrCreateProfile(
- fakeProfileFilename,
- ProfileFileSystemSetting.AWS_PROFILE.defaultValue(),
- TEST_CREDENTIALS[0]);
- RefreshingProfileDefaultCredentialsProvider refreshingCredentialsProvider = RefreshingProfileDefaultCredentialsProvider.builder()
- .profileFilename(fakeProfileFilename)
- .profileName(ProfileFileSystemSetting.AWS_PROFILE.defaultValue())
- .build();
- runUpdatingCredentialsProviderTest(refreshingCredentialsProvider, true);
- }
-
- private void runUpdatingCredentialsProviderTest(
- AwsCredentialsProvider credentialsProviderToTest,
- boolean expectUpdate) throws IOException {
- assumeFalse("Skipping test due to configuration of AWS credentials as Environment Variables."
- + " To run this test unset the environment variables "
- + AWS_ACCESS_KEY_ID + " and " + AWS_SECRET_ACCESS_KEY,
- shouldSkipTests());
- AwsCredentials expectedCredentials = TEST_CREDENTIALS[0];
- for (int i = 1 ; i < TEST_CREDENTIALS.length ; i++) {
- assertEquals(expectedCredentials, credentialsProviderToTest.resolveCredentials());
- ProfileUtils.updateOrCreateProfile(
- fakeProfileFilename,
- ProfileFileSystemSetting.AWS_PROFILE.defaultValue(),
- TEST_CREDENTIALS[i]);
- if (expectUpdate) {
- expectedCredentials = TEST_CREDENTIALS[i];
- }
- }
- }
-
- private String getAbsoluteFakeProfileFilename(String simpleFilename) {
- return new File(RESOURCES_LOCATION + simpleFilename).getAbsolutePath();
- }
-
- /**
- * Hides the system property credentials when for the RefreshingDefaultCredentialsProviderTest to force
- * the profile credentials provider to be used by the DefaultCredentialsProvider.
- *
- * For reference, the {@link DefaultCredentialsProvider} reads credentials in the following manner:
- * {@link SystemPropertyCredentialsProvider}
- * {@link EnvironmentVariableCredentialsProvider}
- * {@link WebIdentityTokenFileCredentialsProvider}
- * {@link ProfileCredentialsProvider}
- * {@link ContainerCredentialsProvider}
- * {@link InstanceProfileCredentialsProvider}
- *
- * To force the underlying {@link DefaultCredentialsProvider} to use the {@link ProfileCredentialsProvider}, we need
- * to guarantee that the {@link SystemPropertyCredentialsProvider}, {@link EnvironmentVariableCredentialsProvider},
- * and {@link WebIdentityTokenFileCredentialsProvider} all will return no credentials. If any of these return
- * credentials then our test will fail, expecting configured Profile credentials but receiving some others.
- *
- * Luckily for the {@link SystemPropertyCredentialsProvider} and {@link WebIdentityTokenFileCredentialsProvider},
- * whether any credentials are loaded is controllable via System properties, so this
- * NoSystemPropertyCredentialsTestEnvironment class caches and unsets any System properties (using
- * {@link System#getProperty(String)}, {@link System#clearProperty(String)}, and
- * {@link System#setProperty(String, String)}).
- *
- * For the {@link EnvironmentVariableCredentialsProvider} however, because environment variables are not
- * controllable at runtime, we have no recourse. Adding environment variable credentials to this test will
- * currently cause this test to fail. If for some reason we need to add environment variable credentials to
- * tests in the installer, this test can be refactored (with the addition of PowerMock to our testing dependencies)
- * to statically mock responses to {@link System#getenv(String)} to return nothing for the credential environment
- * variable names.
- */
- private static class NoSystemPropertyCredentialsTestEnvironment implements Closeable {
- private static final String[] PROPERTIES_TO_CHECK = new String[]{
- SdkSystemSetting.AWS_ACCESS_KEY_ID.property(),
- SdkSystemSetting.AWS_SECRET_ACCESS_KEY.property(),
- SdkSystemSetting.AWS_SESSION_TOKEN.property(),
- SdkSystemSetting.AWS_ROLE_SESSION_NAME.property(),
- SdkSystemSetting.AWS_ROLE_ARN.property(),
- SdkSystemSetting.AWS_WEB_IDENTITY_TOKEN_FILE.property(),
- ProfileFileSystemSetting.AWS_SHARED_CREDENTIALS_FILE.property(),
- ProfileFileSystemSetting.AWS_PROFILE.property()
- };
-
- private final Map hiddenProperties;
-
- public NoSystemPropertyCredentialsTestEnvironment() {
- hiddenProperties = new HashMap<>();
- for (String propertyKey : PROPERTIES_TO_CHECK) {
- String propertyValue = System.getProperty(propertyKey);
- if (propertyValue != null) {
- hiddenProperties.put(propertyKey, propertyValue);
- System.clearProperty(propertyKey);
- }
- }
- }
-
- @Override
- public void close() {
- for (Map.Entry hiddenProperty : hiddenProperties.entrySet()) {
- System.setProperty(hiddenProperty.getKey(), hiddenProperty.getValue());
- }
- }
- }
-}
diff --git a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflowTest.java b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflowTest.java
index 230a87f1..6a38b92a 100644
--- a/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflowTest.java
+++ b/installer/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/workflow/UpdateWorkflowTest.java
@@ -16,9 +16,6 @@
package com.amazon.aws.partners.saasfactory.saasboost.workflow;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
@@ -33,13 +30,12 @@
import java.util.Map;
import java.util.Set;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import com.amazon.aws.partners.saasfactory.saasboost.clients.AwsClientBuilderFactory;
-import com.amazon.aws.partners.saasfactory.saasboost.clients.MockAwsClientBuilderFactory;
import com.amazon.aws.partners.saasfactory.saasboost.model.Environment;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
public class UpdateWorkflowTest {
@@ -49,12 +45,10 @@ public class UpdateWorkflowTest {
.build();
private UpdateWorkflow updateWorkflow;
- private AwsClientBuilderFactory clientBuilderFactory;
private Path workingDir;
- @Before
+ @BeforeEach
public void setup() {
- clientBuilderFactory = new MockAwsClientBuilderFactory();
workingDir = Paths.get("../");
try {
// location is installer/target/something.jar, so we need
@@ -64,10 +58,10 @@ public void setup() {
} catch (URISyntaxException urise) {
throw new RuntimeException("Failed to determine installation directory for test");
}
- updateWorkflow = new UpdateWorkflow(workingDir, testEnvironment, clientBuilderFactory, true);
+ updateWorkflow = new UpdateWorkflow(workingDir, testEnvironment, null, null, null);
}
- @After
+ @AfterEach
public void cleanup() {
for (UpdateAction action : UpdateAction.values()) {
action.resetTargets();
@@ -96,9 +90,9 @@ public void testGetCloudFormationParameterMap() throws Exception {
expected.put("DefaultStringParameter", "foobar");
expected.put("NumericParameter", "1");
- assertEquals("Template has 3 parameters", expected.size(), actual.size());
+ assertEquals(expected.size(), actual.size(), "Template has 3 parameters");
for (Map.Entry entry : expected.entrySet()) {
- assertEquals(entry.getKey() + " equals " + entry.getValue(), entry.getValue(), actual.get(entry.getKey()));
+ assertEquals(entry.getValue(), actual.get(entry.getKey()), entry.getKey() + " equals " + entry.getValue());
}
}
@@ -107,14 +101,14 @@ public void testUpdateActionsFromPaths_basic() {
Set expectedActions = EnumSet.of(UpdateAction.CLIENT, UpdateAction.FUNCTIONS);
List changedPaths = List.of(
Path.of("client/web/src/App.js"),
- Path.of("functions/onboarding-app-stack-listener/pom.xml"));
+ Path.of("functions/authorizer/pom.xml"));
Collection actualActions = updateWorkflow.getUpdateActionsFromPaths(changedPaths);
assertEquals(expectedActions, actualActions);
actualActions.forEach(action -> {
if (action == UpdateAction.FUNCTIONS) {
assertEquals(1, action.getTargets().size());
assertEquals(1, UpdateAction.FUNCTIONS.getTargets().size());
- assertTrue(action.getTargets().contains("onboarding-app-stack-listener"));
+ assertTrue(action.getTargets().contains("authorizer"));
}
});
}
@@ -124,8 +118,8 @@ public void testUpdateActionsFromPaths_layersFirst() {
Set expectedActions = EnumSet.of(UpdateAction.LAYERS, UpdateAction.CLIENT, UpdateAction.FUNCTIONS);
List changedPaths = List.of(
Path.of("client/web/src/App.js"),
- Path.of("functions/onboarding-app-stack-listener/pom.xml"),
- Path.of("layers/apigw-helper/pom.xml"));
+ Path.of("functions/authorizer/pom.xml"),
+ Path.of("layers/utils/pom.xml"));
Collection actualActions = updateWorkflow.getUpdateActionsFromPaths(changedPaths);
assertEquals(expectedActions, actualActions);
// the first item in the set iterator should always be LAYERS
@@ -146,7 +140,7 @@ public void testUpdateActionsFromPaths_customResourcesPath() {
Set expectedActions = EnumSet.of(UpdateAction.CUSTOM_RESOURCES, UpdateAction.RESOURCES);
List changedPaths = List.of(
Path.of("resources/saas-boost.yaml"),
- Path.of("resources/custom-resources/app-services-macro/pom.xml"));
+ Path.of("resources/custom-resources/clear-s3-bucket/pom.xml"));
Collection actualActions = updateWorkflow.getUpdateActionsFromPaths(changedPaths);
assertEquals(expectedActions, actualActions);
actualActions.forEach(action -> {
@@ -156,7 +150,7 @@ public void testUpdateActionsFromPaths_customResourcesPath() {
}
if (action == UpdateAction.CUSTOM_RESOURCES) {
assertEquals(1, action.getTargets().size());
- assertTrue(action.getTargets().contains("app-services-macro"));
+ assertTrue(action.getTargets().contains("clear-s3-bucket"));
}
});
}
diff --git a/layers/apigw-helper/pom.xml b/layers/apigw-helper/pom.xml
index 2e28df89..a8a07ec2 100644
--- a/layers/apigw-helper/pom.xml
+++ b/layers/apigw-helper/pom.xml
@@ -33,6 +33,7 @@ limitations under the License.
+ ${project.basedir}/../..7
@@ -68,10 +69,25 @@ limitations under the License.
provided
+
+ org.apache.httpcomponents.core5
+ httpcore5
+ 5.2.2
+ software.amazon.awssdk
- apache-client
+ secretsmanager${aws.java.sdk.version}
+
+
+ software.amazon.awssdk
+ netty-nio-client
+
+
+ software.amazon.awssdk
+ apache-client
+
+ software.amazon.awssdk
diff --git a/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayHelper.java b/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayHelper.java
index d30484cb..0de0a862 100644
--- a/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayHelper.java
+++ b/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayHelper.java
@@ -16,24 +16,20 @@
package com.amazon.aws.partners.saasfactory.saasboost;
-import org.apache.http.NameValuePair;
-import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.net.URIBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
-import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
import software.amazon.awssdk.auth.signer.Aws4Signer;
import software.amazon.awssdk.auth.signer.params.Aws4SignerParams;
-import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.core.exception.SdkServiceException;
-import software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting;
-import software.amazon.awssdk.core.retry.RetryPolicy;
-import software.amazon.awssdk.core.retry.backoff.BackoffStrategy;
-import software.amazon.awssdk.core.retry.conditions.RetryCondition;
import software.amazon.awssdk.http.*;
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
+import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse;
import software.amazon.awssdk.services.sts.StsClient;
import software.amazon.awssdk.services.sts.model.AssumeRoleResponse;
import software.amazon.awssdk.services.sts.model.Credentials;
@@ -41,12 +37,12 @@
import java.io.*;
import java.net.MalformedURLException;
-import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
-import java.util.List;
-import java.util.Map;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.*;
import java.util.stream.Collectors;
public class ApiGatewayHelper {
@@ -56,23 +52,14 @@ public class ApiGatewayHelper {
private static final String SAAS_BOOST_ENV = System.getenv("SAAS_BOOST_ENV");
private static final Aws4Signer SIG_V4 = Aws4Signer.create();
private static SdkHttpClient HTTP_CLIENT = UrlConnectionHttpClient.create();
- private static final StsClient sts = StsClient.builder()
- .httpClient(HTTP_CLIENT)
- .credentialsProvider(EnvironmentVariableCredentialsProvider.create())
- .region(Region.of(AWS_REGION))
- .endpointOverride(URI.create("https://" + StsClient.SERVICE_NAME + "." + AWS_REGION
- + "." + Utils.endpointSuffix(AWS_REGION)))
- .overrideConfiguration(ClientOverrideConfiguration.builder()
- .retryPolicy(RetryPolicy.builder()
- .backoffStrategy(BackoffStrategy.defaultStrategy())
- .throttlingBackoffStrategy(BackoffStrategy.defaultThrottlingStrategy())
- .numRetries(SdkDefaultRetrySetting.defaultMaxAttempts())
- .retryCondition(RetryCondition.defaultRetryCondition())
- .build()
- )
- .build()
- )
- .build();
+ private final Map> CLIENT_CREDENTIALS_CACHE = new HashMap<>();
+ private SecretsManagerClient secrets;
+ private StsClient sts;
+ private String protocol;
+ private String host;
+ private String stage;
+ private AppClient appClient;
+ private String signingRole;
private ApiGatewayHelper() {
if (Utils.isBlank(AWS_REGION)) {
@@ -83,28 +70,144 @@ private ApiGatewayHelper() {
}
}
- public static String signAndExecuteApiRequest(SdkHttpFullRequest apiRequest, String assumedRole, String context) {
+ public static ApiGatewayHelper clientCredentialsHelper(String appClientSecretArn) {
+ ApiGatewayHelper helper = new ApiGatewayHelper();
+ helper.secrets = Utils.sdkClient(SecretsManagerClient.builder(), SecretsManagerClient.SERVICE_NAME);
+ // Fetch the app client details from SecretsManager
+ try {
+ GetSecretValueResponse response = helper.secrets.getSecretValue(request -> request
+ .secretId(appClientSecretArn)
+ );
+ Map clientDetails = Utils.fromJson(response.secretString(), LinkedHashMap.class);
+ helper.appClient = AppClient.builder()
+ .clientName(clientDetails.get("client_name"))
+ .clientId(clientDetails.get("client_id"))
+ .clientSecret(clientDetails.get("client_secret"))
+ .tokenEndpoint(clientDetails.get("token_endpoint"))
+ .apiEndpoint(clientDetails.get("api_endpoint"))
+ .build();
+ helper.protocol = helper.appClient.getApiEndpointUrl().getProtocol();
+ helper.host = helper.appClient.getApiEndpointUrl().getHost();
+ helper.stage = helper.appClient.getApiEndpointUrl().getPath().substring(1);
+ } catch (SdkServiceException secretsManagerError) {
+ LOGGER.error(Utils.getFullStackTrace(secretsManagerError));
+ throw secretsManagerError;
+ }
+ return helper;
+ }
+
+ public static ApiGatewayHelper iamCredentialsHelper(String signingRoleArn, URL apiGatewayUrl) {
+ ApiGatewayHelper helper = new ApiGatewayHelper();
+ helper.sts = Utils.sdkClient(StsClient.builder(), StsClient.SERVICE_NAME);
+ helper.signingRole = signingRoleArn;
+ helper.protocol = apiGatewayUrl.getProtocol();
+ helper.host = apiGatewayUrl.getHost();
+ helper.stage = apiGatewayUrl.getPath().substring(1);
+ return helper;
+ }
+
+ public String authorizedRequest(String method, String resource) {
+ return authorizedRequest(method, resource, null);
+ }
+
+ public String authorizedRequest(String method, String resource, String body) {
+ if (appClient == null) {
+ throw new IllegalStateException("Missing appClient details");
+ }
+ return executeApiRequest(
+ toSdkHttpFullRequest(HttpRequest.builder()
+ .protocol(protocol)
+ .host(host)
+ .stage(stage)
+ .headers(Map.of("Authorization", getClientCredentialsBearerToken(appClient)))
+ .method(method)
+ .resource(resource)
+ .body(body)
+ .build()
+ )
+ );
+ }
+
+ public String signedRequest(String method, String resource) {
+ return signedRequest(method, resource, null);
+ }
+
+ public String signedRequest(String method, String resource, String body) {
+ if (Utils.isBlank(signingRole)) {
+ throw new IllegalStateException("Missing IAM role ARN");
+ }
+ StackWalker.StackFrame frame = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
+ .walk(stack -> stack
+ .limit(8)
+ .skip(1)
+ .findFirst()
+ .orElse(null)
+ );
+ String assumeRoleSessionName;
+ if (frame != null) {
+ assumeRoleSessionName = frame.getClassName() + "." + frame.getMethodName();
+ } else {
+ assumeRoleSessionName = "Unknown caller in " + SAAS_BOOST_ENV;
+ }
+ return signAndExecuteApiRequest(
+ toSdkHttpFullRequest(HttpRequest.builder()
+ .protocol(protocol)
+ .host(host)
+ .stage(stage)
+ .method(method)
+ .resource(resource)
+ .body(body)
+ .build()
+ ),
+ signingRole,
+ assumeRoleSessionName
+ );
+ }
+
+ public String anonymousRequest(String method, String resource) {
+ return anonymousRequest(method, resource, null);
+ }
+
+ public String anonymousRequest(String method, String resource, String body) {
+ return executeApiRequest(
+ toSdkHttpFullRequest(HttpRequest.builder()
+ .protocol(protocol)
+ .host(host)
+ .stage(stage)
+ .method(method)
+ .resource(resource)
+ .body(body)
+ .build()
+ )
+ );
+ }
+
+ protected String signAndExecuteApiRequest(SdkHttpFullRequest apiRequest, String assumedRole, String context) {
SdkHttpFullRequest signedApiRequest = signApiRequest(apiRequest, assumedRole, context);
return executeApiRequest(apiRequest, signedApiRequest);
}
- public static String executeApiRequest(SdkHttpFullRequest apiRequest) {
+ protected String executeApiRequest(SdkHttpFullRequest apiRequest) {
return executeApiRequest(apiRequest, null);
}
- private static String executeApiRequest(SdkHttpFullRequest apiRequest, SdkHttpFullRequest signedApiRequest) {
- HttpExecuteRequest.Builder requestBuilder = HttpExecuteRequest.builder().request(signedApiRequest != null ? signedApiRequest : apiRequest);
- apiRequest.contentStreamProvider().ifPresent(c -> requestBuilder.contentStreamProvider(c));
+ protected String executeApiRequest(SdkHttpFullRequest apiRequest, SdkHttpFullRequest signedApiRequest) {
+ HttpExecuteRequest.Builder requestBuilder = HttpExecuteRequest.builder()
+ .request(signedApiRequest != null ? signedApiRequest : apiRequest);
+ apiRequest.contentStreamProvider().ifPresent(requestBuilder::contentStreamProvider);
HttpExecuteRequest apiExecuteRequest = requestBuilder.build();
BufferedReader responseReader = null;
String responseBody;
try {
+ LOGGER.debug("Executing API Request {}", apiExecuteRequest.httpRequest().getUri().toString());
HttpExecuteResponse apiResponse = HTTP_CLIENT.prepareRequest(apiExecuteRequest).call();
- responseReader = new BufferedReader(new InputStreamReader(apiResponse.responseBody().get(), StandardCharsets.UTF_8));
+ responseReader = new BufferedReader(new InputStreamReader(apiResponse.responseBody().get(),
+ StandardCharsets.UTF_8));
responseBody = responseReader.lines().collect(Collectors.joining());
- LOGGER.info(responseBody);
+ //LOGGER.debug(responseBody);
if (!apiResponse.httpResponse().isSuccessful()) {
- throw new RuntimeException("{\"statusCode\":" + apiResponse.httpResponse().statusCode() + ", \"message\":\"" + apiResponse.httpResponse().statusText().get() + "\"}");
+ throw new RuntimeException("{\"statusCode\":" + apiResponse.httpResponse().statusCode()
+ + ", \"message\":\"" + apiResponse.httpResponse().statusText().orElse("") + "\"}");
}
} catch (IOException ioe) {
LOGGER.error("HTTP Client error {}", ioe.getMessage());
@@ -119,35 +222,25 @@ private static String executeApiRequest(SdkHttpFullRequest apiRequest, SdkHttpFu
}
}
}
-
return responseBody;
}
- public static SdkHttpFullRequest getApiRequest(String host, String stage, ApiRequest request) {
- return getApiRequest(host, stage, request.getResource(), request.getMethod(), request.getHeaders(), request.getBody());
- }
-
- public static SdkHttpFullRequest getApiRequest(String host, String stage, String resource, SdkHttpMethod method, Map headers, String body) {
+ protected SdkHttpFullRequest toSdkHttpFullRequest(HttpRequest request) {
SdkHttpFullRequest apiRequest;
- String protocol = "https";
try {
- URL url = new URL(protocol, host, stage + "/" + resource);
+ URL url = request.toUrl();
SdkHttpFullRequest.Builder sdkRequestBuilder = SdkHttpFullRequest.builder()
- .protocol(protocol)
- .host(host)
+ .protocol(request.getProtocol())
+ .host(request.getHost())
.encodedPath(url.getPath())
- .method(method);
+ .method(request.getMethod());
appendQueryParams(sdkRequestBuilder, url);
- putHeaders(sdkRequestBuilder, headers);
- if (body != null) {
- sdkRequestBuilder.putHeader("Content-Type", "application/json; charset=utf-8");
- sdkRequestBuilder.contentStreamProvider(() -> new StringInputStream(body));
+ putHeaders(sdkRequestBuilder, request.getHeaders());
+ sdkRequestBuilder.putHeader("Content-Type", "application/json; charset=utf-8");
+ if (SdkHttpMethod.GET != request.getMethod() && request.getBody() != null) {
+ sdkRequestBuilder.contentStreamProvider(() -> new StringInputStream(request.getBody()));
}
apiRequest = sdkRequestBuilder.build();
- } catch (MalformedURLException mue) {
- LOGGER.error("URL parse error {}", mue.getMessage());
- LOGGER.error(Utils.getFullStackTrace(mue));
- throw new RuntimeException(mue);
} catch (URISyntaxException use) {
LOGGER.error("URI parse error {}", use.getMessage());
LOGGER.error(Utils.getFullStackTrace(use));
@@ -156,29 +249,78 @@ public static SdkHttpFullRequest getApiRequest(String host, String stage, String
return apiRequest;
}
- public static SdkHttpFullRequest signApiRequest(SdkHttpFullRequest apiRequest, String assumedRole, String context) {
- Aws4SignerParams sigV4Params = Aws4SignerParams.builder()
+ protected String getClientCredentialsBearerToken(AppClient appClient) {
+ // If we've been called within the access token's expiry period, just return the cached copy
+ Map token = getCachedClientCredentials(appClient.getClientId());
+ if (token == null) {
+ token = executeClientCredentialsGrant(appClient.getTokenEndpointUrl(), appClient.getClientCredentials());
+ // Cache this access token until it expires
+ putCachedClientCredentials(appClient.getClientId(), token);
+ }
+ return "Bearer " + token.get("access_token");
+ }
+
+ protected Map executeClientCredentialsGrant(URL tokenEndpoint, String clientSecret) {
+ // POST to the OAuth provider's token endpoint a client_credentials grant
+ SdkHttpFullRequest.Builder requestBuilder = SdkHttpFullRequest.builder()
+ .protocol(tokenEndpoint.getProtocol())
+ .host(tokenEndpoint.getHost())
+ .encodedPath(tokenEndpoint.getPath())
+ .method(SdkHttpMethod.POST);
+ String body = "grant_type=client_credentials";
+ requestBuilder.putHeader("Content-Type", "application/x-www-form-urlencoded");
+ requestBuilder.putHeader("Authorization", "Basic " + clientSecret);
+ requestBuilder.contentStreamProvider(() -> new StringInputStream(body));
+
+ SdkHttpFullRequest clientCredentialsRequest = requestBuilder.build();
+ Map clientCredentialsGrant = Utils.fromJson(
+ executeApiRequest(clientCredentialsRequest), LinkedHashMap.class);
+ return clientCredentialsGrant;
+ }
+
+ protected Map getCachedClientCredentials(String key) {
+ LOGGER.debug(Utils.toJson(CLIENT_CREDENTIALS_CACHE));
+ Map cached = CLIENT_CREDENTIALS_CACHE.get(key);
+ if (cached != null) {
+ Duration buffer = Duration.ofSeconds(2);
+ if (Instant.now().plus(buffer).isBefore((Instant) cached.get("expiry"))) {
+ LOGGER.debug("Client credentials cache hit {}", key);
+ return (Map) cached.get("token");
+ } else {
+ LOGGER.debug("Cached credentials are expiring < 2s {}", key);
+ }
+ } else {
+ LOGGER.debug("Client credentials cache miss {}", key);
+ }
+ return null;
+ }
+
+ protected void putCachedClientCredentials(String key, Map token) {
+ LOGGER.debug("Caching client credentials for {} seconds {}", token.get("expires_in"), key);
+ CLIENT_CREDENTIALS_CACHE.put(key, Map.of(
+ "expiry", Instant.now().plusSeconds(((Integer) token.get("expires_in")).longValue()),
+ "token", token)
+ );
+ }
+
+ protected SdkHttpFullRequest signApiRequest(SdkHttpFullRequest apiRequest, String assumedRole, String context) {
+ return SIG_V4.sign(apiRequest, Aws4SignerParams.builder()
.signingName("execute-api")
.signingRegion(Region.of(AWS_REGION))
- .awsCredentials(getTemporaryCredentials(assumedRole, context))
- .build();
- SdkHttpFullRequest signedApiRequest = SIG_V4.sign(apiRequest, sigV4Params);
- return signedApiRequest;
+ .awsCredentials(getAwsCredentials(assumedRole, context))
+ .build());
}
- protected static AwsCredentials getTemporaryCredentials(final String assumedRole, final String context) {
- AwsCredentials systemCredentials = null;
-
- //LOGGER.info("Calling AssumeRole for {}", assumedRole);
- // Calling STS here instead of in the constructor so we can name the
- // temporary session with the request context for CloudTrail logging
+ protected AwsCredentials getAwsCredentials(final String assumedRole, final String context) {
+ // Calling STS here so we can name the temporary session with
+ // the request context for CloudTrail logging
+ AwsCredentials awsCredentials;
try {
AssumeRoleResponse response = sts.assumeRole(request -> request
.roleArn(assumedRole)
.durationSeconds(900)
- .roleSessionName((Utils.isNotBlank(context)) ? context : SAAS_BOOST_ENV)
+ .roleSessionName(context)
);
-
//AssumedRoleUser assumedUser = response.assumedRoleUser();
//LOGGER.info("Assumed IAM User {}", assumedUser.arn());
//LOGGER.info("Assumed IAM Role {}", assumedUser.assumedRoleId());
@@ -186,7 +328,7 @@ protected static AwsCredentials getTemporaryCredentials(final String assumedRole
// Could use STSAssumeRoleSessionCredentialsProvider here, but this
// lambda will timeout before we need to refresh the temporary creds
Credentials temporaryCredentials = response.credentials();
- systemCredentials = AwsSessionCredentials.create(
+ awsCredentials = AwsSessionCredentials.create(
temporaryCredentials.accessKeyId(),
temporaryCredentials.secretAccessKey(),
temporaryCredentials.sessionToken());
@@ -195,11 +337,12 @@ protected static AwsCredentials getTemporaryCredentials(final String assumedRole
LOGGER.error(Utils.getFullStackTrace(stsError));
throw stsError;
}
- return systemCredentials;
+ return awsCredentials;
}
- protected static void appendQueryParams(SdkHttpFullRequest.Builder sdkRequestBuilder, URL url) throws URISyntaxException {
- List queryParams = URLEncodedUtils.parse(url.toURI(), StandardCharsets.UTF_8);
+ protected static void appendQueryParams(SdkHttpFullRequest.Builder sdkRequestBuilder, URL url)
+ throws URISyntaxException {
+ List queryParams = new URIBuilder(url.toURI()).getQueryParams();
if (queryParams != null) {
for (NameValuePair queryParam : queryParams) {
sdkRequestBuilder.appendRawQueryParameter(queryParam.getName(), queryParam.getValue());
@@ -214,4 +357,129 @@ protected static void putHeaders(SdkHttpFullRequest.Builder sdkRequestBuilder, M
}
}
}
+
+ private static final class HttpRequest {
+ private final String protocol;
+ private final String host;
+ private final String stage;
+ private final String resource;
+ private final SdkHttpMethod method;
+ private final String body;
+ private final Map headers;
+
+ private HttpRequest(HttpRequest.Builder builder) {
+ this.protocol = Utils.isNotBlank(builder.protocol) ? builder.protocol : "https";
+ this.host = builder.host;
+ this.stage = builder.stage;
+ this.resource = builder.resource;
+ this.method = builder.method;
+ this.body = builder.body;
+ if (builder.headers != null) {
+ this.headers = Collections.unmodifiableMap(builder.headers);
+ } else {
+ this.headers = Collections.unmodifiableMap(Collections.emptyMap());
+ }
+ }
+
+ public static HttpRequest.Builder builder() {
+ return new HttpRequest.Builder();
+ }
+
+ public String getProtocol() {
+ return protocol;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public String getStage() {
+ return stage;
+ }
+
+ public String getResource() {
+ return resource;
+ }
+
+ public SdkHttpMethod getMethod() {
+ return method;
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public Map getHeaders() {
+ return Collections.unmodifiableMap(headers);
+ }
+
+ public URL toUrl() {
+ try {
+ return new URL(protocol, host, "/" + stage + "/" + resource);
+ } catch (MalformedURLException mue) {
+ LOGGER.error("URL parse error {}", mue.getMessage());
+ LOGGER.error(Utils.getFullStackTrace(mue));
+ throw new RuntimeException(mue);
+ }
+ }
+
+ public static final class Builder {
+
+ private String protocol;
+ private String host;
+ private String stage;
+ private String resource;
+ private SdkHttpMethod method;
+ private String body;
+ private Map headers;
+
+ private Builder() {
+ }
+
+ public HttpRequest.Builder protocol(String protocol) {
+ this.protocol = protocol;
+ return this;
+ }
+
+ public HttpRequest.Builder host(String host) {
+ this.host = host;
+ return this;
+ }
+
+ public HttpRequest.Builder stage(String stage) {
+ this.stage = stage;
+ return this;
+ }
+
+ public HttpRequest.Builder resource(String resource) {
+ if (resource != null && resource.startsWith("/")) {
+ this.resource = resource.substring(1);
+ } else {
+ this.resource = resource;
+ }
+ return this;
+ }
+
+ public HttpRequest.Builder method(String method) {
+ this.method = SdkHttpMethod.fromValue(method);
+ return this;
+ }
+
+ public HttpRequest.Builder body(String body) {
+ this.body = body;
+ return this;
+ }
+
+ public HttpRequest.Builder headers(final Map headers) {
+ if (headers != null) {
+ this.headers = Collections.unmodifiableMap(headers);
+ }
+ return this;
+ }
+
+ public HttpRequest build() {
+ return new HttpRequest(this);
+ }
+ }
+ }
}
diff --git a/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiRequest.java b/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiRequest.java
deleted file mode 100644
index 1ac72c38..00000000
--- a/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApiRequest.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.amazon.aws.partners.saasfactory.saasboost;
-
-import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
-import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
-import software.amazon.awssdk.http.SdkHttpMethod;
-
-import java.util.Collections;
-import java.util.Map;
-
-@JsonDeserialize(builder = ApiRequest.Builder.class)
-public class ApiRequest {
-
- private String resource;
- private final SdkHttpMethod method;
- private String body;
- private String callback;
- private final Map headers;
-
- private ApiRequest(Builder builder) {
- this.resource = builder.resource;
- this.method = builder.method;
- this.body = builder.body;
- this.callback = builder.callback;
- if (builder.headers != null) {
- this.headers = Collections.unmodifiableMap(builder.headers);
- } else {
- this.headers = Collections.unmodifiableMap(Collections.EMPTY_MAP);
- }
- }
-
- public static Builder builder() {
- return new Builder();
- }
-
- public String getResource() {
- return resource;
- }
-
- public SdkHttpMethod getMethod() {
- return method;
- }
-
- public String getBody() {
- return body;
- }
-
- public String getCallback() {
- return callback;
- }
-
- public Map getHeaders() {
- return Collections.unmodifiableMap(headers);
- }
-
- @JsonPOJOBuilder(withPrefix = "") // setters aren't named with[Property]
- public static final class Builder {
-
- private String resource;
- private SdkHttpMethod method;
- private String body;
- private String callback;
- private Map headers;
-
- private Builder() {
- }
-
- public Builder resource(String resource) {
- if (resource != null && resource.startsWith("/")) {
- this.resource = resource.substring(1);
- } else {
- this.resource = resource;
- }
- return this;
- }
-
- public Builder method(String method) {
- this.method = SdkHttpMethod.fromValue(method);
- return this;
- }
-
- public Builder body(String body) {
- this.body = body;
- return this;
- }
-
- public Builder callback(String callback) {
- this.callback = callback;
- return this;
- }
-
- public Builder headers(final Map headers) {
- if (headers != null) {
- this.headers = Collections.unmodifiableMap(headers);
- }
- return this;
- }
-
- public ApiRequest build() {
- return new ApiRequest(this);
- }
- }
-}
diff --git a/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppClient.java b/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppClient.java
new file mode 100644
index 00000000..8b56dd25
--- /dev/null
+++ b/layers/apigw-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppClient.java
@@ -0,0 +1,123 @@
+package com.amazon.aws.partners.saasfactory.saasboost;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+@JsonDeserialize(builder = AppClient.Builder.class)
+public class AppClient {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AppClient.class);
+ private final String clientId;
+ private final String clientName;
+ private final String clientSecret;
+ private final String tokenEndpoint;
+ private final String apiEndpoint;
+
+ private AppClient(Builder builder) {
+ this.clientId = builder.clientId;
+ this.clientName = builder.clientName;
+ this.clientSecret = builder.clientSecret;
+ this.tokenEndpoint = builder.tokenEndpoint;
+ this.apiEndpoint = builder.apiEndpoint;
+ }
+
+ public String getClientCredentials() {
+ // Generate a Base64 secret for HTTP Basic authorization
+ return new String(Base64.getEncoder().encode((clientId + ":" + clientSecret)
+ .getBytes(StandardCharsets.UTF_8)
+ ), StandardCharsets.UTF_8);
+ }
+
+ public String getClientId() {
+ return clientId;
+ }
+
+ public String getClientName() {
+ return clientName;
+ }
+
+ public String getClientSecret() {
+ return clientSecret;
+ }
+
+ public String getTokenEndpoint() {
+ return tokenEndpoint;
+ }
+
+ public URL getTokenEndpointUrl() {
+ try {
+ return new URL(getTokenEndpoint());
+ } catch (MalformedURLException mue) {
+ LOGGER.error("URL parse error {}", mue.getMessage());
+ LOGGER.error(Utils.getFullStackTrace(mue));
+ throw new RuntimeException(mue);
+ }
+ }
+
+ public String getApiEndpoint() {
+ return apiEndpoint;
+ }
+
+ public URL getApiEndpointUrl() {
+ try {
+ return new URL(getApiEndpoint());
+ } catch (MalformedURLException mue) {
+ LOGGER.error("URL parse error {}", mue.getMessage());
+ LOGGER.error(Utils.getFullStackTrace(mue));
+ throw new RuntimeException(mue);
+ }
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @JsonPOJOBuilder(withPrefix = "") // setters aren't named with[Property]
+ public static final class Builder {
+
+ private String clientId;
+ private String clientName;
+ private String clientSecret;
+ private String tokenEndpoint;
+ private String apiEndpoint;
+
+ private Builder() {
+ }
+
+ public Builder clientId(String clientId) {
+ this.clientId = clientId;
+ return this;
+ }
+
+ public Builder clientName(String clientName) {
+ this.clientName = clientName;
+ return this;
+ }
+
+ public Builder clientSecret(String clientSecret) {
+ this.clientSecret = clientSecret;
+ return this;
+ }
+
+ public Builder tokenEndpoint(String tokenEndpoint) {
+ this.tokenEndpoint = tokenEndpoint;
+ return this;
+ }
+
+ public Builder apiEndpoint(String apiEndpoint) {
+ this.apiEndpoint = apiEndpoint;
+ return this;
+ }
+
+ public AppClient build() {
+ return new AppClient(this);
+ }
+ }
+}
diff --git a/layers/apigw-helper/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayHelperTest.java b/layers/apigw-helper/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayHelperTest.java
index 708eb92e..ab573195 100644
--- a/layers/apigw-helper/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayHelperTest.java
+++ b/layers/apigw-helper/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/ApiGatewayHelperTest.java
@@ -16,43 +16,62 @@
package com.amazon.aws.partners.saasfactory.saasboost;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import software.amazon.awssdk.http.SdkHttpFullRequest;
+import software.amazon.awssdk.http.SdkHttpMethod;
import java.net.URL;
+import java.time.Instant;
import java.util.List;
import java.util.Map;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
public class ApiGatewayHelperTest {
@Test
public void testAppendQueryParams() throws Exception {
- ApiRequest request = ApiRequest.builder()
- .resource("settings?setting=SAAS_BOOST_STACK&setting=DOMAIN_NAME")
- .method("GET")
- .build();
-
String protocol = "https";
String host = "xxxxxxxxxx.execute-api.us-east-2.amazonaws.com";
String stage = "v1";
+ String method = "GET";
+ String resource = "settings?setting=FOO&setting=BAR";
- URL url = new URL(protocol, host, stage + "/" + request.getResource());
+ URL url = new URL(protocol, host, stage + "/" + resource);
SdkHttpFullRequest.Builder sdkRequestBuilder = SdkHttpFullRequest.builder()
- .protocol(protocol)
+ .protocol("https")
.host(host)
.encodedPath(url.getPath())
- .method(request.getMethod());
+ .method(SdkHttpMethod.fromValue(method));
ApiGatewayHelper.appendQueryParams(sdkRequestBuilder, url);
Map> actual = sdkRequestBuilder.rawQueryParameters();
- assertEquals("2 query params with same name", 1, actual.size());
- assertEquals("2 query params with same name", 2, actual.get("setting").size());
- assertTrue("query parameter is named", actual.containsKey("setting"));
- assertTrue("multivalue param", actual.get("setting").contains("SAAS_BOOST_STACK"));
- assertTrue("multivalue param", actual.get("setting").contains("DOMAIN_NAME"));
+ assertEquals(1, actual.size(), "2 query params with same name");
+ assertEquals(2, actual.get("setting").size(), "2 query params with same name");
+ assertTrue(actual.containsKey("setting"), "query parameter is named");
+ assertTrue(actual.get("setting").contains("FOO"), "multivalue param");
+ assertTrue(actual.get("setting").contains("BAR"), "multivalue param");
+ }
+
+ @Test
+ public void testCachedClientCredentials() {
+// Integer expireSeconds = 300;
+// Integer buffer = 2;
+// Map clientCredentials = Map.of(
+// "access_token", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" +
+// ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ" +
+// ".SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
+// "expires_in", expireSeconds,
+// "token_type", "Bearer"
+// );
+// ApiGatewayHelper api = ApiGatewayHelper.builder().host("").stage("").build();
+// Instant now = Instant.now();
+// Instant expires = now.plusSeconds(expireSeconds);
+// api.putCachedClientCredentials("foo", clientCredentials);
+// Map cached = api.getCachedClientCredentials("foo");
+// assertNotNull(cached);
+// assertEquals(cached.get("access_token"), clientCredentials.get("access_token"));
}
}
diff --git a/layers/cloudformation-utils/pom.xml b/layers/cloudformation-utils/pom.xml
index ed3c950c..cdc11add 100644
--- a/layers/cloudformation-utils/pom.xml
+++ b/layers/cloudformation-utils/pom.xml
@@ -33,6 +33,7 @@ limitations under the License.
+ ${project.basedir}/../..0
diff --git a/layers/cloudformation-utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CloudFormationResponse.java b/layers/cloudformation-utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CloudFormationResponse.java
index a6116875..4b1db65f 100644
--- a/layers/cloudformation-utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CloudFormationResponse.java
+++ b/layers/cloudformation-utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CloudFormationResponse.java
@@ -79,6 +79,7 @@ protected static void send(String responseUrl, String responseBody) {
LOGGER.info("curl -H 'Content-Type: \"\"' -X PUT -d '" + responseBody + "' \"" + responseUrl + "\"");
HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1)
.uri(URI.create(responseUrl))
.setHeader("Content-Type", "")
.PUT(HttpRequest.BodyPublishers.ofString(responseBody, StandardCharsets.UTF_8))
diff --git a/layers/cloudformation-utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/CloudFormationEventDeserializerTest.java b/layers/cloudformation-utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/CloudFormationEventDeserializerTest.java
index f420e845..08367c2e 100644
--- a/layers/cloudformation-utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/CloudFormationEventDeserializerTest.java
+++ b/layers/cloudformation-utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/CloudFormationEventDeserializerTest.java
@@ -16,13 +16,13 @@
package com.amazon.aws.partners.saasfactory.saasboost;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
public class CloudFormationEventDeserializerTest {
diff --git a/functions/workload-deploy/pom.xml b/layers/keycloak-helper/pom.xml
similarity index 65%
rename from functions/workload-deploy/pom.xml
rename to layers/keycloak-helper/pom.xml
index e7e71b2a..a5f2bbc7 100644
--- a/functions/workload-deploy/pom.xml
+++ b/layers/keycloak-helper/pom.xml
@@ -19,10 +19,10 @@ limitations under the License.
4.0.0com.amazon.aws.partners.saasfactory.saasboost
- saasboost-functions
+ saasboost-layers1.0.0
- WorkloadDeploy
+ KeycloakHelper1.0.0jar
@@ -33,6 +33,7 @@ limitations under the License.
+ ${project.basedir}/../..0
@@ -46,15 +47,17 @@ limitations under the License.
org.apache.maven.pluginsmaven-surefire-plugin
+
+
+ us-east-1
+ test
+
+ org.apache.maven.pluginsmaven-assembly-plugin
-
- io.github.git-commit-id
- git-commit-id-maven-plugin
-
@@ -67,42 +70,33 @@ limitations under the License.
provided
- com.amazon.aws.partners.saasfactory.saasboost
- ApiGatewayHelper
- 1.0.0
-
- provided
-
-
- software.amazon.awssdk
- s3
- ${aws.java.sdk.version}
+ org.keycloak
+ keycloak-admin-client
+ 19.0.3
+
- software.amazon.awssdk
- netty-nio-client
+ org.jboss.resteasy
+ resteasy-client
- software.amazon.awssdk
- apache-client
+ org.jboss.resteasy
+ resteasy-multipart-provider
-
-
-
- software.amazon.awssdk
- codepipeline
- ${aws.java.sdk.version}
-
- software.amazon.awssdk
- netty-nio-client
+ org.jboss.resteasy
+ resteasy-jackson2-provider
- software.amazon.awssdk
- apache-client
+ org.jboss.resteasy
+ resteasy-jaxb-provider
+
+ org.jboss.logging
+ jboss-logging
+ 3.3.2.Final
+
-
diff --git a/layers/keycloak-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakAdminApi.java b/layers/keycloak-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakAdminApi.java
new file mode 100644
index 00000000..c86d6422
--- /dev/null
+++ b/layers/keycloak-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakAdminApi.java
@@ -0,0 +1,894 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.aws.partners.saasfactory.saasboost.keycloak;
+
+import com.amazon.aws.partners.saasfactory.saasboost.Utils;
+import org.keycloak.representations.idm.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.*;
+
+public class KeycloakAdminApi {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(KeycloakAdminApi.class);
+ private final HttpClient httpClient = HttpClient.newBuilder().build();
+ private final String keycloakHost;
+ private Map passwordGrant;
+ private Instant tokenExpiry;
+ private String accessToken;
+
+ public KeycloakAdminApi(String keycloakHost, String username, String password) {
+ this.keycloakHost = keycloakHost;
+ setPasswordGrant(adminPasswordGrant(username, password));
+ }
+
+ public KeycloakAdminApi(String keycloakHost, String bearerToken) {
+ this.keycloakHost = keycloakHost;
+ this.accessToken = bearerToken.replaceAll("^[B|b]earer ", "");
+ }
+
+ public ClientRepresentation getClient(RealmRepresentation realm, String clientId) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin/realms/"
+ + realm.getRealm() + "/clients"
+ + "?search=true&clientId=" + URLEncoder.encode(clientId, StandardCharsets.UTF_8));
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1)
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .GET()
+ .build();
+ LOGGER.debug("Invoking Keycloak realm clients endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (HttpURLConnection.HTTP_OK == response.statusCode()) {
+ ClientRepresentation[] clients = Utils.fromJson(response.body(), ClientRepresentation[].class);
+ if (clients != null && clients.length == 1) {
+ return clients[0];
+ } else {
+ LOGGER.error("Can't find client {}", clientId);
+ LOGGER.error(response.body());
+ throw new RuntimeException("Can't find client " + clientId);
+ }
+ } else {
+ LOGGER.error("Received HTTP status " + response.statusCode());
+ LOGGER.error(response.body());
+ throw new RuntimeException("Keycloak realm clients failed with HTTP "
+ + response.statusCode());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public List getClients(RealmRepresentation realm) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin/realms/"
+ + realm.getRealm() + "/clients");
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1)
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .GET()
+ .build();
+ LOGGER.debug("Invoking Keycloak realm clients endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (HttpURLConnection.HTTP_OK == response.statusCode()) {
+ ClientRepresentation[] clients = Utils.fromJson(response.body(), ClientRepresentation[].class);
+ if (clients != null) {
+ return new ArrayList<>(Arrays.asList(clients));
+ } else {
+ LOGGER.error("Can't parse realm clients response {}", response.body());
+ throw new RuntimeException("Invalid response from " + request.uri());
+ }
+ } else {
+ LOGGER.error("Received HTTP status " + response.statusCode());
+ LOGGER.error(response.body());
+ throw new RuntimeException("Keycloak realm clients failed with HTTP "
+ + response.statusCode());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public ClientRepresentation createClient(RealmRepresentation realm, ClientRepresentation client) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm.getRealm() + "/clients");
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1)
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(Utils.toJson(client)))
+ .build();
+ LOGGER.debug("Invoking Keycloak client create endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (response.statusCode() == HttpURLConnection.HTTP_CREATED) {
+ LOGGER.debug("Created client {} {}", client.getName(), client.getClientId());
+ return getClient(realm, client.getClientId());
+ } else {
+ LOGGER.error("Expected HTTP_CREATED ({}) from client create, but got {}",
+ HttpURLConnection.HTTP_CREATED, response.statusCode());
+ throw new RuntimeException("Unexpected error while creating client: " + client.getClientId());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public ClientRepresentation updateClient(RealmRepresentation realm, ClientRepresentation client) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm.getRealm()
+ + "/clients/" + client.getId()
+ );
+ String body = Utils.toJson(client);
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .PUT(HttpRequest.BodyPublishers.ofString(body))
+ .build();
+ LOGGER.debug("Invoking Keycloak update client endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (response.statusCode() == HttpURLConnection.HTTP_NO_CONTENT) {
+ LOGGER.info("Updated client {} {}", client.getName(), client.getClientId());
+ return getClient(realm, client.getClientId());
+ } else {
+ LOGGER.error("Expected HTTP_NO_CONTENT ({}) from update client, but got {}",
+ HttpURLConnection.HTTP_NO_CONTENT, response.statusCode());
+ throw new RuntimeException("Unexpected error while updating client: " + response.body());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public ClientScopeRepresentation getClientScope(RealmRepresentation realm, String scopeName) {
+ return getClientScopes(realm).stream()
+ .filter(scope -> scopeName.equals(scope.getName()))
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException("Can't find client scope " + scopeName));
+ }
+
+ public List getClientScopes(RealmRepresentation realm) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin"
+ + "/realms/" + realm.getRealm() + "/client-scopes/"
+ );
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1)
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .GET()
+ .build();
+ LOGGER.debug("Invoking Keycloak realm client scopes endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (HttpURLConnection.HTTP_OK == response.statusCode()) {
+ ClientScopeRepresentation[] scopes = Utils.fromJson(response.body(),
+ ClientScopeRepresentation[].class);
+ if (scopes != null) {
+ return new ArrayList<>(Arrays.asList(scopes));
+ } else {
+ LOGGER.error("Can't parse realm client scopes response {}", response.body());
+ throw new RuntimeException("Invalid response from " + request.uri());
+ }
+ } else {
+ LOGGER.error("Received HTTP status " + response.statusCode());
+ LOGGER.error(response.body());
+ throw new RuntimeException("Keycloak realm client scopes failed with HTTP "
+ + response.statusCode());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public ProtocolMapperRepresentation getClientScopeProtocolMapper(RealmRepresentation realm,
+ ClientScopeRepresentation clientScope,
+ String protocolMapper) {
+ return getClientScopeProtocolMappers(realm, clientScope).stream()
+ .filter(mapper -> protocolMapper.equals(mapper.getName()))
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException("Can't find protocol mapper " + protocolMapper));
+ }
+
+ public List getClientScopeProtocolMappers(RealmRepresentation realm,
+ ClientScopeRepresentation clientScope) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin"
+ + "/realms/" + realm.getRealm()
+ + "/client-scopes/" + clientScope.getId()
+ + "/protocol-mappers/models"
+ );
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1)
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .GET()
+ .build();
+ LOGGER.debug("Invoking Keycloak realm client scope protocol mappers endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (HttpURLConnection.HTTP_OK == response.statusCode()) {
+ ProtocolMapperRepresentation[] protocolMappers = Utils.fromJson(response.body(),
+ ProtocolMapperRepresentation[].class);
+ if (protocolMappers != null) {
+ return new ArrayList<>(Arrays.asList(protocolMappers));
+ } else {
+ LOGGER.error("Can't parse protocol mappers response {}", response.body());
+ throw new RuntimeException("Invalid response from " + request.uri());
+ }
+ } else {
+ LOGGER.error("Received HTTP status " + response.statusCode());
+ LOGGER.error(response.body());
+ throw new RuntimeException("Keycloak realm client scope protocol mappers failed with HTTP "
+ + response.statusCode());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public ClientRepresentation addClientProtocolMapperModel(RealmRepresentation realm, ClientRepresentation client,
+ ProtocolMapperRepresentation model) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin"
+ + "/realms/" + realm.getRealm()
+ + "/clients/" + client.getId()
+ + "/protocol-mappers"
+ + "/add-models"
+ );
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1)
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(Utils.toJson(List.of(model))))
+ .build();
+ LOGGER.debug("Invoking Keycloak client protocol mapper model {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (response.statusCode() == HttpURLConnection.HTTP_NO_CONTENT) {
+ // let's return the client we just updated
+ LOGGER.debug("Created protocol mapper model {} {}", client.getName(), model.getName());
+ return getClient(realm, client.getClientId());
+ } else {
+ LOGGER.error("Expected HTTP_CREATED ({}) from protocol mapper model create, but got {}",
+ HttpURLConnection.HTTP_CREATED, response.statusCode());
+ throw new RuntimeException("Unexpected error while creating protocol mapper model: " + model.getName());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public RoleRepresentation getRole(RealmRepresentation realm, String roleName) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm.getRealm() + "/roles/"
+ + URLEncoder.encode(roleName, StandardCharsets.UTF_8));
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1)
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .GET()
+ .build();
+ LOGGER.debug("Invoking Keycloak roles endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (response.statusCode() == HttpURLConnection.HTTP_OK) {
+ LOGGER.debug("Found role: {}", response.body());
+ return Utils.fromJson(response.body(), RoleRepresentation.class);
+ } else {
+ LOGGER.error("Expected HTTP_OK ({}) from get role by name, but got {}",
+ HttpURLConnection.HTTP_OK, response.statusCode());
+ throw new RuntimeException("Unexpected error while getting role by name: " + response.body());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public RoleRepresentation createRole(RealmRepresentation realm, RoleRepresentation role) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm.getRealm() + "/roles");
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1)
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(Utils.toJson(role)))
+ .build();
+ LOGGER.debug("Invoking Keycloak role create endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (response.statusCode() == HttpURLConnection.HTTP_CREATED) {
+ // let's return the role we just created
+ LOGGER.debug("Created role {}", role.getName());
+ return getRole(realm, role.getName());
+ } else {
+ LOGGER.error("Expected HTTP_CREATED ({}) from role create, but got {}",
+ HttpURLConnection.HTTP_CREATED, response.statusCode());
+ throw new RuntimeException("Unexpected error while creating role: " + role.getName());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public GroupRepresentation createGroup(RealmRepresentation realm, GroupRepresentation group) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm.getRealm() + "/groups");
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1)
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(Utils.toJson(group)))
+ .build();
+ LOGGER.debug("Invoking Keycloak group create endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (response.statusCode() == HttpURLConnection.HTTP_CREATED) {
+ // let's return the group we just created
+ LOGGER.debug("Created group {}", group.getName());
+ return getGroup(realm, group.getName());
+ } else {
+ LOGGER.error("Expected HTTP_CREATED ({}) from group create, but got {}",
+ HttpURLConnection.HTTP_CREATED, response.statusCode());
+ throw new RuntimeException("Unexpected error while creating group: " + group.getName());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public GroupRepresentation getGroup(RealmRepresentation realm, String groupName) {
+ return getGroups(realm).stream()
+ .filter(group -> groupName.equals(group.getName()))
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException("Can't find group " + groupName));
+ }
+
+ public List getGroups(RealmRepresentation realm) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm.getRealm() + "/groups");
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1)
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .GET()
+ .build();
+ LOGGER.debug("Invoking Keycloak list groups endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (response.statusCode() == HttpURLConnection.HTTP_OK) {
+ LOGGER.debug("Found groups: {}", response.body());
+ GroupRepresentation[] groups = Utils.fromJson(response.body(), GroupRepresentation[].class);
+ if (groups != null) {
+ return new ArrayList<>(Arrays.asList(groups));
+ } else {
+ LOGGER.error("Can't parse groups response {}", response.body());
+ throw new RuntimeException("Invalid response from " + request.uri());
+ }
+ } else {
+ LOGGER.error("Expected HTTP_OK ({}) from list groups, but got {}",
+ HttpURLConnection.HTTP_OK, response.statusCode());
+ throw new RuntimeException("Unexpected error while listing groups: " + response.body());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public RoleRepresentation getClientRole(RealmRepresentation realm, ClientRepresentation client, String roleName) {
+ return getClientRoles(realm, client).stream()
+ .filter(role -> roleName.equals(role.getName()))
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException("Can't find client role " + roleName
+ + " for client " + client.getClientId()));
+ }
+
+ public List getClientRoles(RealmRepresentation realm, ClientRepresentation client) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin"
+ + "/realms/" + realm.getRealm()
+ + "/clients/" + client.getId()
+ + "/roles"
+ );
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1)
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .GET()
+ .build();
+ LOGGER.debug("Invoking Keycloak realm client roles endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (HttpURLConnection.HTTP_OK == response.statusCode()) {
+ RoleRepresentation[] roles = Utils.fromJson(response.body(), RoleRepresentation[].class);
+ if (roles != null) {
+ return new ArrayList<>(Arrays.asList(roles));
+ } else {
+ LOGGER.error("Can't parse realm client roles response {}", response.body());
+ throw new RuntimeException("Invalid response from " + request.uri());
+ }
+ } else {
+ LOGGER.error("Received HTTP status " + response.statusCode());
+ LOGGER.error(response.body());
+ throw new RuntimeException("Keycloak realm client roles failed with HTTP "
+ + response.statusCode());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public List getUsers(RealmRepresentation realm) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm.getRealm() + "/users");
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1)
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .GET()
+ .build();
+ LOGGER.debug("Invoking Keycloak list groups endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (response.statusCode() == HttpURLConnection.HTTP_OK) {
+ LOGGER.debug("Found users: {}", response.body());
+ UserRepresentation[] users = Utils.fromJson(response.body(), UserRepresentation[].class);
+ if (users != null) {
+ return new ArrayList<>(Arrays.asList(users));
+ } else {
+ LOGGER.error("Can't parse users response {}", response.body());
+ throw new RuntimeException("Invalid response from " + request.uri());
+ }
+ } else {
+ LOGGER.error("Expected HTTP_OK ({}) from list users, but got {}",
+ HttpURLConnection.HTTP_OK, response.statusCode());
+ throw new RuntimeException("Unexpected error while listing users: " + response.body());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public UserRepresentation getUser(RealmRepresentation realm, String username) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin/realms/"
+ + realm.getRealm() + "/users?exact=true&username="
+ + URLEncoder.encode(username, StandardCharsets.UTF_8));
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1)
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .GET()
+ .build();
+ LOGGER.debug("Invoking Keycloak realm users endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (HttpURLConnection.HTTP_OK == response.statusCode()) {
+ UserRepresentation[] users = Utils.fromJson(response.body(), UserRepresentation[].class);
+ if (users != null) {
+ if (users.length == 1) {
+ return users[0];
+ } else {
+ LOGGER.error("Can't find user {}", username);
+ LOGGER.error(response.body());
+ throw new RuntimeException("Can't find user " + username);
+ }
+ } else {
+ LOGGER.error("Can't parse realm users response {}", response.body());
+ throw new RuntimeException("Invalid response from " + request.uri());
+ }
+ } else {
+ LOGGER.error("Received HTTP status " + response.statusCode());
+ LOGGER.error(response.body());
+ throw new RuntimeException("Keycloak realm users failed with HTTP "
+ + response.statusCode());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public UserRepresentation createUser(RealmRepresentation realm, UserRepresentation user) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin"
+ + "/realms/" + realm.getRealm()
+ + "/users"
+ );
+ String body = Utils.toJson(user);
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(body))
+ .build();
+ LOGGER.debug("Invoking Keycloak realm users endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (HttpURLConnection.HTTP_CREATED == response.statusCode()) {
+ LOGGER.info("Created user " + user.getUsername());
+ return getUser(realm, user.getUsername());
+ } else {
+ LOGGER.error("Expected HTTP_CREATED ({}) from create user, but got {}",
+ HttpURLConnection.HTTP_CREATED, response.statusCode());
+ throw new RuntimeException("Keycloak users " + user.getUsername()
+ + " failed with HTTP " + response.statusCode());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public UserRepresentation updateUser(RealmRepresentation realm, UserRepresentation user) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm.getRealm()
+ + "/users/" + user.getId()
+ );
+ String body = Utils.toJson(user);
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .PUT(HttpRequest.BodyPublishers.ofString(body))
+ .build();
+ LOGGER.debug("Invoking Keycloak update user scope endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (response.statusCode() == HttpURLConnection.HTTP_NO_CONTENT) {
+ LOGGER.info("Updated user {}", user.getUsername());
+ return getUser(realm, user.getUsername());
+ } else {
+ LOGGER.error("Expected HTTP_NO_CONTENT ({}) from update user, but got {}",
+ HttpURLConnection.HTTP_NO_CONTENT, response.statusCode());
+ throw new RuntimeException("Unexpected error while updating user: " + response.body());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void deleteUser(RealmRepresentation realm, UserRepresentation user) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin/realms/" + realm.getRealm()
+ + "/users/" + user.getId()
+ );
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .DELETE()
+ .build();
+ LOGGER.debug("Invoking Keycloak delete user scope endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (response.statusCode() == HttpURLConnection.HTTP_NO_CONTENT) {
+ LOGGER.info("Deleted user {}", user.getUsername());
+ } else {
+ LOGGER.error("Expected HTTP_NO_CONTENT ({}) from delete user, but got {}",
+ HttpURLConnection.HTTP_NO_CONTENT, response.statusCode());
+ throw new RuntimeException("Unexpected error while deleting user: " + response.body());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public ClientScopeRepresentation createClientScope(RealmRepresentation realm, ClientScopeRepresentation scope) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin"
+ + "/realms/" + realm.getRealm()
+ + "/client-scopes"
+ );
+ String body = Utils.toJson(scope);
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(body))
+ .build();
+ LOGGER.debug("Invoking Keycloak realm client scopes endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (HttpURLConnection.HTTP_CREATED == response.statusCode()) {
+ LOGGER.info("Created client scope {}", scope.getName());
+ return getClientScope(realm, scope.getName());
+ } else {
+ LOGGER.error("Expected HTTP_CREATED ({}) from create client scope, but got {}",
+ HttpURLConnection.HTTP_CREATED, response.statusCode());
+ throw new RuntimeException("Keycloak client scope " + scope.getName()
+ + " failed with HTTP " + response.statusCode());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public RealmRepresentation getRealm(RealmRepresentation realm) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin/realms/"
+ + URLEncoder.encode(realm.getRealm(), StandardCharsets.UTF_8));
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1)
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .GET()
+ .build();
+ LOGGER.debug("Invoking Keycloak realms endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (response.statusCode() == HttpURLConnection.HTTP_OK) {
+ LOGGER.debug("Found realm: {}", response.body());
+ return Utils.fromJson(response.body(), RealmRepresentation.class);
+ } else {
+ LOGGER.error("Expected HTTP_OK ({}) from get realm by name, but got {}",
+ HttpURLConnection.HTTP_OK, response.statusCode());
+ throw new RuntimeException("Unexpected error while getting realm by name: " + response.body());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public RealmRepresentation createRealm(RealmRepresentation realm) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin/realms");
+ String requestBody = Utils.toJson(realm);
+ //String requestBody = JsonSerialization.writeValueAsString(JsonSerialization.createObjectNode(realm));
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(requestBody))
+ .build();
+
+ LOGGER.debug("Invoking Keycloak realm import endpoint {}", request.uri());
+ LOGGER.debug(requestBody);
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (HttpURLConnection.HTTP_CREATED == response.statusCode()) {
+ LOGGER.info("Created realm {}", realm.getRealm());
+ return getRealm(realm);
+ } else {
+ LOGGER.error("Expected HTTP_CREATED ({}) from create realm, but got {}",
+ HttpURLConnection.HTTP_CREATED, response.statusCode());
+ LOGGER.error(response.body());
+ throw new RuntimeException("Keycloak create realm " + realm.getRealm()
+ + " failed with HTTP " + response.statusCode());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void createGroupClientRoleMapping(RealmRepresentation realm, GroupRepresentation group,
+ ClientRepresentation client, RoleRepresentation role) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin"
+ + "/realms/" + realm.getRealm()
+ + "/groups/" + group.getId()
+ + "/role-mappings"
+ + "/clients/" + client.getId()
+ );
+ String body = Utils.toJson(List.of(role));
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(body))
+ .build();
+
+ LOGGER.debug("Invoking Keycloak group client role mapping endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (HttpURLConnection.HTTP_NO_CONTENT == response.statusCode()) {
+ LOGGER.info("Mapped role {} to group {} for client {}",
+ role.getName(), group.getName(), client.getName());
+ } else {
+ throw new RuntimeException("Keycloak clientrole mapping for group failed with HTTP "
+ + response.statusCode());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void createGroupRoleMapping(RealmRepresentation realm, GroupRepresentation group, RoleRepresentation role) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin"
+ + "/realms/" + realm.getRealm()
+ + "/groups/" + group.getId()
+ + "/role-mappings"
+ + "/realm"
+ );
+ // Just in case
+ role.setContainerId(realm.getId());
+ String body = Utils.toJson(List.of(role));
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(body))
+ .build();
+
+ LOGGER.debug("Invoking Keycloak group realm role mapping endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ // The POST to map realm roles to a group returns a 204 instead of a 201 created...
+ if (HttpURLConnection.HTTP_NO_CONTENT == response.statusCode()) {
+ LOGGER.info("Mapped role {} to group {}", role.getName(), group.getName());
+ } else {
+ throw new RuntimeException("Keycloak admin user group attachment failed with HTTP "
+ + response.statusCode());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void addUserToGroup(RealmRepresentation realm, UserRepresentation user, GroupRepresentation group) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/admin"
+ + "/realms/" + realm.getRealm()
+ + "/users/" + user.getId()
+ + "/groups/" + group.getId()
+ );
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding
+ .uri(endpoint)
+ .setHeader("Authorization", "Bearer " + getBearerToken())
+ .setHeader("Content-Type", "application/json")
+ .PUT(HttpRequest.BodyPublishers.noBody())
+ .build();
+
+ LOGGER.debug("Invoking Keycloak user group attach endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ // The POST to map client roles to user returns a 204 instead of a 201 created...
+ if (HttpURLConnection.HTTP_NO_CONTENT == response.statusCode()) {
+ LOGGER.info("Added user {} to group {}", user.getUsername(), group.getName());
+ } else {
+ throw new RuntimeException("Keycloak admin user group attachment failed with HTTP "
+ + response.statusCode());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String getBearerToken() {
+ // Are we using the Keycloak super user password grant and the refreshing
+ // access token from the admin-cli client, or are we using the access token
+ // passed through from a user sign in via the admin web app client?
+ if (passwordGrant != null && passwordGrant.containsKey("access_token")) {
+ //LOGGER.debug("Using admin-cli client access token");
+ if (Instant.now().plus(Duration.ofSeconds(1)).isAfter(tokenExpiry)) {
+ refreshBearerToken();
+ }
+ return (String) passwordGrant.get("access_token");
+ } else if (Utils.isNotBlank(accessToken)) {
+ //LOGGER.debug("Using provided access token");
+ LOGGER.debug(accessToken);
+ return accessToken;
+ } else {
+ throw new IllegalStateException("No bearer token set");
+ }
+ }
+
+ private void refreshBearerToken() {
+ try {
+ URI endpoint = new URI(keycloakHost + "/realms/master/protocol/openid-connect/token");
+ String body = "grant_type=refresh_token"
+ + "&client_id=admin-cli"
+ + "&refresh_token=" + passwordGrant.get("refresh_token");
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding
+ .uri(endpoint)
+ .setHeader("Content-Type", "application/x-www-form-urlencoded")
+ .POST(HttpRequest.BodyPublishers.ofString(body))
+ .build();
+ LOGGER.debug("Invoking Keycloak refresh token endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (HttpURLConnection.HTTP_OK == response.statusCode()) {
+ setPasswordGrant(Utils.fromJson(response.body(), LinkedHashMap.class));
+ } else {
+ LOGGER.error("Received HTTP status " + response.statusCode());
+ LOGGER.error(response.body());
+ throw new RuntimeException("Keycloak admin password grant failed HTTP " + response.statusCode());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ private void setPasswordGrant(Map passwordGrant) {
+ if (passwordGrant == null || passwordGrant.isEmpty()) {
+ throw new IllegalArgumentException("passwordGrant required");
+ }
+ this.passwordGrant = passwordGrant;
+ this.tokenExpiry = Instant.now().plusSeconds(((Integer) passwordGrant.get("expires_in")).longValue());
+ }
+
+ private Map adminPasswordGrant(String username, String password) {
+ try {
+ URI endpoint = new URI(keycloakHost + "/realms/master/protocol/openid-connect/token");
+ String body = "grant_type=password"
+ + "&client_id=admin-cli"
+ + "&username=" + URLEncoder.encode(username, StandardCharsets.UTF_8)
+ + "&password=" + URLEncoder.encode(password, StandardCharsets.UTF_8);
+ HttpRequest request = HttpRequest.newBuilder()
+ .version(HttpClient.Version.HTTP_1_1) // EOF reached while reading due to chunked transfer-encoding
+ .uri(endpoint)
+ .setHeader("Content-Type", "application/x-www-form-urlencoded")
+ .POST(HttpRequest.BodyPublishers.ofString(body))
+ .build();
+
+ LOGGER.debug("Invoking Keycloak password grant endpoint {}", request.uri());
+ HttpResponse response = httpClient.send(request,
+ HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ if (HttpURLConnection.HTTP_OK == response.statusCode()) {
+ return Utils.fromJson(response.body(), LinkedHashMap.class);
+ } else {
+ LOGGER.error("Received HTTP status " + response.statusCode());
+ LOGGER.error(response.body());
+ throw new RuntimeException("Keycloak admin password grant failed HTTP " + response.statusCode());
+ }
+ } catch (URISyntaxException | IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/layers/keycloak-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakUtils.java b/layers/keycloak-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakUtils.java
new file mode 100644
index 00000000..60740dc4
--- /dev/null
+++ b/layers/keycloak-helper/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/keycloak/KeycloakUtils.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.aws.partners.saasfactory.saasboost.keycloak;
+
+import com.amazon.aws.partners.saasfactory.saasboost.Utils;
+import org.keycloak.representations.idm.*;
+
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class KeycloakUtils {
+
+ private KeycloakUtils() {
+ }
+
+ public static RealmRepresentation asRealm(String realmName) {
+ RealmRepresentation realm = new RealmRepresentation();
+ realm.setRealm(realmName);
+ realm.setEnabled(Boolean.TRUE);
+ return realm;
+ }
+
+ public static RoleRepresentation asRole(String roleName) {
+ RoleRepresentation role = new RoleRepresentation();
+ role.setName(roleName);
+ return role;
+ }
+
+ public static GroupRepresentation asGroup(String groupName) {
+ GroupRepresentation group = new GroupRepresentation();
+ group.setName(groupName);
+ return group;
+ }
+
+ public static UserRepresentation asUser(String username, String email, String password) {
+ UserRepresentation user = new UserRepresentation();
+ user.setEnabled(true);
+ user.setCreatedTimestamp(LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli());
+ user.setUsername(username);
+ user.setEmail(email);
+ user.setEmailVerified(true);
+ CredentialRepresentation credentials = new CredentialRepresentation();
+ credentials.setType("password");
+ credentials.setTemporary(true);
+ credentials.setValue(password);
+ user.setCredentials(List.of(credentials));
+ user.setRequiredActions(List.of("UPDATE_PASSWORD"));
+ return user;
+ }
+
+ public static ClientRepresentation asConfidentialClient(String clientName, String clientId,
+ String description, List scopes) {
+ ClientRepresentation client = new ClientRepresentation();
+ client.setEnabled(true);
+ client.setProtocol("openid-connect");
+ client.setName(clientName);
+ client.setClientId(clientId);
+ client.setDescription(description);
+ client.setStandardFlowEnabled(false);
+ client.setDirectAccessGrantsEnabled(false);
+ client.setImplicitFlowEnabled(false);
+ client.setServiceAccountsEnabled(true);
+ client.setPublicClient(false);
+ client.setAttributes(Map.of("use.refresh.tokens", Boolean.FALSE.toString()));
+ client.setDefaultClientScopes(scopes);
+ client.setProtocolMappers(new ArrayList<>());
+ return client;
+ }
+
+ public static ClientRepresentation asPublicClient(String clientName, String clientId, String description,
+ String redirects) {
+ ClientRepresentation client = new ClientRepresentation();
+ client.setEnabled(true);
+ client.setProtocol("openid-connect");
+ client.setName(clientName);
+ client.setClientId(clientId);
+ client.setDescription(description);
+ client.setStandardFlowEnabled(true);
+ client.setDirectAccessGrantsEnabled(false);
+ client.setImplicitFlowEnabled(false);
+ client.setServiceAccountsEnabled(false);
+ client.setPublicClient(true);
+ client.setAttributes(Map.of("pkce.code.challenge.method", "S256"));
+ client.setRedirectUris(List.of(redirects));
+ client.setProtocolMappers(new ArrayList<>());
+ return client;
+ }
+
+ public static ClientScopeRepresentation asClientScope(String scope, String description) {
+ ClientScopeRepresentation clientScope = new ClientScopeRepresentation();
+ clientScope.setName(scope);
+ clientScope.setDescription(description);
+ clientScope.setProtocol("openid-connect");
+ clientScope.setAttributes(Map.of(
+ "include.in.token.scope", Boolean.TRUE.toString(),
+ "display.on.consent.screen", Boolean.FALSE.toString())
+ );
+ return clientScope;
+ }
+
+ public static CredentialRepresentation temporaryPassword() {
+ CredentialRepresentation tempPassword = new CredentialRepresentation();
+ tempPassword.setType("password");
+ tempPassword.setTemporary(Boolean.TRUE);
+ tempPassword.setValue(Utils.randomString(12));
+ return tempPassword;
+ }
+}
diff --git a/functions/system-rest-api-client/src/main/resources/lambda-assembly.xml b/layers/keycloak-helper/src/main/resources/lambda-layer-assembly.xml
similarity index 69%
rename from functions/system-rest-api-client/src/main/resources/lambda-assembly.xml
rename to layers/keycloak-helper/src/main/resources/lambda-layer-assembly.xml
index 26364854..2daf79c4 100644
--- a/functions/system-rest-api-client/src/main/resources/lambda-assembly.xml
+++ b/layers/keycloak-helper/src/main/resources/lambda-layer-assembly.xml
@@ -21,22 +21,11 @@ limitations under the License.
zipfalse
-
-
-
- ${project.build.outputDirectory}
-
- com/amazon/aws/partners/saasfactory/**
- log4j2.xml
- git.properties
-
-
-
- false
+ java/lib
+ truetrue
- lib
-
\ No newline at end of file
+
diff --git a/layers/keycloak-helper/update.sh b/layers/keycloak-helper/update.sh
new file mode 100755
index 00000000..3e04b6ef
--- /dev/null
+++ b/layers/keycloak-helper/update.sh
@@ -0,0 +1,81 @@
+#!/bin/bash
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License").
+# You may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+if [ -z $1 ]; then
+ echo "Usage: $0 [Lambda Folder]"
+ exit 2
+fi
+
+MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}')
+echo "AWS Region = $MY_AWS_REGION"
+
+ENVIRONMENT=$1
+LAMBDA_STAGE_FOLDER=$2
+if [ -z $LAMBDA_STAGE_FOLDER ]; then
+ LAMBDA_STAGE_FOLDER="lambdas"
+fi
+LAMBDA_CODE=KeycloakHelper-lambda.zip
+LAYER_NAME="sb-${ENVIRONMENT}-keycloak-helper"
+
+#set this for V2 AWS CLI to disable paging
+export AWS_PAGER=""
+
+SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text)
+echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET"
+if [ -z $SAAS_BOOST_BUCKET ]; then
+ echo "Can't find SAAS_BOOST_BUCKET in Parameter Store"
+ exit 1
+fi
+
+# Do a fresh build of the project
+mvn
+if [ $? -ne 0 ]; then
+ echo "Error building project"
+ exit 1
+fi
+
+# And copy it up to S3
+aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/
+
+# Publish a new version of the layer
+PUBLISHED_LAYER=$(aws --region $MY_AWS_REGION lambda publish-layer-version --layer-name "${LAYER_NAME}" --compatible-runtimes java11 --content S3Bucket="${SAAS_BOOST_BUCKET}",S3Key="${LAMBDA_STAGE_FOLDER}/${LAMBDA_CODE}")
+
+# Use eval to deal with the backticks in the filter expression
+eval LAYER_VERSION_ARN=\$\("aws lambda list-layers --query 'Layers[?LayerName==\`${LAYER_NAME}\`].LatestMatchingVersion.LayerVersionArn' --output text"\)
+echo "Published new layer = $LAYER_VERSION_ARN"
+
+# Find all the functions for this SaaS Boost environment that have layers
+eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-\`)] | [?Layers != null] | [].FunctionName' --output text"\)
+#echo "Updating ${#FUNCTIONS[@]} functions with new layer version"
+
+for FX in ${FUNCTIONS[@]}; do
+ # The order of the function's layers must be maintained. Iterate through this function's layers
+ # and update this layer's ARN with the newly published version.
+ FOUND=0
+ LAYERS=""
+ for LAYER_ARN in $(aws --region $MY_AWS_REGION lambda get-function --function-name $FX --query 'Configuration.Layers[].Arn' --output text); do
+ if [[ $LAYER_ARN == *"${LAYER_NAME}"* ]]; then
+ LAYER_ARN=$LAYER_VERSION_ARN
+ FOUND=1
+ fi
+ if [ ${#LAYERS} -gt 0 ]; then
+ LAYERS="${LAYERS} "
+ fi
+ LAYERS="${LAYERS}${LAYER_ARN}"
+ done
+ if (( $FOUND )); then
+ eval "aws --region $MY_AWS_REGION lambda update-function-configuration --function-name $FX --layers $LAYERS"
+ fi
+done
\ No newline at end of file
diff --git a/layers/pom.xml b/layers/pom.xml
index ea4ac1b9..fdfeb898 100644
--- a/layers/pom.xml
+++ b/layers/pom.xml
@@ -14,10 +14,15 @@
pomhttps://github.com/awslabs/aws-saas-boost
- apigw-helperutils
- cloudformation-utils
+ apigw-helper
+ cloudformation-utils
+ keycloak-helper
+ saas-boost-api-client-helper/java
+
+ ${project.basedir}/..
+ Apache-2.0
@@ -48,8 +53,12 @@
slf4j-api
- junit
- junit
+ org.junit.jupiter
+ junit-jupiter-engine
+
+
+ org.junit.jupiter
+ junit-jupiter-apiorg.slf4j
diff --git a/layers/saas-boost-api-client-helper/.gitignore b/layers/saas-boost-api-client-helper/.gitignore
new file mode 100644
index 00000000..378eac25
--- /dev/null
+++ b/layers/saas-boost-api-client-helper/.gitignore
@@ -0,0 +1 @@
+build
diff --git a/layers/saas-boost-api-client-helper/build.sh b/layers/saas-boost-api-client-helper/build.sh
new file mode 100755
index 00000000..64a8ee9a
--- /dev/null
+++ b/layers/saas-boost-api-client-helper/build.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+artifact="SaaSBoostApiClientHelper"
+package=$artifact-lambda.zip
+
+if [ -d build ]
+then
+ echo "cleaning existing build dir"
+ rm -rf build
+fi
+
+mkdir build
+cp -a python build
+
+mvn -q -f java/pom.xml clean package
+unzip -q java/target/$package -d build
+
+cd build
+if [ -f $package ]
+then
+ rm -f $package
+fi
+zip -r $package .
+cd ..
diff --git a/layers/saas-boost-api-client-helper/java/pom.xml b/layers/saas-boost-api-client-helper/java/pom.xml
new file mode 100644
index 00000000..16373267
--- /dev/null
+++ b/layers/saas-boost-api-client-helper/java/pom.xml
@@ -0,0 +1,255 @@
+
+
+
+ 4.0.0
+ com.amazon.aws.partners.saasfactory.saasboost
+ SaaSBoostApiClientHelper
+ 1.0.0
+ jar
+
+
+
+ Apache-2.0
+ http://www.apache.org/licenses/LICENSE-2.0
+
+
+
+
+ ${project.basedir}/../../..
+ UTF-8
+ 0
+ 3.1.1
+ 3.3.0
+ 4.7.3.5
+ 3.8.1
+ 11
+ 3.0.0
+ 5.9.3
+ 1.7.32
+ 2.20.120
+ 2.13.3
+
+
+
+ ${project.artifactId}
+ clean install
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ ${compiler.version}
+
+ ${compiler.java.version}
+ ${compiler.java.version}
+ UTF-8
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ ${surefire.version}
+
+
+
+ org.apache.logging.log4j:log4j-slf4j-impl
+
+ ${env.JAVA_HOME}/bin/java
+
+ us-east-1
+ test
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ ${assembly.version}
+
+ false
+
+ src/main/resources/lambda-layer-assembly.xml
+
+
+
+
+ package
+
+ single
+
+
+
+
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+ ${spotbugs.version}
+
+ false
+
+
+
+ spot-bugs
+ verify
+
+ check
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ ${checkstyle.version}
+
+ UTF-8
+ src/main/resources/checkstyle/checkstyle.xml
+ true
+ true
+ true
+ warning
+ javadoc
+ true
+ false
+
+
+
+ checkstyle
+ validate
+
+ check
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-project-info-reports-plugin
+ 2.9
+
+
+ io.github.git-commit-id
+ git-commit-id-maven-plugin
+ 5.0.0
+
+
+ get-the-git-infos
+
+ revision
+
+ initialize
+
+
+
+ true
+ ${project.build.outputDirectory}/git.properties
+
+ ^git.commit.id.abbrev
+ ^git.commit.id.describe
+ ^git.commit.id.describe-short
+ ^git.closest.tag.name
+
+ full
+ ${project.basedir}/../../../.git
+ false
+
+
+ true
+
+
+
+
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit.version}
+ test
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+ org.slf4j
+ slf4j-nop
+ ${slf4j.version}
+
+
+ org.apache.httpcomponents.core5
+ httpcore5
+ 5.2.2
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ ${jackson.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ ${jackson.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.13.4.1
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ ${jackson.version}
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-joda
+ ${jackson.version}
+
+
+ software.amazon.awssdk
+ url-connection-client
+ ${aws.java.sdk.version}
+
+
+ software.amazon.awssdk
+ secretsmanager
+ ${aws.java.sdk.version}
+
+
+ software.amazon.awssdk
+ netty-nio-client
+
+
+ software.amazon.awssdk
+ apache-client
+
+
+
+
+
+
\ No newline at end of file
diff --git a/layers/saas-boost-api-client-helper/java/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppClient.java b/layers/saas-boost-api-client-helper/java/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppClient.java
new file mode 100644
index 00000000..1f62372e
--- /dev/null
+++ b/layers/saas-boost-api-client-helper/java/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/AppClient.java
@@ -0,0 +1,133 @@
+package com.amazon.aws.partners.saasfactory.saasboost;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+@JsonDeserialize(builder = AppClient.Builder.class)
+public class AppClient {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AppClient.class);
+ private final String clientId;
+ private final String clientName;
+ private final String clientSecret;
+ private final String tokenEndpoint;
+ private final String apiEndpoint;
+
+ private AppClient(Builder builder) {
+ this.clientId = builder.clientId;
+ this.clientName = builder.clientName;
+ this.clientSecret = builder.clientSecret;
+ this.tokenEndpoint = builder.tokenEndpoint;
+ this.apiEndpoint = builder.apiEndpoint;
+ }
+
+ public String getClientCredentials() {
+ // Generate a Base64 secret for HTTP Basic authorization
+ return new String(Base64.getEncoder().encode((clientId + ":" + clientSecret)
+ .getBytes(StandardCharsets.UTF_8)
+ ), StandardCharsets.UTF_8);
+ }
+
+ public String getApiEndpointProtocol() {
+ return getApiEndpointUrl().getProtocol();
+ }
+
+ public String getApiEndpointHost() {
+ return getApiEndpointUrl().getHost();
+ }
+
+ public String getApiEndpointStage() {
+ return getApiEndpointUrl().getPath().substring(1);
+ }
+
+ public String getClientId() {
+ return clientId;
+ }
+
+ public String getClientName() {
+ return clientName;
+ }
+
+ public String getClientSecret() {
+ return clientSecret;
+ }
+
+ public String getTokenEndpoint() {
+ return tokenEndpoint;
+ }
+
+ public URL getTokenEndpointUrl() {
+ try {
+ return new URL(getTokenEndpoint());
+ } catch (MalformedURLException mue) {
+ LOGGER.error("URL parse error {}", mue.getMessage());
+ throw new RuntimeException(mue);
+ }
+ }
+
+ public String getApiEndpoint() {
+ return apiEndpoint;
+ }
+
+ public URL getApiEndpointUrl() {
+ try {
+ return new URL(getApiEndpoint());
+ } catch (MalformedURLException mue) {
+ LOGGER.error("URL parse error {}", mue.getMessage());
+ throw new RuntimeException(mue);
+ }
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @JsonPOJOBuilder(withPrefix = "") // setters aren't named with[Property]
+ public static final class Builder {
+
+ private String clientId;
+ private String clientName;
+ private String clientSecret;
+ private String tokenEndpoint;
+ private String apiEndpoint;
+
+ private Builder() {
+ }
+
+ public Builder clientId(String clientId) {
+ this.clientId = clientId;
+ return this;
+ }
+
+ public Builder clientName(String clientName) {
+ this.clientName = clientName;
+ return this;
+ }
+
+ public Builder clientSecret(String clientSecret) {
+ this.clientSecret = clientSecret;
+ return this;
+ }
+
+ public Builder tokenEndpoint(String tokenEndpoint) {
+ this.tokenEndpoint = tokenEndpoint;
+ return this;
+ }
+
+ public Builder apiEndpoint(String apiEndpoint) {
+ this.apiEndpoint = apiEndpoint;
+ return this;
+ }
+
+ public AppClient build() {
+ return new AppClient(this);
+ }
+ }
+}
diff --git a/layers/saas-boost-api-client-helper/java/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostApiHelper.java b/layers/saas-boost-api-client-helper/java/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostApiHelper.java
new file mode 100644
index 00000000..d0661b5d
--- /dev/null
+++ b/layers/saas-boost-api-client-helper/java/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/SaaSBoostApiHelper.java
@@ -0,0 +1,459 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.aws.partners.saasfactory.saasboost;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.net.URIBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
+import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
+import software.amazon.awssdk.core.exception.SdkServiceException;
+import software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting;
+import software.amazon.awssdk.core.retry.RetryPolicy;
+import software.amazon.awssdk.core.retry.backoff.BackoffStrategy;
+import software.amazon.awssdk.core.retry.conditions.RetryCondition;
+import software.amazon.awssdk.http.*;
+import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
+import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse;
+import software.amazon.awssdk.utils.StringInputStream;
+
+import java.io.*;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class SaaSBoostApiHelper {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SaaSBoostApiHelper.class);
+ private static final String AWS_REGION = System.getenv("AWS_REGION");
+ private static final DateFormat JAVASCRIPT_ISO8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX'Z'");
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+ private static final SdkHttpClient HTTP_CLIENT = UrlConnectionHttpClient.create();
+
+ static {
+ JAVASCRIPT_ISO8601.setTimeZone(TimeZone.getTimeZone("UTC"));
+ MAPPER.findAndRegisterModules();
+ MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+ MAPPER.setDateFormat(JAVASCRIPT_ISO8601);
+ MAPPER.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
+ MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+ MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+ MAPPER.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
+ MAPPER.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING);
+ }
+
+ private final Map> cache = new HashMap<>();
+ private final SecretsManagerClient secrets;
+ private AppClient appClient;
+
+ public SaaSBoostApiHelper(String appClientSecretArn) {
+ this(new DefaultDependencyFactory(), appClientSecretArn);
+ }
+
+ // Facilitates testing by being able to mock out the Secrets Manager dependency
+ public SaaSBoostApiHelper(SaaSBoostApiHelperDependencyFactory init, String appClientSecretArn) {
+ if (AWS_REGION == null || AWS_REGION.isBlank()) {
+ throw new IllegalStateException("Missing environment variable AWS_REGION");
+ }
+ this.secrets = init.secrets();
+ // Fetch the app client details from SecretsManager
+ try {
+ GetSecretValueResponse response = secrets.getSecretValue(request -> request
+ .secretId(appClientSecretArn)
+ );
+ Map clientDetails = fromJson(response.secretString(), LinkedHashMap.class);
+ appClient = AppClient.builder()
+ .clientName(clientDetails.get("client_name"))
+ .clientId(clientDetails.get("client_id"))
+ .clientSecret(clientDetails.get("client_secret"))
+ .tokenEndpoint(clientDetails.get("token_endpoint"))
+ .apiEndpoint(clientDetails.get("api_endpoint"))
+ .build();
+ } catch (SdkServiceException secretsManagerError) {
+ LOGGER.error(getFullStackTrace(secretsManagerError));
+ throw secretsManagerError;
+ }
+ }
+
+ public String authorizedRequest(String method, String resource) {
+ return authorizedRequest(method, resource, null);
+ }
+
+ public String authorizedRequest(String method, String resource, String body) {
+ return executeApiRequest(
+ toSdkHttpFullRequest(HttpRequest.builder()
+ .protocol(appClient.getApiEndpointProtocol())
+ .host(appClient.getApiEndpointHost())
+ .stage(appClient.getApiEndpointStage())
+ .headers(Map.of("Authorization", getClientCredentialsBearerToken(appClient)))
+ .method(method)
+ .resource(resource)
+ .body(body)
+ .build()
+ )
+ );
+ }
+
+ public String anonymousRequest(String method, String resource) {
+ return anonymousRequest(method, resource, null);
+ }
+
+ public String anonymousRequest(String method, String resource, String body) {
+ return executeApiRequest(
+ toSdkHttpFullRequest(HttpRequest.builder()
+ .protocol(appClient.getApiEndpointProtocol())
+ .host(appClient.getApiEndpointHost())
+ .stage(appClient.getApiEndpointStage())
+ .method(method)
+ .resource(resource)
+ .body(body)
+ .build()
+ )
+ );
+ }
+
+ protected String executeApiRequest(SdkHttpFullRequest apiRequest) {
+ HttpExecuteRequest.Builder requestBuilder = HttpExecuteRequest.builder()
+ .request(apiRequest);
+ apiRequest.contentStreamProvider().ifPresent(requestBuilder::contentStreamProvider);
+ HttpExecuteRequest apiExecuteRequest = requestBuilder.build();
+ BufferedReader responseReader = null;
+ String responseBody;
+ try {
+ LOGGER.debug("Executing API Request {} {}", apiExecuteRequest.httpRequest().method(),
+ apiExecuteRequest.httpRequest().getUri().toString());
+ HttpExecuteResponse apiResponse = HTTP_CLIENT.prepareRequest(apiExecuteRequest).call();
+ responseReader = new BufferedReader(new InputStreamReader(apiResponse.responseBody().get(),
+ StandardCharsets.UTF_8));
+ responseBody = responseReader.lines().collect(Collectors.joining());
+ //LOGGER.debug(responseBody);
+ if (!apiResponse.httpResponse().isSuccessful()) {
+ throw new RuntimeException("{\"statusCode\":" + apiResponse.httpResponse().statusCode()
+ + ", \"message\":\"" + apiResponse.httpResponse().statusText().orElse("") + "\"}");
+ }
+ } catch (IOException ioe) {
+ LOGGER.error("HTTP Client error {}", ioe.getMessage());
+ LOGGER.error(getFullStackTrace(ioe));
+ throw new RuntimeException(ioe);
+ } finally {
+ if (responseReader != null) {
+ try {
+ responseReader.close();
+ } catch (IOException ioe) {
+ // swallow
+ }
+ }
+ }
+ return responseBody;
+ }
+
+ protected SdkHttpFullRequest toSdkHttpFullRequest(HttpRequest request) {
+ SdkHttpFullRequest apiRequest;
+ try {
+ URL url = request.toUrl();
+ SdkHttpFullRequest.Builder sdkRequestBuilder = SdkHttpFullRequest.builder()
+ .protocol(request.getProtocol())
+ .host(request.getHost())
+ .encodedPath(url.getPath())
+ .method(request.getMethod());
+ appendQueryParams(sdkRequestBuilder, url);
+ putHeaders(sdkRequestBuilder, request.getHeaders());
+ sdkRequestBuilder.putHeader("Content-Type", "application/json; charset=utf-8");
+ if (SdkHttpMethod.GET != request.getMethod() && request.getBody() != null) {
+ sdkRequestBuilder.contentStreamProvider(() -> new StringInputStream(request.getBody()));
+ }
+ apiRequest = sdkRequestBuilder.build();
+ } catch (URISyntaxException use) {
+ LOGGER.error("URI parse error {}", use.getMessage());
+ LOGGER.error(getFullStackTrace(use));
+ throw new RuntimeException(use);
+ }
+ return apiRequest;
+ }
+
+ protected String getClientCredentialsBearerToken(AppClient appClient) {
+ // If we've been called within the access token's expiry period, just return the cached copy
+ Map token = getCachedClientCredentials(appClient.getClientId());
+ if (token == null) {
+ token = executeClientCredentialsGrant(appClient.getTokenEndpointUrl(), appClient.getClientCredentials());
+ // Cache this access token until it expires
+ putCachedClientCredentials(appClient.getClientId(), token);
+ }
+ return "Bearer " + token.get("access_token");
+ }
+
+ protected Map executeClientCredentialsGrant(URL tokenEndpoint, String clientSecret) {
+ // POST to the OAuth provider's token endpoint a client_credentials grant
+ SdkHttpFullRequest.Builder requestBuilder = SdkHttpFullRequest.builder()
+ .protocol(tokenEndpoint.getProtocol())
+ .host(tokenEndpoint.getHost())
+ .encodedPath(tokenEndpoint.getPath())
+ .method(SdkHttpMethod.POST);
+ String body = "grant_type=client_credentials";
+ requestBuilder.putHeader("Content-Type", "application/x-www-form-urlencoded");
+ requestBuilder.putHeader("Authorization", "Basic " + clientSecret);
+ requestBuilder.contentStreamProvider(() -> new StringInputStream(body));
+
+ SdkHttpFullRequest clientCredentialsRequest = requestBuilder.build();
+ Map clientCredentialsGrant = fromJson(
+ executeApiRequest(clientCredentialsRequest), LinkedHashMap.class);
+ return clientCredentialsGrant;
+ }
+
+ protected Map getCachedClientCredentials(String key) {
+ LOGGER.debug(toJson(cache));
+ Map cached = cache.get(key);
+ if (cached != null) {
+ Duration buffer = Duration.ofSeconds(2);
+ if (Instant.now().plus(buffer).isBefore((Instant) cached.get("expiry"))) {
+ LOGGER.debug("Client credentials cache hit {}", key);
+ return (Map) cached.get("token");
+ } else {
+ LOGGER.debug("Cached credentials are expiring < 2s {}", key);
+ }
+ } else {
+ LOGGER.debug("Client credentials cache miss {}", key);
+ }
+ return null;
+ }
+
+ protected void putCachedClientCredentials(String key, Map token) {
+ LOGGER.debug("Caching client credentials for {} seconds {}", token.get("expires_in"), key);
+ cache.put(key, Map.of(
+ "expiry", Instant.now().plusSeconds(((Integer) token.get("expires_in")).longValue()),
+ "token", token)
+ );
+ }
+
+ protected void appendQueryParams(SdkHttpFullRequest.Builder sdkRequestBuilder, URL url)
+ throws URISyntaxException {
+ List queryParams = new URIBuilder(url.toURI()).getQueryParams();
+ for (NameValuePair queryParam : queryParams) {
+ sdkRequestBuilder.appendRawQueryParameter(queryParam.getName(), queryParam.getValue());
+ }
+ }
+
+ protected void putHeaders(SdkHttpFullRequest.Builder sdkRequestBuilder, Map headers) {
+ if (sdkRequestBuilder != null && headers != null) {
+ for (Map.Entry header : headers.entrySet()) {
+ sdkRequestBuilder.putHeader(header.getKey(), header.getValue());
+ }
+ }
+ }
+
+ private String toJson(Object obj) {
+ String json = null;
+ try {
+ json = MAPPER.writeValueAsString(obj);
+ } catch (Exception e) {
+ LOGGER.error(getFullStackTrace(e));
+ }
+ return json;
+ }
+
+ private T fromJson(String json, Class serializeTo) {
+ T object = null;
+ try {
+ object = MAPPER.readValue(json, serializeTo);
+ } catch (Exception e) {
+ LOGGER.error(getFullStackTrace(e));
+ }
+ return object;
+ }
+
+ private String getFullStackTrace(Exception e) {
+ final StringWriter sw = new StringWriter();
+ final PrintWriter pw = new PrintWriter(sw, true);
+ e.printStackTrace(pw);
+ return sw.getBuffer().toString();
+ }
+
+ interface SaaSBoostApiHelperDependencyFactory {
+ SecretsManagerClient secrets();
+ }
+
+ private static final class DefaultDependencyFactory implements SaaSBoostApiHelperDependencyFactory {
+
+ @Override
+ public SecretsManagerClient secrets() {
+ Region region = Region.of(AWS_REGION);
+ String endpoint = "https://" + SecretsManagerClient.SERVICE_NAME + "." + region.id()
+ + "." + region.metadata().partition().dnsSuffix();
+
+ return SecretsManagerClient.builder()
+ .httpClientBuilder(UrlConnectionHttpClient.builder())
+ .credentialsProvider(EnvironmentVariableCredentialsProvider.create())
+ .region(region)
+ .endpointOverride(URI.create(endpoint))
+ .overrideConfiguration(ClientOverrideConfiguration.builder()
+ .retryPolicy(RetryPolicy.builder()
+ .backoffStrategy(BackoffStrategy.defaultStrategy())
+ .throttlingBackoffStrategy(BackoffStrategy.defaultThrottlingStrategy())
+ .numRetries(SdkDefaultRetrySetting.defaultMaxAttempts())
+ .retryCondition(RetryCondition.defaultRetryCondition())
+ .build()
+ )
+ .build()
+ )
+ .build();
+ }
+ }
+
+ private static final class HttpRequest {
+ private final String protocol;
+ private final String host;
+ private final String stage;
+ private final String resource;
+ private final SdkHttpMethod method;
+ private final String body;
+ private final Map headers;
+
+ private HttpRequest(HttpRequest.Builder builder) {
+ this.protocol = builder.protocol;
+ this.host = builder.host;
+ this.stage = builder.stage;
+ this.resource = builder.resource;
+ this.method = builder.method;
+ this.body = builder.body;
+ if (builder.headers != null) {
+ this.headers = Collections.unmodifiableMap(builder.headers);
+ } else {
+ this.headers = Collections.emptyMap();
+ }
+ }
+
+ public static HttpRequest.Builder builder() {
+ return new HttpRequest.Builder();
+ }
+
+ public String getProtocol() {
+ return protocol;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public String getStage() {
+ return stage;
+ }
+
+ public String getResource() {
+ return resource;
+ }
+
+ public SdkHttpMethod getMethod() {
+ return method;
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public Map getHeaders() {
+ return headers;
+ }
+
+ public URL toUrl() {
+ try {
+ return new URL(protocol, host, "/" + stage + "/" + resource);
+ } catch (MalformedURLException mue) {
+ LOGGER.error("URL parse error {}", mue.getMessage());
+ throw new RuntimeException(mue);
+ }
+ }
+
+ public static final class Builder {
+
+ private String protocol = "https";
+ private String host;
+ private String stage;
+ private String resource;
+ private SdkHttpMethod method;
+ private String body;
+ private Map headers;
+
+ private Builder() {
+ }
+
+ public HttpRequest.Builder protocol(String protocol) {
+ if (protocol == null || protocol.isBlank()) {
+ throw new IllegalArgumentException("protocol can't be blank");
+ }
+ this.protocol = protocol;
+ return this;
+ }
+
+ public HttpRequest.Builder host(String host) {
+ this.host = host;
+ return this;
+ }
+
+ public HttpRequest.Builder stage(String stage) {
+ this.stage = stage;
+ return this;
+ }
+
+ public HttpRequest.Builder resource(String resource) {
+ if (resource != null && resource.startsWith("/")) {
+ this.resource = resource.substring(1);
+ } else {
+ this.resource = resource;
+ }
+ return this;
+ }
+
+ public HttpRequest.Builder method(String method) {
+ this.method = SdkHttpMethod.fromValue(method);
+ return this;
+ }
+
+ public HttpRequest.Builder body(String body) {
+ this.body = body;
+ return this;
+ }
+
+ public HttpRequest.Builder headers(final Map headers) {
+ if (headers != null) {
+ this.headers = Collections.unmodifiableMap(headers);
+ }
+ return this;
+ }
+
+ public HttpRequest build() {
+ return new HttpRequest(this);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/layers/saas-boost-api-client-helper/java/src/main/resources/checkstyle/checkstyle.xml b/layers/saas-boost-api-client-helper/java/src/main/resources/checkstyle/checkstyle.xml
new file mode 100644
index 00000000..197b52a3
--- /dev/null
+++ b/layers/saas-boost-api-client-helper/java/src/main/resources/checkstyle/checkstyle.xml
@@ -0,0 +1,319 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/functions/workload-deploy/src/main/resources/lambda-assembly.xml b/layers/saas-boost-api-client-helper/java/src/main/resources/lambda-layer-assembly.xml
similarity index 69%
rename from functions/workload-deploy/src/main/resources/lambda-assembly.xml
rename to layers/saas-boost-api-client-helper/java/src/main/resources/lambda-layer-assembly.xml
index 26364854..2daf79c4 100644
--- a/functions/workload-deploy/src/main/resources/lambda-assembly.xml
+++ b/layers/saas-boost-api-client-helper/java/src/main/resources/lambda-layer-assembly.xml
@@ -21,22 +21,11 @@ limitations under the License.
zipfalse
-
-
-
- ${project.build.outputDirectory}
-
- com/amazon/aws/partners/saasfactory/**
- log4j2.xml
- git.properties
-
-
-
- false
+ java/lib
+ truetrue
- lib
-
\ No newline at end of file
+
diff --git a/layers/saas-boost-api-client-helper/python/SaaSBoostApiHelper.py b/layers/saas-boost-api-client-helper/python/SaaSBoostApiHelper.py
new file mode 100644
index 00000000..5ae43843
--- /dev/null
+++ b/layers/saas-boost-api-client-helper/python/SaaSBoostApiHelper.py
@@ -0,0 +1,107 @@
+import boto3
+import base64
+import json
+import logging
+from botocore.exceptions import ClientError
+from datetime import datetime, timedelta
+from urllib.parse import urlencode
+from urllib.request import Request, urlopen
+#from urllib.request import HTTPHandler, HTTPSHandler, build_opener, install_opener
+
+logger = logging.getLogger()
+
+class SaaSBoostApiHelper:
+
+ __credentials_cache = {}
+
+ def __init__(self, app_client_secret_arn):
+ # Get the OAuth application client details from the SaaS Boost control
+ # plane Secrets Manager entry
+ self.secrets = boto3.client('secretsmanager')
+ try:
+ api_secret = self.secrets.get_secret_value(SecretId=app_client_secret_arn)
+ client_details = json.loads(api_secret['SecretString'])
+ self.client_name = client_details['client_name']
+ self.client_id = client_details['client_id']
+ self.client_secret = client_details['client_secret']
+ self.token_endpoint = client_details['token_endpoint']
+ self.api_endpoint = client_details['api_endpoint']
+ logger.debug("Fetched API client details from Secrets Manager")
+ except ClientError as secrets_manager_error:
+ logger.error("Error fetching API client secret from SaaS Boost control plane")
+ logger.error(str(secrets_manager_error))
+ raise
+
+ def authorized_request(self, method, resource, body=None):
+ if not resource.startswith('/'):
+ resource = '/' + resource
+ api_request = Request(
+ url=self.api_endpoint + resource,
+ method=method,
+ data=body.encode() if body else None
+ )
+ api_request.add_header('Authorization', self.__bearer_token())
+ api_request.add_header('Content-Type', 'application/json')
+
+ #http_handler = HTTPHandler(debuglevel=1)
+ #https_handler = HTTPSHandler(debuglevel=1)
+ #opener = build_opener(http_handler, https_handler)
+ #install_opener(opener)
+
+ with urlopen(api_request) as api_response:
+ response_data = api_response.read()
+ if response_data:
+ return json.loads(response_data.decode())
+ else:
+ return
+
+ def __get_cached_credentials(self):
+ cached = self.__credentials_cache.get(self.client_id)
+ if cached:
+ exp = datetime.fromtimestamp(cached['expiry'])
+ time_buffer = 2
+ if datetime.now() + timedelta(seconds=time_buffer) < exp:
+ logger.debug(f"Client credentials cache hit {self.client_id}")
+ return cached
+ else:
+ logger.debug(f"Cached credentials expiring in < {time_buffer}s {self.client_id}")
+ else:
+ logger.debug(f"Client credentials cache miss {self.client_id}")
+ return None
+
+ def __put_cached_credentials(self, grant):
+ self.__credentials_cache[self.client_id] = {
+ 'expiry': (datetime.now() + timedelta(seconds=grant['expires_in'])).timestamp(),
+ 'access_token': grant['access_token']
+ }
+
+ def __bearer_token(self):
+ token = self.__get_cached_credentials()
+ if not token:
+ token = self.__client_credentials_grant()
+ self.__put_cached_credentials(token)
+ return f"Bearer {token['access_token']}"
+
+ def __client_credentials(self):
+ # Generate a Base64 encoded string of the client credential
+ return base64.b64encode(f"{self.client_id}:{self.client_secret}".encode()).decode()
+
+ def __client_credentials_grant(self):
+ # Generate the encoded client secret string
+ client_secret = self.__client_credentials()
+
+ # POST to the token endpoint a client_credentials grant
+ token_request = Request(
+ url=self.token_endpoint,
+ data=urlencode({"grant_type": "client_credentials"}).encode(),
+ method='POST'
+ )
+ token_request.add_header('Authorization', 'Basic ' + client_secret)
+ token_request.add_header('Content-Type', 'application/x-www-form-urlencoded')
+
+ with urlopen(token_request) as token_response:
+ # {'expires_in': seconds, 'token_type': 'Bearer', 'access_token': jwt}
+ grant = json.loads(token_response.read().decode())
+ logger.debug(grant)
+ return grant
+
diff --git a/layers/saas-boost-api-client-helper/update.sh b/layers/saas-boost-api-client-helper/update.sh
new file mode 100755
index 00000000..dedc7384
--- /dev/null
+++ b/layers/saas-boost-api-client-helper/update.sh
@@ -0,0 +1,82 @@
+#!/bin/bash
+# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License").
+# You may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+if [ -z $1 ]; then
+ echo "Usage: $0 [Lambda Folder]"
+ exit 2
+fi
+
+MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}')
+echo "AWS Region = $MY_AWS_REGION"
+
+ENVIRONMENT=$1
+LAMBDA_STAGE_FOLDER=$2
+if [ -z $LAMBDA_STAGE_FOLDER ]; then
+ LAMBDA_STAGE_FOLDER="lambdas"
+fi
+LAMBDA_CODE=SaaSBoostApiClientHelper-lambda.zip
+LAYER_NAME="sb-${ENVIRONMENT}-api-client-helper"
+
+#set this for V2 AWS CLI to disable paging
+export AWS_PAGER=""
+
+SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text)
+echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET"
+if [ -z $SAAS_BOOST_BUCKET ]; then
+ echo "Can't find SAAS_BOOST_BUCKET in Parameter Store"
+ exit 1
+fi
+
+# Do a fresh build of the project
+sh build.sh
+if [ $? -ne 0 ]; then
+ echo "Error building project"
+ exit 1
+fi
+
+# And copy it up to S3
+aws s3 cp build/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/
+
+# Publish a new version of the layer
+PUBLISHED_LAYER=$(aws --region $MY_AWS_REGION lambda publish-layer-version --layer-name "${LAYER_NAME}" --compatible-runtimes java21 java17 java11 python3.12 python3.11 python3.10 python3.9 python3.8 --content S3Bucket="${SAAS_BOOST_BUCKET}",S3Key="${LAMBDA_STAGE_FOLDER}/${LAMBDA_CODE}")
+
+# Use eval to deal with the backticks in the filter expression
+eval LAYER_VERSION_ARN=\$\("aws lambda list-layers --query 'Layers[?LayerName==\`${LAYER_NAME}\`].LatestMatchingVersion.LayerVersionArn' --output text"\)
+echo "Published new layer = $LAYER_VERSION_ARN"
+
+# Find all the functions for this SaaS Boost environment that have layers
+eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-\`)] | [?Layers != null] | [].FunctionName' --output text"\)
+FUNCTIONS=($FUNCTIONS)
+#echo "Updating ${#FUNCTIONS[@]} functions with new layer version"
+
+for FX in ${FUNCTIONS[@]}; do
+ # The order of the function's layers must be maintained. Iterate through this function's layers
+ # and update this layer's ARN with the newly published version.
+ FOUND=0
+ LAYERS=""
+ for LAYER_ARN in $(aws --region $MY_AWS_REGION lambda get-function --function-name $FX --query 'Configuration.Layers[].Arn' --output text); do
+ if [[ $LAYER_ARN == *"${LAYER_NAME}"* ]]; then
+ LAYER_ARN=$LAYER_VERSION_ARN
+ FOUND=1
+ fi
+ if [ ${#LAYERS} -gt 0 ]; then
+ LAYERS="${LAYERS} "
+ fi
+ LAYERS="${LAYERS}${LAYER_ARN}"
+ done
+ if (( $FOUND )); then
+ eval "aws --region $MY_AWS_REGION lambda update-function-configuration --function-name $FX --layers $LAYERS"
+ fi
+done
diff --git a/layers/utils/pom.xml b/layers/utils/pom.xml
index b7bd4189..275862b9 100644
--- a/layers/utils/pom.xml
+++ b/layers/utils/pom.xml
@@ -33,7 +33,9 @@ limitations under the License.
+ ${project.basedir}/../..4
+ 2.14.2
@@ -58,27 +60,27 @@ limitations under the License.
com.fasterxml.jackson.corejackson-core
- 2.13.3
+ ${jackson.version}com.fasterxml.jackson.corejackson-annotations
- 2.13.3
+ ${jackson.version}com.fasterxml.jackson.corejackson-databind
- 2.13.4.1
+ ${jackson.version}com.fasterxml.jackson.datatypejackson-datatype-jsr310
- 2.13.3
+ ${jackson.version}com.fasterxml.jackson.datatypejackson-datatype-joda
- 2.13.3
+ ${jackson.version}software.amazon.awssdk
@@ -96,5 +98,15 @@ limitations under the License.
${aws.java.sdk.version}provided
+
+ com.amazonaws
+ aws-lambda-java-core
+ provided
+
+
+ com.amazonaws
+ aws-lambda-java-events
+ provided
+
diff --git a/layers/utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Utils.java b/layers/utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Utils.java
index 3b09053e..e2da6d5a 100644
--- a/layers/utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Utils.java
+++ b/layers/utils/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/Utils.java
@@ -16,15 +16,20 @@
package com.amazon.aws.partners.saasfactory.saasboost;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.core.io.JsonStringEncoder;
import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder;
import software.amazon.awssdk.awscore.client.builder.AwsSyncClientBuilder;
@@ -34,6 +39,7 @@
import software.amazon.awssdk.core.retry.RetryPolicy;
import software.amazon.awssdk.core.retry.backoff.BackoffStrategy;
import software.amazon.awssdk.core.retry.conditions.RetryCondition;
+import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.regions.partitionmetadata.AwsCnPartitionMetadata;
@@ -68,12 +74,15 @@ public class Utils {
MAPPER.setDateFormat(JAVASCRIPT_ISO8601);
MAPPER.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+ MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
MAPPER.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
MAPPER.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING);
}
- static final char[] LOWERCASE_LETTERS = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
- static final char[] UPPERCASE_LETTERS = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
+ static final char[] LOWERCASE_LETTERS = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+ 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
+ static final char[] UPPERCASE_LETTERS = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+ 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
static final char[] NUMBERS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
static final char[] SYMBOLS = {'!', '#', '$', '%', '&', '*', '+', '-', '.', ':', '=', '?', '^', '_'};
@@ -95,7 +104,8 @@ public static String unescapeJson(String quotedJson) {
char escapedCharacter = quotedJson.charAt(index);
index++;
- if (escapedCharacter == '"' || escapedCharacter == '\\' || escapedCharacter == '/' || escapedCharacter == '\'') {
+ if (escapedCharacter == '"' || escapedCharacter == '\\' || escapedCharacter == '/'
+ || escapedCharacter == '\'') {
// If the character after the backslash is another slash or a quote
// then add it to the JSON string we're building. Normal use case is
// that the next character should be a double quote mark.
@@ -173,6 +183,10 @@ public static T fromJson(InputStream json, Class serializeTo) {
return object;
}
+ public static Map asMap(Object obj) {
+ return fromJson(toJson(obj), LinkedHashMap.class);
+ }
+
public static boolean isChinaRegion(String region) {
return isChinaRegion(Region.of(region));
}
@@ -197,7 +211,19 @@ public static String endpointSuffix(Region region) {
return region.metadata().partition().dnsSuffix();
}
- public static & AwsClientBuilder, C>, C> C sdkClient(AwsSyncClientBuilder builder, String service) {
+ public static & AwsClientBuilder, C>, C> C sdkClient(
+ AwsSyncClientBuilder builder, String service) {
+ return sdkClient(builder, service, UrlConnectionHttpClient.builder());
+ }
+
+ public static & AwsClientBuilder, C>, C> C sdkClient(
+ AwsSyncClientBuilder builder, String service, SdkHttpClient.Builder httpClientBuilder) {
+ return sdkClient(builder, service, httpClientBuilder, EnvironmentVariableCredentialsProvider.create());
+ }
+
+ public static & AwsClientBuilder, C>, C> C sdkClient(
+ AwsSyncClientBuilder builder, String service, SdkHttpClient.Builder httpClientBuilder,
+ AwsCredentialsProvider credentialsProvider) {
if (Utils.isBlank(System.getenv("AWS_REGION"))) {
throw new IllegalStateException("Missing required environment variable AWS_REGION");
}
@@ -231,12 +257,11 @@ public static & AwsClientBuilder, C>, C>
region = Region.AWS_GLOBAL;
endpoint = "https://iam.amazonaws.com";
}
-
}
C client = builder
- .httpClientBuilder(UrlConnectionHttpClient.builder())
- .credentialsProvider(EnvironmentVariableCredentialsProvider.create())
+ .httpClientBuilder(httpClientBuilder)
+ .credentialsProvider(credentialsProvider)
.region(region)
.endpointOverride(URI.create(endpoint))
.overrideConfiguration(ClientOverrideConfiguration.builder()
@@ -398,7 +423,37 @@ public static String randomString(int length, String allowedCharactersRegex) {
return String.valueOf(randomCharacters);
}
- public static String getFullStackTrace(Exception e) {
+ public static String generatePassword(int passwordLength) {
+ if (passwordLength < 8) {
+ throw new IllegalArgumentException("Invalid password length. Minimum of 8 characters is required.");
+ }
+
+ // Split the classes of characters into separate buckets so we can be sure to use
+ // the correct amount of each type
+ final char[][] chars = {UPPERCASE_LETTERS, LOWERCASE_LETTERS, NUMBERS, SYMBOLS};
+ Random random = new Random();
+ StringBuilder password = new StringBuilder(passwordLength);
+
+ // Randomly select one character from each of the required character types
+ ArrayList reqCharBucket = new ArrayList<>(3);
+ reqCharBucket.add(0, 0);
+ reqCharBucket.add(1, 1);
+ reqCharBucket.add(2, 2);
+ reqCharBucket.add(3, 3);
+ while (!reqCharBucket.isEmpty()) {
+ Integer ranReqCharBucket = reqCharBucket.remove(random.nextInt(reqCharBucket.size()));
+ password.append(chars[ranReqCharBucket][random.nextInt(chars[ranReqCharBucket].length)]);
+ }
+
+ // Fill out the rest of the password with randomly selected characters
+ for (int i = 0; i < passwordLength - reqCharBucket.size(); i++) {
+ int charBucket = random.nextInt(chars.length);
+ password.append(chars[charBucket][random.nextInt(chars[charBucket].length)]);
+ }
+ return password.toString();
+ }
+
+ public static String getFullStackTrace(Throwable e) {
final StringWriter sw = new StringWriter();
final PrintWriter pw = new PrintWriter(sw, true);
e.printStackTrace(pw);
@@ -409,24 +464,64 @@ public static void logRequestEvent(Map event) {
LOGGER.info(toJson(event));
}
+ public static void logRequestEvent(APIGatewayProxyRequestEvent event) {
+ LOGGER.info(toJson(event));
+ }
+
+ public static boolean warmup(APIGatewayProxyRequestEvent event) {
+ Map queryParams = event.getQueryStringParameters();
+ // Before parsing the request body, look to see if the request was
+ // ?source=warmup
+ if (queryParams != null && "warmup".equals(queryParams.get("source"))) {
+ return true;
+ }
+ if (isNotEmpty(event.getBody())) {
+ try {
+ // Don't know if request body is an array or an object
+ // Ignore arrays. We're looking for {"source": "warmup"}
+ JsonNode json = MAPPER.readTree(event.getBody());
+ if (json.isObject()) {
+ Map body = MAPPER.treeToValue(json, LinkedHashMap.class);
+ if (body.containsKey("source") && "warmup".equals(body.get("source"))) {
+ return true;
+ }
+ }
+ } catch (JsonProcessingException jpe) {
+ // swallow
+ }
+ }
+ return false;
+ }
+
public static boolean warmup(Map event) {
- boolean warmup = false;
+ if ("warmup".equals(event.get("source"))) {
+ // Lambda invocation _not_ through API Gateway
+ return true;
+ }
if (event.containsKey("queryStringParameters")) {
+ // Before parsing the request body, look to see if the request was
+ // ?source=warmup
Map queryParams = (Map) event.get("queryStringParameters");
if (queryParams != null && "warmup".equals(queryParams.get("source"))) {
- warmup = true;
+ return true;
}
- } else if (event.containsKey("body")) {
- Map body = Utils.fromJson((String) event.get("body"), HashMap.class);
- if (body != null && body.containsKey("source") && "warmup".equals(body.get("source"))) {
- warmup = true;
- }
- } else {
- if ("warmup".equals(event.get("source"))) {
- warmup = true;
+ }
+ if (event.containsKey("body") && isNotEmpty((String) event.get("body"))) {
+ try {
+ // Don't know if request body is an array or an object
+ // Ignore arrays. We're looking for {"source": "warmup"}
+ JsonNode json = MAPPER.readTree((String) event.get("body"));
+ if (json.isObject()) {
+ Map body = MAPPER.treeToValue(json, LinkedHashMap.class);
+ if (body.containsKey("source") && "warmup".equals(body.get("source"))) {
+ return true;
+ }
+ }
+ } catch (JsonProcessingException jpe) {
+ // swallow
}
}
- return warmup;
+ return false;
}
public static String version(Class> clazz) {
diff --git a/layers/utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/GitVersionInfoTest.java b/layers/utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/GitVersionInfoTest.java
index 46bb2a34..6a08f6be 100644
--- a/layers/utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/GitVersionInfoTest.java
+++ b/layers/utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/GitVersionInfoTest.java
@@ -16,13 +16,12 @@
package com.amazon.aws.partners.saasfactory.saasboost;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
import java.util.Properties;
-import org.junit.Before;
-import org.junit.Test;
+import static org.junit.jupiter.api.Assertions.*;
public final class GitVersionInfoTest {
@@ -37,7 +36,7 @@ public final class GitVersionInfoTest {
private Properties properties;
- @Before
+ @BeforeEach
public void setup() {
properties = new Properties();
properties.setProperty(GitVersionInfo.TAG_NAME_PROPERTY, VALID_TAG);
@@ -45,15 +44,19 @@ public void setup() {
properties.setProperty(GitVersionInfo.DESCRIPTION_PROPERTY, VALID_DESC);
}
- @Test(expected = IllegalArgumentException.class)
+ @Test
public void testFromProperties_empty() {
properties.clear();
- GitVersionInfo.fromProperties(properties);
+ assertThrows(IllegalArgumentException.class, () -> {
+ GitVersionInfo.fromProperties(properties);
+ });
}
- @Test(expected = NullPointerException.class)
+ @Test
public void testFromProperties_null() {
- GitVersionInfo.fromProperties(null);
+ assertThrows(NullPointerException.class, () -> {
+ GitVersionInfo.fromProperties(null);
+ });
}
@Test
diff --git a/layers/utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/UtilsTest.java b/layers/utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/UtilsTest.java
index 649e2acc..559a349e 100644
--- a/layers/utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/UtilsTest.java
+++ b/layers/utils/src/test/java/com/amazon/aws/partners/saasfactory/saasboost/UtilsTest.java
@@ -16,18 +16,23 @@
package com.amazon.aws.partners.saasfactory.saasboost;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
-import static org.junit.Assert.*;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
public class UtilsTest {
@Test
public void testIsChinaRegion() {
- assertFalse("N. Virginia is not in China", Utils.isChinaRegion("us-east-1"));
- assertFalse("US Gov Cloud is not in China", Utils.isChinaRegion("us-gov-west-1"));
- assertTrue("Beijing is in China", Utils.isChinaRegion("cn-north-1"));
- assertTrue("Ningxia is in China", Utils.isChinaRegion("cn-northwest-1"));
+ assertFalse(Utils.isChinaRegion("us-east-1"), "N. Virginia is not in China");
+ assertFalse(Utils.isChinaRegion("us-gov-west-1"), "US Gov Cloud is not in China");
+ assertTrue(Utils.isChinaRegion("cn-north-1"), "Beijing is in China");
+ assertTrue(Utils.isChinaRegion("cn-northwest-1"), "Ningxia is in China");
}
@Test
@@ -41,8 +46,9 @@ public void testRandomString() {
String randomString = Utils.randomString(1000);
for (int ch = 0; ch < randomString.length(); ch++) {
String character = String.valueOf(randomString.charAt(ch));
- assertEquals("Character " + character + " is illegal", -1, illegalCharacters.indexOf(character));
- assertTrue("Character " + character + " is legal", legalCharacters.contains(character));
+ assertEquals(-1, illegalCharacters.indexOf(character),
+ "Character " + character + " is illegal");
+ assertTrue(legalCharacters.contains(character), "Character " + character + " is legal");
}
}
@@ -62,4 +68,35 @@ public void testToSnakeCase() {
assertEquals("foo_bar_baz", Utils.toSnakeCase("fooBarBaz"));
assertEquals("foo_bar_baz", Utils.toSnakeCase("fooBarBAZ"));
}
+
+ @Test
+ public void testCollectionFromJson() {
+ String json = "[{\"foo\": \"Santa\", \"bar\": \"Claus\"}]";
+ MyPojo[] pojoArray = Utils.fromJson(json, MyPojo[].class);
+ List pojoList = Arrays.asList(pojoArray);
+ }
+
+ public static class MyPojo {
+ private String foo;
+ private String bar;
+
+ public MyPojo() {
+ }
+
+ public String getFoo() {
+ return foo;
+ }
+
+ public void setFoo(String foo) {
+ this.foo = foo;
+ }
+
+ public String getBar() {
+ return bar;
+ }
+
+ public void setBar(String bar) {
+ this.bar = bar;
+ }
+ }
}
diff --git a/layers/utils/update.sh b/layers/utils/update.sh
index f60cceeb..b54e7eab 100755
--- a/layers/utils/update.sh
+++ b/layers/utils/update.sh
@@ -62,7 +62,7 @@ eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'F
# In case we have multiple environments in the same account/region, this could potentially override the Utils implementation
# when one environment is updated from underneath another. This shouldn't be an issue unless the Utils upgrade includes a
# change to the isBlank, isEmpty, logRequestEvent, or Utils.toJson functions.
-FUNCTIONS=($FUNCTIONS "saas-boost-app-services-macro")
+#FUNCTIONS=($FUNCTIONS "saas-boost-app-services-macro")
#echo "Updating ${#FUNCTIONS[@]} functions with new layer version"
for FX in ${FUNCTIONS[@]}; do
@@ -83,4 +83,4 @@ for FX in ${FUNCTIONS[@]}; do
if (( $FOUND )); then
eval "aws --region $MY_AWS_REGION lambda update-function-configuration --function-name $FX --layers $LAYERS"
fi
-done
\ No newline at end of file
+done
diff --git a/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/aggregation/StripeBillingPublish.java b/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/aggregation/StripeBillingPublish.java
index 90ac2ab6..db21c090 100644
--- a/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/aggregation/StripeBillingPublish.java
+++ b/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/aggregation/StripeBillingPublish.java
@@ -15,10 +15,10 @@
*/
package com.amazon.aws.partners.saasfactory.metering.aggregation;
-
import com.amazon.aws.partners.saasfactory.metering.common.AggregationEntry;
import com.amazon.aws.partners.saasfactory.metering.common.BillingUtils;
import com.amazon.aws.partners.saasfactory.metering.common.TenantConfiguration;
+import com.amazon.aws.partners.saasfactory.saasboost.ApiGatewayHelper;
import com.amazon.aws.partners.saasfactory.saasboost.Utils;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
@@ -36,10 +36,7 @@
import java.io.OutputStream;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
import static com.amazon.aws.partners.saasfactory.metering.common.Constants.*;
@@ -47,32 +44,22 @@ public class StripeBillingPublish implements RequestStreamHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(StripeBillingPublish.class);
private final static String TABLE_NAME = System.getenv(TABLE_ENV_VARIABLE);
- private static final String API_GATEWAY_HOST = System.getenv("API_GATEWAY_HOST");
- private static final String API_GATEWAY_STAGE = System.getenv("API_GATEWAY_STAGE");
- private static final String API_TRUST_ROLE = System.getenv("API_TRUST_ROLE");
+ private static final String API_APP_CLIENT = System.getenv("API_APP_CLIENT");
private final DynamoDbClient ddb;
public StripeBillingPublish() {
- long startTimeMillis = System.currentTimeMillis();
if (Utils.isBlank(TABLE_NAME)) {
throw new IllegalStateException("Missing required environment variable " + TABLE_ENV_VARIABLE);
}
- if (Utils.isBlank(API_GATEWAY_HOST)) {
- throw new IllegalStateException("Missing required environment variable API_GATEWAY_HOST");
- }
- if (Utils.isBlank(API_GATEWAY_STAGE)) {
- throw new IllegalStateException("Missing required environment variable API_GATEWAY_STAGE");
- }
- if (Utils.isBlank(API_TRUST_ROLE)) {
- throw new IllegalStateException("Missing required environment variable API_TRUST_ROLE");
+ if (Utils.isBlank(API_APP_CLIENT)) {
+ throw new IllegalStateException("Missing required environment variable API_APP_CLIENT");
}
// Used by TenantConfiguration
if (Utils.isBlank(System.getenv("DYNAMODB_CONFIG_INDEX_NAME"))) {
throw new IllegalStateException("Missing required environment variable DYNAMODB_CONFIG_INDEX_NAME");
}
LOGGER.info("Version Info: " + Utils.version(this.getClass()));
- ddb = Utils.sdkClient(DynamoDbClient.builder(), DynamoDbClient.SERVICE_NAME);
- LOGGER.info("Constructor init: {}", System.currentTimeMillis() - startTimeMillis);
+ this.ddb = Utils.sdkClient(DynamoDbClient.builder(), DynamoDbClient.SERVICE_NAME);
}
private List getAggregationEntries(String tenantID) {
@@ -224,7 +211,8 @@ private void markAggregationRecordAsSubmitted(AggregationEntry aggregationEntry)
@Override
public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) {
- Stripe.apiKey = BillingUtils.getBillingApiKey(API_GATEWAY_HOST, API_GATEWAY_STAGE, API_TRUST_ROLE);
+ ApiGatewayHelper api = ApiGatewayHelper.clientCredentialsHelper(API_APP_CLIENT);
+ Stripe.apiKey = BillingUtils.getBillingApiKey(api);
LOGGER.info("Fetching tenant IDs in table {}", TABLE_NAME);
List tenantConfigurations = TenantConfiguration.getTenantConfigurations(TABLE_NAME, ddb, LOGGER);
if (tenantConfigurations == null || tenantConfigurations.isEmpty()) {
diff --git a/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/common/BillingUtils.java b/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/common/BillingUtils.java
index ad515a16..86a8149c 100644
--- a/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/common/BillingUtils.java
+++ b/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/common/BillingUtils.java
@@ -17,11 +17,9 @@
package com.amazon.aws.partners.saasfactory.metering.common;
import com.amazon.aws.partners.saasfactory.saasboost.ApiGatewayHelper;
-import com.amazon.aws.partners.saasfactory.saasboost.ApiRequest;
import com.amazon.aws.partners.saasfactory.saasboost.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import software.amazon.awssdk.http.SdkHttpFullRequest;
import java.util.HashMap;
import java.util.Map;
@@ -31,18 +29,11 @@ public final class BillingUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(BillingUtils.class);
- public static String getBillingApiKey(String apiGatewayHost, String apiGatewayStage, String apiGatewayRole) {
+ public static String getBillingApiKey(ApiGatewayHelper api) {
//invoke SaaS Boost private API to get API Key for Billing
String apiKey = null;
- ApiRequest billingApiKeySecret = ApiRequest.builder()
- .resource("settings/BILLING_API_KEY/secret")
- .method("GET")
- .build();
- SdkHttpFullRequest apiRequest = ApiGatewayHelper.getApiRequest(
- apiGatewayHost, apiGatewayStage, billingApiKeySecret);
try {
- String responseBody = ApiGatewayHelper.signAndExecuteApiRequest(
- apiRequest, apiGatewayRole, "BillingIntegration");
+ String responseBody = api.authorizedRequest("GET", "settings/BILLING_API_KEY/secret");
Map setting = Utils.fromJson(responseBody, HashMap.class);
if (null == setting) {
throw new RuntimeException("responseBody is invalid");
diff --git a/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/onboarding/BillingIntegration.java b/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/onboarding/BillingIntegration.java
index fd0dcb4a..13de8768 100644
--- a/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/onboarding/BillingIntegration.java
+++ b/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/onboarding/BillingIntegration.java
@@ -19,6 +19,7 @@
import com.amazon.aws.partners.saasfactory.metering.common.EventBridgeEvent;
import com.amazon.aws.partners.saasfactory.metering.common.MeteredProduct;
import com.amazon.aws.partners.saasfactory.metering.common.SubscriptionPlan;
+import com.amazon.aws.partners.saasfactory.saasboost.ApiGatewayHelper;
import com.amazon.aws.partners.saasfactory.saasboost.Utils;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
@@ -39,31 +40,19 @@ public class BillingIntegration implements RequestHandler event, Context co
private void provisionTenantInStripe(String tenantId, String planId) throws StripeException {
LOGGER.info("provisionTenantInStripe: Starting...");
- Stripe.apiKey = BillingUtils.getBillingApiKey(API_GATEWAY_HOST, API_GATEWAY_STAGE, API_TRUST_ROLE);
+ ApiGatewayHelper api = ApiGatewayHelper.clientCredentialsHelper(API_APP_CLIENT);
+ Stripe.apiKey = BillingUtils.getBillingApiKey(api);
if (Utils.isBlank(tenantId)) {
throw new RuntimeException("provisionTenantInStripe: No TenantID found in the event detail");
@@ -329,7 +320,8 @@ private void cancelSubscriptionInStripe(String tenantId) throws StripeException
LOGGER.info("cancelSubscriptionInStripe: Starting...");
try {
- Stripe.apiKey = BillingUtils.getBillingApiKey(API_GATEWAY_HOST, API_GATEWAY_STAGE, API_TRUST_ROLE);
+ ApiGatewayHelper api = ApiGatewayHelper.clientCredentialsHelper(API_APP_CLIENT);
+ Stripe.apiKey = BillingUtils.getBillingApiKey(api);
} catch (Exception e) {
LOGGER.error("No api key found so skipping subscription cancellation");
return;
@@ -505,5 +497,6 @@ private void putTenantProductOnboardEvent(String eventBridgeDetail) {
public Object handleRequest(EventBridgeEvent eventBridgeEvent, Context context) {
return null;
}
+
}
diff --git a/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/onboarding/SubscriptionService.java b/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/onboarding/SubscriptionService.java
index 8e137305..5f357691 100644
--- a/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/onboarding/SubscriptionService.java
+++ b/metering-billing/lambdas/src/main/java/com/amazon/aws/partners/saasfactory/metering/onboarding/SubscriptionService.java
@@ -17,6 +17,7 @@
package com.amazon.aws.partners.saasfactory.metering.onboarding;
import com.amazon.aws.partners.saasfactory.metering.common.BillingUtils;
+import com.amazon.aws.partners.saasfactory.saasboost.ApiGatewayHelper;
import com.amazon.aws.partners.saasfactory.saasboost.Utils;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
@@ -37,20 +38,12 @@
public class SubscriptionService {
private static final Map CORS = Map.of("Access-Control-Allow-Origin", "*");
private static final Logger LOGGER = LoggerFactory.getLogger(SubscriptionService.class);
- private static final String API_GATEWAY_HOST = System.getenv("API_GATEWAY_HOST");
- private static final String API_GATEWAY_STAGE = System.getenv("API_GATEWAY_STAGE");
- private static final String API_TRUST_ROLE = System.getenv("API_TRUST_ROLE");
+ private static final String API_APP_CLIENT = System.getenv("API_APP_CLIENT");
public SubscriptionService() {
LOGGER.info("Version Info: " + Utils.version(this.getClass()));
- if (Utils.isBlank(API_GATEWAY_HOST)) {
- throw new IllegalStateException("Missing required environment variable API_GATEWAY_HOST");
- }
- if (Utils.isBlank(API_GATEWAY_STAGE)) {
- throw new IllegalStateException("Missing required environment variable API_GATEWAY_STAGE");
- }
- if (Utils.isBlank(API_TRUST_ROLE)) {
- throw new IllegalStateException("Missing required environment variable API_TRUST_ROLE");
+ if (Utils.isBlank(API_APP_CLIENT)) {
+ throw new IllegalStateException("Missing required environment variable API_APP_CLIENT");
}
}
@@ -63,7 +56,8 @@ public APIGatewayProxyResponseEvent getPlans(Map event, Context
Utils.logRequestEvent(event);
APIGatewayProxyResponseEvent response;
- Stripe.apiKey = BillingUtils.getBillingApiKey(API_GATEWAY_HOST, API_GATEWAY_STAGE, API_TRUST_ROLE);
+ ApiGatewayHelper api = ApiGatewayHelper.clientCredentialsHelper(API_APP_CLIENT);
+ Stripe.apiKey = BillingUtils.getBillingApiKey(api);
if (Stripe.apiKey != null) {
try {
ArrayNode plans = JsonNodeFactory.instance.arrayNode();
@@ -108,4 +102,5 @@ public APIGatewayProxyResponseEvent getPlans(Map event, Context
return response;
}
+
}
diff --git a/pom.xml b/pom.xml
index 248786ca..8fa6267f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,44 +15,55 @@
https://github.com/awslabs/aws-saas-boost
+ layers
+ installerresources/custom-resourcesfunctions
- installer
- layers
- metering-billing
- metrics-analyticsservices
+ ${project.basedir}UTF-83.1.1
- 3.1.2
+ 3.3.03.8.1
- 11
- 4.3.0
-
- 2.22.2
+ 17
+ 4.7.3.5
+ 3.0.01.5.1
- 1.2.1
- 3.11.0
- 2.17.132
+ 1.2.2
+ 3.11.2
+ 2.20.1202.17.1
- 4.13.2
+ 5.9.33.12.4
+ 0.8.111.7.32
- junit
- junit
+ org.junit.jupiter
+ junit-jupiter-engine${junit.version}test
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit.version}
+ test
+
+
+ com.amazonaws
+ aws-lambda-java-tests
+ 1.1.1
+ test
+ org.mockitomockito-core
@@ -128,6 +139,11 @@
org.apache.logging.log4j:log4j-slf4j-impl
+ ${env.JAVA_HOME}/bin/java
+
+ us-east-1
+ test
+
@@ -148,6 +164,42 @@
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+ ${spotbugs.version}
+
+ false
+
+
+
+ spot-bugs
+ verify
+
+ check
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ ${jacoco.version}
+
+
+
+ prepare-agent
+
+
+
+ report
+ prepare-package
+
+ report
+
+
+
+
@@ -155,27 +207,14 @@
com.github.spotbugsspotbugs-maven-plugin
- ${spotbugs.version}
-
- false
-
-
-
- spot-bugs
- verify
-
- check
-
-
- org.apache.maven.pluginsmaven-checkstyle-plugin${checkstyle.version}
- UTF-8
- resources/checkstyle/checkstyle.xml
+ UTF-8
+ ${saasboost.rootdir}/resources/checkstyle/checkstyle.xmltruetruetrue
diff --git a/resources/api-docs/app.js b/resources/api-docs/app.js
new file mode 100644
index 00000000..c7d40924
--- /dev/null
+++ b/resources/api-docs/app.js
@@ -0,0 +1,31 @@
+import express from 'express';
+import serverless from 'serverless-http';
+import { serve, setup } from 'swagger-ui-express';
+import { readFile } from 'fs/promises';
+
+const swagger = JSON.parse(
+ await readFile(
+ new URL(process.env.LAMBDA_TASK_ROOT + '/swagger.json', import.meta.url)
+ )
+);
+
+let applicationPath = process.env.SWAGGER_UI_URL_PATH;
+if (!applicationPath) {
+ applicationPath = "/docs";
+}
+if (!applicationPath.startsWith('/')) {
+ applicationPath = '/' + applicationPath;
+}
+
+const app = express();
+app.use(
+ applicationPath,
+ serve,
+ setup(null, {
+ swaggerOptions: {
+ spec: swagger
+ }
+ })
+);
+
+export const handler = serverless(app);
diff --git a/resources/api-docs/buildspec_no_post_build.yaml b/resources/api-docs/buildspec_no_post_build.yaml
new file mode 100644
index 00000000..6b731dfd
--- /dev/null
+++ b/resources/api-docs/buildspec_no_post_build.yaml
@@ -0,0 +1,16 @@
+version: 0.2
+phases:
+ pre_build:
+ commands:
+ - if [ "$AWS_DEFAULT_REGION" = "cn-northwest-1" ] || [ "$AWS_DEFAULT_REGION" = "cn-north-1" ]; then npm config set registry https://registry.npm.taobao.org; fi
+ - aws s3 cp s3://$SOURCE_BUCKET/${SOURCE_BUCKET_PREFIX}client/api-docs/src.zip src.zip
+ - unzip src.zip
+ - aws s3 cp s3://$SOURCE_BUCKET/${SOURCE_BUCKET_PREFIX}client/api-docs/swagger.json ./resources/api-docs/swagger.json
+ build:
+ commands:
+ - cd ./resources/api-docs
+ - npm install
+ - cd ../../
+cache:
+ paths:
+ - $CODEBUILD_SRC_DIR/resources/api-docs/node_modules/**/*
diff --git a/resources/api-docs/package-lock.json b/resources/api-docs/package-lock.json
new file mode 100644
index 00000000..548a31e7
--- /dev/null
+++ b/resources/api-docs/package-lock.json
@@ -0,0 +1,1311 @@
+{
+ "name": "api-docs",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "serverless-http": "^2.6.1",
+ "swagger-ui-express": "^5.0.0"
+ }
+ },
+ "node_modules/@types/aws-lambda": {
+ "version": "8.10.126",
+ "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.126.tgz",
+ "integrity": "sha512-5eh4ffLdGYgGYI1Xr6W5L4IVse4RR7L2ns5OVUXA52nW5GFapIcGMcCzHAIMMOdpcQs3aGVxbvFlJNZH6IpgEQ==",
+ "optional": true
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "peer": true,
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "peer": true
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
+ "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
+ "peer": true,
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.11.0",
+ "raw-body": "2.5.1",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
+ "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
+ "peer": true,
+ "dependencies": {
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.1",
+ "set-function-length": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "peer": true,
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+ "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "peer": true
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "peer": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
+ "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
+ "peer": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.1",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "peer": true
+ },
+ "node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "peer": true
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.18.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
+ "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
+ "peer": true,
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.1",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.5.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.2.0",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.11.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.18.0",
+ "serve-static": "1.15.0",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
+ "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
+ "peer": true,
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
+ "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
+ "peer": true,
+ "dependencies": {
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "peer": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
+ "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
+ "peer": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+ "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
+ "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+ "peer": true,
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "peer": true,
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "peer": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "peer": true
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==",
+ "peer": true
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "peer": true,
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "peer": true,
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "peer": true
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
+ "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "peer": true,
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
+ "peer": true
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "peer": true,
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+ "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+ "peer": true,
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
+ "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
+ "peer": true,
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "peer": true
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "peer": true
+ },
+ "node_modules/send": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+ "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "peer": true,
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "peer": true
+ },
+ "node_modules/serve-static": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
+ "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+ "peer": true,
+ "dependencies": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.18.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/serverless-http": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/serverless-http/-/serverless-http-2.7.0.tgz",
+ "integrity": "sha512-iWq0z1X2Xkuvz6wL305uCux/SypbojHlYsB5bzmF5TqoLYsdvMNIoCsgtWjwqWoo3AR2cjw3zAmHN2+U6mF99Q==",
+ "engines": {
+ "node": ">=8.0"
+ },
+ "optionalDependencies": {
+ "@types/aws-lambda": "^8.10.56"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
+ "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
+ "peer": true,
+ "dependencies": {
+ "define-data-property": "^1.1.1",
+ "get-intrinsic": "^1.2.1",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "peer": true
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "peer": true,
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/swagger-ui-dist": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.9.3.tgz",
+ "integrity": "sha512-/OgHfO96RWXF+p/EOjEnvKNEh94qAG/VHukgmVKh5e6foX9kas1WbjvQnDDj0sSTAMr9MHRBqAWytDcQi0VOrg=="
+ },
+ "node_modules/swagger-ui-express": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz",
+ "integrity": "sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==",
+ "dependencies": {
+ "swagger-ui-dist": ">=5.0.0"
+ },
+ "engines": {
+ "node": ">= v0.10.32"
+ },
+ "peerDependencies": {
+ "express": ">=4.0.0 || >=5.0.0-beta"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "peer": true,
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "peer": true,
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ }
+ },
+ "dependencies": {
+ "@types/aws-lambda": {
+ "version": "8.10.126",
+ "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.126.tgz",
+ "integrity": "sha512-5eh4ffLdGYgGYI1Xr6W5L4IVse4RR7L2ns5OVUXA52nW5GFapIcGMcCzHAIMMOdpcQs3aGVxbvFlJNZH6IpgEQ==",
+ "optional": true
+ },
+ "accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "peer": true,
+ "requires": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ }
+ },
+ "array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "peer": true
+ },
+ "body-parser": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
+ "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
+ "peer": true,
+ "requires": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.11.0",
+ "raw-body": "2.5.1",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ }
+ },
+ "bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "peer": true
+ },
+ "call-bind": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
+ "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
+ "peer": true,
+ "requires": {
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.1",
+ "set-function-length": "^1.1.1"
+ }
+ },
+ "content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "peer": true,
+ "requires": {
+ "safe-buffer": "5.2.1"
+ }
+ },
+ "content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "peer": true
+ },
+ "cookie": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+ "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+ "peer": true
+ },
+ "cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "peer": true
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "peer": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "define-data-property": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
+ "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
+ "peer": true,
+ "requires": {
+ "get-intrinsic": "^1.2.1",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.0"
+ }
+ },
+ "depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "peer": true
+ },
+ "destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "peer": true
+ },
+ "ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "peer": true
+ },
+ "encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "peer": true
+ },
+ "escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "peer": true
+ },
+ "etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "peer": true
+ },
+ "express": {
+ "version": "4.18.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
+ "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
+ "peer": true,
+ "requires": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.1",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.5.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.2.0",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.11.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.18.0",
+ "serve-static": "1.15.0",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ }
+ },
+ "finalhandler": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
+ "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
+ "peer": true,
+ "requires": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ }
+ },
+ "forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "peer": true
+ },
+ "fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "peer": true
+ },
+ "function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "peer": true
+ },
+ "get-intrinsic": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
+ "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
+ "peer": true,
+ "requires": {
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ }
+ },
+ "gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "peer": true,
+ "requires": {
+ "get-intrinsic": "^1.1.3"
+ }
+ },
+ "has-property-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
+ "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
+ "peer": true,
+ "requires": {
+ "get-intrinsic": "^1.2.2"
+ }
+ },
+ "has-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+ "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "peer": true
+ },
+ "has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "peer": true
+ },
+ "hasown": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
+ "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+ "peer": true,
+ "requires": {
+ "function-bind": "^1.1.2"
+ }
+ },
+ "http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "peer": true,
+ "requires": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ }
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "peer": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "peer": true
+ },
+ "ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "peer": true
+ },
+ "media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "peer": true
+ },
+ "merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==",
+ "peer": true
+ },
+ "methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "peer": true
+ },
+ "mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "peer": true
+ },
+ "mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "peer": true
+ },
+ "mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "peer": true,
+ "requires": {
+ "mime-db": "1.52.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "peer": true
+ },
+ "negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "peer": true
+ },
+ "object-inspect": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
+ "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
+ "peer": true
+ },
+ "on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "peer": true,
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ },
+ "parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "peer": true
+ },
+ "path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
+ "peer": true
+ },
+ "proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "peer": true,
+ "requires": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ }
+ },
+ "qs": {
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+ "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+ "peer": true,
+ "requires": {
+ "side-channel": "^1.0.4"
+ }
+ },
+ "range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "peer": true
+ },
+ "raw-body": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
+ "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
+ "peer": true,
+ "requires": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "peer": true
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "peer": true
+ },
+ "send": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+ "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "peer": true,
+ "requires": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "dependencies": {
+ "ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "peer": true
+ }
+ }
+ },
+ "serve-static": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
+ "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+ "peer": true,
+ "requires": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.18.0"
+ }
+ },
+ "serverless-http": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/serverless-http/-/serverless-http-2.7.0.tgz",
+ "integrity": "sha512-iWq0z1X2Xkuvz6wL305uCux/SypbojHlYsB5bzmF5TqoLYsdvMNIoCsgtWjwqWoo3AR2cjw3zAmHN2+U6mF99Q==",
+ "requires": {
+ "@types/aws-lambda": "^8.10.56"
+ }
+ },
+ "set-function-length": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
+ "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
+ "peer": true,
+ "requires": {
+ "define-data-property": "^1.1.1",
+ "get-intrinsic": "^1.2.1",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.0"
+ }
+ },
+ "setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "peer": true
+ },
+ "side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "peer": true,
+ "requires": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ }
+ },
+ "statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "peer": true
+ },
+ "swagger-ui-dist": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.9.3.tgz",
+ "integrity": "sha512-/OgHfO96RWXF+p/EOjEnvKNEh94qAG/VHukgmVKh5e6foX9kas1WbjvQnDDj0sSTAMr9MHRBqAWytDcQi0VOrg=="
+ },
+ "swagger-ui-express": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz",
+ "integrity": "sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==",
+ "requires": {
+ "swagger-ui-dist": ">=5.0.0"
+ }
+ },
+ "toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "peer": true
+ },
+ "type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "peer": true,
+ "requires": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ }
+ },
+ "unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "peer": true
+ },
+ "utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "peer": true
+ },
+ "vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "peer": true
+ }
+ }
+}
diff --git a/resources/api-docs/package.json b/resources/api-docs/package.json
new file mode 100644
index 00000000..87e0c1f8
--- /dev/null
+++ b/resources/api-docs/package.json
@@ -0,0 +1,7 @@
+{
+ "type": "module",
+ "dependencies": {
+ "serverless-http": "^2.6.1",
+ "swagger-ui-express": "^5.0.0"
+ }
+}
\ No newline at end of file
diff --git a/resources/api-docs/swagger.json b/resources/api-docs/swagger.json
new file mode 100644
index 00000000..f50e41da
--- /dev/null
+++ b/resources/api-docs/swagger.json
@@ -0,0 +1,20 @@
+{
+ "swagger" : "2.0",
+ "info" : {
+ "title" : "sb-env-api"
+ },
+ "basePath" : "/v1",
+ "schemes" : [ "https" ],
+ "paths" : {
+ },
+ "securityDefinitions" : {
+ "sb-workshop-api-authorizer" : {
+ "type" : "apiKey",
+ "name" : "Authorization",
+ "in" : "header",
+ "x-amazon-apigateway-authtype" : "custom"
+ }
+ },
+ "definitions" : {
+ }
+}
diff --git a/resources/custom-resources/rds-bootstrap/update.sh b/resources/api-docs/update.sh
similarity index 55%
rename from resources/custom-resources/rds-bootstrap/update.sh
rename to resources/api-docs/update.sh
index 77674b7e..b54822af 100755
--- a/resources/custom-resources/rds-bootstrap/update.sh
+++ b/resources/api-docs/update.sh
@@ -17,42 +17,45 @@ if [ -z $1 ]; then
echo "Usage: $0 [Lambda Folder]"
exit 2
fi
-
-MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}')
-echo "AWS Region = $MY_AWS_REGION"
-
ENVIRONMENT=$1
-LAMBDA_STAGE_FOLDER=$2
-if [ -z $LAMBDA_STAGE_FOLDER ]; then
- LAMBDA_STAGE_FOLDER="lambdas"
-fi
-LAMBDA_CODE=RdsBootstrap-lambda.zip
+FUNCTIONS=($2)
+LAMBDA_CODE=ApiDocs-lambda.zip
-#set this for V2 AWS CLI to disable paging
+# Disable AWS CLI paging
export AWS_PAGER=""
+MY_AWS_REGION=$(aws configure list | grep region | awk '{print $2}')
+echo "AWS Region = $MY_AWS_REGION"
+
SAAS_BOOST_BUCKET=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/SAAS_BOOST_BUCKET" --query 'Parameter.Value' --output text)
echo "SaaS Boost Bucket = $SAAS_BOOST_BUCKET"
if [ -z $SAAS_BOOST_BUCKET ]; then
echo "Can't find SAAS_BOOST_BUCKET in Parameter Store"
exit 1
fi
+LAMBDAS_FOLDER=$(aws --region $MY_AWS_REGION ssm get-parameter --name "/saas-boost/${ENVIRONMENT}/LAMBDAS_FOLDER" --query 'Parameter.Value' --output text 2>/dev/null)
+if [ -z $LAMBDAS_FOLDER ]; then
+ LAMBDAS_FOLDER="lambdas/"
+fi
+if [[ $LAMBDAS_FOLDER != */ ]]; then
+ LAMBDAS_FOLDER="$LAMBDAS_FOLDER/"
+fi
+echo "Lambdas folder = $LAMBDAS_FOLDER"
+echo "Function code = $LAMBDA_CODE"
# Do a fresh build of the project
-mvn
-if [ $? -ne 0 ]; then
- echo "Error building project"
- exit 1
-fi
+rm -rf build
+mkdir build
+npm install
+zip -r build/$LAMBDA_CODE app.js package.json package-lock.json swagger.json node_modules
# And copy it up to S3
-aws s3 cp target/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDA_STAGE_FOLDER/
-
-# Find all the functions for this microservice
-# We must list in the rds-bootstrap case since functions are created with a tenant ID suffix
-eval FUNCTIONS=\$\("aws --region $MY_AWS_REGION lambda list-functions --query 'Functions[?starts_with(FunctionName, \`sb-${ENVIRONMENT}-rds-bootstrap-\`)] | [].FunctionName' --output text"\)
-FUNCTIONS=($FUNCTIONS)
-for FX in "${FUNCTIONS[@]}"; do
- printf "Updating function code for %s\n" $FX
- aws lambda --region "$MY_AWS_REGION" update-function-code --function-name "$FX" --s3-bucket "$SAAS_BOOST_BUCKET" --s3-key $LAMBDA_STAGE_FOLDER/$LAMBDA_CODE
-done
+aws s3 cp build/$LAMBDA_CODE s3://$SAAS_BOOST_BUCKET/$LAMBDAS_FOLDER
+
+# Make sure the function exists before trying to update it
+FUNCTION="sb-${ENVIRONMENT}-api-docs"
+aws lambda --region "$MY_AWS_REGION" get-function --function-name "$FUNCTION" > /dev/null 2>&1
+if [ $? -eq 0 ]; then
+ printf "Updating function code for ${FUNCTION}\n"
+ aws lambda --region "$MY_AWS_REGION" update-function-code --function-name "$FUNCTION" --s3-bucket "$SAAS_BOOST_BUCKET" --s3-key "${LAMBDAS_FOLDER}${LAMBDA_CODE}"
+fi
diff --git a/resources/checkstyle/audit.py b/resources/checkstyle/audit.py
new file mode 100644
index 00000000..337ee351
--- /dev/null
+++ b/resources/checkstyle/audit.py
@@ -0,0 +1,105 @@
+#!/bin/env python3
+
+import logging
+import operator
+import os
+import re
+import subprocess
+import sys
+import xml.dom.minidom as minidom
+
+logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, format="%(levelname)s - %(message)s")
+
+audit_start_pattern = re.compile(r"^\[INFO\] Ignored \d+ errors, \d+ violations remaining\.$")
+audit_end_pattern = re.compile(r"^\[INFO\] You have \d+ Checkstyle violations\. The maximum number of allowed violations is \d+\.$")
+#audit_line_pattern = re.compile(r"^\[WARN\] (.*): (.*)\. \[(.*)\]$")
+audit_line_pattern = re.compile(r"^\[WARNING\] .*:\[\d+(?:,\d+)?] \(.*\) (.*?): .*$")
+max_allowed_violations = -1
+
+def checkstyle_stats():
+ maven_project_dir = sys.argv[1]
+ maven_project_dir_path = os.path.abspath(maven_project_dir)
+ pom_file = os.path.join(maven_project_dir_path, "pom.xml")
+ if not os.path.isfile(pom_file):
+ logging.error("Can't find pom.xml file in %s" % maven_project_dir_path)
+ sys.exit(1)
+
+ logging.info("Processing pom.xml file in %s" % maven_project_dir_path)
+ os.chdir(maven_project_dir_path)
+
+ pom = minidom.parse(open(pom_file))
+ pom_properties = pom.getElementsByTagName("properties")
+ if pom_properties is not None:
+ for property_tag in pom_properties:
+ checkstyle_tag = property_tag.getElementsByTagName("checkstyle.maxAllowedViolations")
+ if checkstyle_tag is not None and checkstyle_tag.length == 1:
+ global max_allowed_violations
+ max_allowed_violations = int(checkstyle_tag[0].firstChild.data)
+ break
+ else:
+ logging.error("Can't find checkstyle.maxAllowedViolations")
+ else:
+ logging.error("Can't find properties tag in pom.xml file")
+ logging.debug("max allowed violations = %d" % max_allowed_violations)
+
+ try:
+ maven = subprocess.check_output(["mvn", "checkstyle:check"], universal_newlines=True)
+ checkstyle_audit = maven.split("\n")
+ logging.debug("mvn checkstyle:check finished with %d lines of output" % len(checkstyle_audit))
+ except subprocess.CalledProcessError as e:
+ logging.error("command mvn checkstyle:check failed")
+ sys.exit(1)
+
+ audit = {}
+ start = audit_start(checkstyle_audit)
+ end = audit_end(checkstyle_audit)
+
+ for line in checkstyle_audit[start:end]:
+ regex = audit_line_pattern.match(line)
+ if regex:
+ category = regex.group(1)
+ if category in audit:
+ audit[category] += 1
+ else:
+ audit[category] = 1
+ else:
+ logging.warning("Line did not match audit pattern:\n" + line)
+
+ print_results(audit)
+
+def audit_start(lines):
+ start = 0
+ for line_no, line in enumerate(lines):
+ #if "[INFO] Starting audit..." == line:
+ if audit_start_pattern.match(line):
+ logging.debug("Found start of audit on line %d %s" % (line_no, line))
+ start = line_no + 1
+ break
+ return start
+
+def audit_end(lines):
+ end = 0
+ for line_no, line in enumerate(lines):
+ #if "Audit done." == line:
+ if audit_end_pattern.match(line):
+ logging.debug("Found end of audit on line %d %s" % (line_no, line))
+ end = line_no
+ break
+ return end
+
+def print_results(results):
+ total = sum(results.values())
+ print("Total checkstyle warnings %d. Maximum allowed violations %d." % (total, max_allowed_violations))
+ if total > max_allowed_violations:
+ print("PR sanity check will fail until you reduce CheckStyle violations")
+ elif max_allowed_violations > total:
+ print("Update pom.xml and set checkstyle.maxAllowedViolations to %d" % total)
+ data = sorted(results.items(), key=operator.itemgetter(1), reverse=True)
+ for category, count in data:
+ print("% 5.1f%% %4d %s" % (((count / total) * 100), count, category))
+
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ print("Usage: %s dir_with_pom_file" % sys.argv[0])
+ sys.exit(1)
+ checkstyle_stats()
diff --git a/resources/checkstyle/checkstyle.xml b/resources/checkstyle/checkstyle.xml
index 587308e7..197b52a3 100644
--- a/resources/checkstyle/checkstyle.xml
+++ b/resources/checkstyle/checkstyle.xml
@@ -283,7 +283,7 @@
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
-
+
diff --git a/resources/custom-resources/app-services-macro/pom.xml b/resources/custom-resources/app-services-macro/pom.xml
deleted file mode 100644
index 8203756e..00000000
--- a/resources/custom-resources/app-services-macro/pom.xml
+++ /dev/null
@@ -1,70 +0,0 @@
-
-
-
- 4.0.0
-
- com.amazon.aws.partners.saasfactory.saasboost
- saasboost-custom-resources
- 1.0.0
-
- ApplicationServicesMacro
- 1.0.0
- jar
-
-
- Apache-2.0
- http://www.apache.org/licenses/LICENSE-2.0
-
-
-
- 0
-
-
-
- ${project.artifactId}
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
-
-
- org.apache.maven.plugins
- maven-surefire-plugin
-
-
- org.apache.maven.plugins
- maven-assembly-plugin
-
-
- io.github.git-commit-id
- git-commit-id-maven-plugin
-
-
-
-
-
-
- com.amazon.aws.partners.saasfactory.saasboost
- Utils
- 1.0.0
-
- provided
-
-
-
-
diff --git a/resources/custom-resources/app-services-macro/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApplicationServicesMacro.java b/resources/custom-resources/app-services-macro/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApplicationServicesMacro.java
deleted file mode 100644
index 5cdd36f6..00000000
--- a/resources/custom-resources/app-services-macro/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/ApplicationServicesMacro.java
+++ /dev/null
@@ -1,348 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.amazon.aws.partners.saasfactory.saasboost;
-
-import com.amazonaws.services.lambda.runtime.Context;
-import com.amazonaws.services.lambda.runtime.RequestHandler;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.*;
-
-public class ApplicationServicesMacro implements RequestHandler, Map> {
-
- private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationServicesMacro.class);
- private static final String FRAGMENT = "fragment";
- private static final String REQUEST_ID = "requestId";
- private static final String TEMPLATE_PARAMETERS = "templateParameterValues";
- private static final String STATUS = "status";
- private static final String SUCCESS = "SUCCESS";
- private static final String FAILURE = "FAILURE";
- private static final String ERROR_MSG = "errorMessage";
-
- /**
- * CloudFormation macro to create resources based on SaaS Boost appConfig objects:
- * 1/ ECR repository resources for each application service passed as a template parameter.
- * Returns failure if the "ApplicationServices" template parameter is missing.
- * 2/ S3 bucket resource for all application services if enabled via a template parameter.
- * Assumes a missing `AppExtension` parameter means S3 support is not enabled.
- *
- * @param event Lambda event containing the CloudFormation request id, fragment, and template parameters
- * @param context Lambda execution context
- * @return CloudFormation macro response of success or failure and the modified template fragment
- */
- @Override
- @SuppressWarnings("unchecked")
- public Map handleRequest(Map event, Context context) {
- Utils.logRequestEvent(event);
-
- Map response = new HashMap<>();
- response.put(REQUEST_ID, event.get(REQUEST_ID));
- response.put(STATUS, FAILURE);
-
- Map templateParameters = (Map) event.get(TEMPLATE_PARAMETERS);
- Map template = (Map) event.get(FRAGMENT);
-
- String ecrError = updateTemplateForEcr(templateParameters, template);
- if (ecrError != null) {
- LOGGER.error("Encountered error updating template for ECR repositories: {}");
- response.put(ERROR_MSG, ecrError);
- return response;
- }
- LOGGER.info("Successfully altered template for ECR repositories");
-
- String extensionsError = updateTemplateForPooledExtensions(templateParameters, template);
- if (extensionsError != null) {
- LOGGER.error("Encountered error updating template for pooled extensions: {}");
- response.put(ERROR_MSG, extensionsError);
- return response;
- }
- LOGGER.info("Successfully altered template for extensions");
-
- response.put(FRAGMENT, template);
- response.put(STATUS, SUCCESS);
- return response;
- }
-
- protected static String updateTemplateForPooledExtensions(
- final Map templateParameters,
- Map template) {
- Set processedExtensions = new HashSet();
- if (templateParameters.containsKey("AppExtensions") && template.containsKey("Resources")) {
- String applicationExtensions = (String) templateParameters.get("AppExtensions");
- if (Utils.isNotEmpty(applicationExtensions)) {
- String[] extensions = applicationExtensions.split(",");
- for (String extension : extensions) {
- if (processedExtensions.contains(extension)) {
- LOGGER.warn("Skipping duplicate extension {}", extension);
- continue;
- }
- switch (extension) {
- case "s3": {
- String s3Error = updateTemplateForS3(templateParameters, template);
- if (s3Error != null) {
- LOGGER.error("Processing S3 extension failed: {}", s3Error);
- return s3Error;
- }
- LOGGER.info("Successfully processed s3 extension");
- break;
- }
- default: {
- LOGGER.warn("Skipping unknown extension {}", extension);
- }
- }
- processedExtensions.add(extension);
- }
- } else {
- LOGGER.debug("Empty AppExtensions parameter, skipping updating template for extensions");
- }
- } else {
- LOGGER.error("Invalid template, missing AppExtensions parameter or missing Resources");
- return "Invalid template, missing AppExtensions parameter or missing Resources";
- }
- return null;
- }
-
- protected static String updateTemplateForS3(final Map templateParameters,
- Map template) {
- if (templateParameters.containsKey("Environment")
- && templateParameters.containsKey("LoggingBucket")) {
- String environmentName = (String) templateParameters.get("Environment");
- // Bucket Resource
- Map s3Resource = s3Resource(environmentName,
- (String) templateParameters.get("LoggingBucket"));
- ((Map) template.get("Resources")).put("TenantStorage", s3Resource);
-
- // Custom Resource to clear the bucket before we delete it
- Map clearBucketResource = Map.of(
- "Type", "Custom::CustomResource",
- "Properties", Map.of(
- "ServiceToken", "{{resolve:ssm:/saas-boost/" + environmentName + "/CLEAR_BUCKET_ARN}}",
- "Bucket", Map.of("Ref", "TenantStorage")
- )
- );
- ((Map) template.get("Resources")).put("ClearTenantStorageBucket", clearBucketResource);
- } else {
- return "Invalid template, missing parameter Environment or LoggingBucket";
- }
- return null;
- }
-
- protected static Map s3Resource(String environment, String loggingBucket) {
- Map resourceProperties = new LinkedHashMap<>();
-
- // tags
- resourceProperties.put("Tags", List.of(Map.of(
- "Key", "SaaS Boost",
- "Value", environment
- )));
-
- // encryptionConfiguration
- resourceProperties.put("BucketEncryption", Map.of(
- "ServerSideEncryptionConfiguration", List.of(Map.of(
- "BucketKeyEnabled", true,
- "ServerSideEncryptionByDefault", Map.of("SSEAlgorithm", "AES256")
- ))
- ));
-
- // loggingConfiguration
- resourceProperties.put("LoggingConfiguration", Map.of(
- "DestinationBucketName", loggingBucket,
- "LogFilePrefix", "s3extension-logs"
- ));
-
- // ownershipControls
- resourceProperties.put("OwnershipControls", Map.of(
- "Rules", List.of(Map.of("ObjectOwnership", "BucketOwnerEnforced"))
- ));
-
- // publicAccessBlockConfiguration
- resourceProperties.put("PublicAccessBlockConfiguration", Map.of(
- "BlockPublicAcls", true,
- "BlockPublicPolicy", true,
- "IgnorePublicAcls", true,
- "RestrictPublicBuckets", true
- ));
-
- Map resource = new LinkedHashMap<>();
- resource.put("Type", "AWS::S3::Bucket");
- resource.put("Properties", resourceProperties);
-
- return resource;
- }
-
- protected static String updateTemplateForEcr(
- final Map templateParameters,
- Map template) {
- if (templateParameters.containsKey("ApplicationServices")) {
- if (template.containsKey("Resources")) {
- String servicesList = (String) templateParameters.get("ApplicationServices");
- if (Utils.isNotEmpty(servicesList)) {
- String[] services = servicesList.split(",");
- for (String service : services) {
- // Each application service needs its own ECR repository
- String ecrResourceName = ecrResourceName(service);
- ((Map) template.get("Resources")).put(ecrResourceName, ecrResource(service));
-
- // Define an EventBridge rule to capture image events on the repo
- String eventRuleResourceName = eventRuleResourceName(service);
- ((Map) template.get("Resources")).put(eventRuleResourceName,
- eventRuleResource(service, ecrResourceName));
-
- // And we need a Lambda permission for this rule to invoke the workload deploy function
- ((Map) template.get("Resources")).put(eventRulePermissionName(service),
- eventRulePermissionResource(eventRuleResourceName));
- }
- LOGGER.info(Utils.toJson(template));
- } else {
- LOGGER.info("Empty ApplicationServices list. Skipping template modification.");
- }
- } else {
- LOGGER.warn("CloudFormation template fragment does not have Resources");
- }
- } else {
- LOGGER.error("Invalid template, missing parameter ApplicationServices");
- return "Invalid template, missing parameter ApplicationServices";
- }
- // no error message implies success?
- return null;
- }
-
- protected static String cloudFormationResourceName(String name) {
- return name != null ? name.replaceAll("[^A-Za-z0-9]", "") : null;
- }
-
- protected static String ecrResourceName(String serviceName) {
- if (Utils.isBlank(serviceName)) {
- throw new IllegalArgumentException("service name cannot be blank");
- }
- return cloudFormationResourceName(serviceName);
- }
-
- protected static Map ecrResource(String serviceName) {
- if (Utils.isBlank(serviceName)) {
- throw new IllegalArgumentException("service name cannot be blank");
- }
-
- Map resourceProperties = new LinkedHashMap<>();
- resourceProperties.put("EncryptionConfiguration", Collections
- .singletonMap("EncryptionType", "AES256")
- );
- Map tag = new LinkedHashMap<>();
- tag.put("Key", "Name");
- tag.put("Value", serviceName.trim());
- resourceProperties.put("Tags", Collections.singletonList(tag));
-
- Map resource = new LinkedHashMap<>();
- resource.put("Type", "AWS::ECR::Repository");
- resource.put("Properties", resourceProperties);
-
- return resource;
- }
-
- protected static String eventRuleResourceName(String serviceName) {
- if (Utils.isBlank(serviceName)) {
- throw new IllegalArgumentException("service name cannot be blank");
- }
- return "ImageEventRule" + cloudFormationResourceName(serviceName);
- }
-
- /**
- * Generates an AWS::Events::Rule resource to trigger the workload deployment Lambda function when an ECR image
- * event occurs for a given repository.
- *
- *