mirror of https://github.com/boxyhq/jackson.git
Compare commits
169 Commits
Author | SHA1 | Date |
---|---|---|
dependabot[bot] | 2c80f7c920 | |
dependabot[bot] | 1cad1aeb0c | |
dependabot[bot] | 76b1d21c86 | |
Nitendra | a1d2e2ac3d | |
Deepak Prabhakara | 0048671643 | |
Nitendra | 73b1337f35 | |
Deepak Prabhakara | 654fdd4220 | |
dependabot[bot] | 55df28b0c7 | |
Deepak Prabhakara | f90da2bb8f | |
Deepak Prabhakara | 2e75bb361a | |
dependabot[bot] | bf8a79031b | |
dependabot[bot] | 26c807d56a | |
dependabot[bot] | 9fa12b471f | |
dependabot[bot] | 37fddf8f11 | |
dependabot[bot] | 0800a92f13 | |
nadilas | 7d0c31f7bc | |
dependabot[bot] | a64a5a0dbc | |
dependabot[bot] | be768bc70b | |
Nitendra | ce565c4e7e | |
Deepak Prabhakara | 7ccd7b0e48 | |
Aswin V | 9f4dddad09 | |
Deepak Prabhakara | 183e8fb1e1 | |
Deepak Prabhakara | e008af5c90 | |
dependabot[bot] | 5dfac0fc50 | |
dependabot[bot] | 69afafb8f8 | |
dependabot[bot] | cf1979d479 | |
dependabot[bot] | 00d75a861e | |
dependabot[bot] | 0fba606fe9 | |
Chirag Gupta | 937e69e5f9 | |
Deepak Prabhakara | 4330eeb530 | |
dependabot[bot] | 7ef912c0ba | |
dependabot[bot] | c8e8abaaa8 | |
dependabot[bot] | ec9e5f285a | |
dependabot[bot] | 9a2600e1ee | |
dependabot[bot] | 016b456b75 | |
dependabot[bot] | e11aa6e3fe | |
Deepak Prabhakara | c76791fabd | |
Deepak Prabhakara | 93cbf30cfc | |
Deepak Prabhakara | ac698864aa | |
Deepak Prabhakara | 99f55485fc | |
Deepak Prabhakara | 09c6777f9e | |
Aswin V | 25b29d70ef | |
dependabot[bot] | a30efd8e3c | |
dependabot[bot] | caf4b75983 | |
dependabot[bot] | 76bf07aaaf | |
dependabot[bot] | c987444b04 | |
dependabot[bot] | f4ea08cd0f | |
dependabot[bot] | d3b68813a7 | |
dependabot[bot] | 2ca666ff3e | |
dependabot[bot] | bcd5c751bd | |
dependabot[bot] | dcd291d394 | |
dependabot[bot] | 9786a9f8f4 | |
dependabot[bot] | 6163460947 | |
dependabot[bot] | 22839cd2ee | |
Deepak Prabhakara | 2d5d723549 | |
Deepak Prabhakara | 2907ce1e41 | |
Deepak Prabhakara | 10dd52cb54 | |
Deepak Prabhakara | 6f0eb99f03 | |
Deepak Prabhakara | 83a5f4de6b | |
Deepak Prabhakara | 7e8612c07b | |
dependabot[bot] | 34bf0a7c70 | |
dependabot[bot] | 643d54a2e5 | |
dependabot[bot] | 9a4bb57ce1 | |
Deepak Prabhakara | 2035dbb42e | |
dependabot[bot] | 90496f267c | |
dependabot[bot] | 02af9ac577 | |
dependabot[bot] | 02385162f8 | |
dependabot[bot] | cbd423baf4 | |
dependabot[bot] | be5b9062b2 | |
dependabot[bot] | 5799729f0a | |
Deepak Prabhakara | c703b9dfea | |
dependabot[bot] | 2c80abb235 | |
Aswin V | f4c49173ef | |
dependabot[bot] | 1e8ebd239e | |
dependabot[bot] | 469cb6db8b | |
dependabot[bot] | a8e4a22547 | |
dependabot[bot] | ead31c603f | |
dependabot[bot] | f4871e246c | |
dependabot[bot] | b7471eb6e2 | |
Deepak Prabhakara | b00fbcc58b | |
Deepak Prabhakara | 7f12921f22 | |
Deepak Prabhakara | dd29849ffb | |
Deepak Prabhakara | 5eb106e5c4 | |
Deepak Prabhakara | 3d543bc7ab | |
Deepak Prabhakara | 6bfb89a74e | |
Deepak Prabhakara | 147fbaa9ec | |
dependabot[bot] | d89cefcf87 | |
dependabot[bot] | cdf41e00d0 | |
dependabot[bot] | 37df8881ff | |
dependabot[bot] | 1302254727 | |
dependabot[bot] | 526f3d0891 | |
dependabot[bot] | 43fcf5b85f | |
Deepak Prabhakara | def9239d1a | |
Deepak Prabhakara | a86efad2e0 | |
dependabot[bot] | 69be15c1fc | |
dependabot[bot] | 216b0c83d2 | |
Deepak Prabhakara | 59a80772e0 | |
dependabot[bot] | 3a7da89593 | |
dependabot[bot] | 113ab82924 | |
dependabot[bot] | c8ba0ecac6 | |
dependabot[bot] | 0893a65d1c | |
dependabot[bot] | b4d89d42bf | |
Utkarsh Mehta | 48f9dd49b1 | |
Deepak Prabhakara | dd1755b669 | |
Deepak Prabhakara | 235db3b024 | |
Aswin V | f8e823f4b2 | |
Deepak Prabhakara | ff988bb271 | |
Deepak Prabhakara | 67d05e0ba1 | |
Aswin V | 9943a06ace | |
Deepak Prabhakara | f08025cc31 | |
Deepak Prabhakara | 63baf12a38 | |
Deepak Prabhakara | 668a1ba499 | |
Deepak Prabhakara | b6adc308f7 | |
dependabot[bot] | bbcea282f8 | |
dependabot[bot] | 143ab81eea | |
dependabot[bot] | 14e222163c | |
dependabot[bot] | 571a2d1f0f | |
dependabot[bot] | a6e993d1c8 | |
dependabot[bot] | bd2dc3d382 | |
dependabot[bot] | de83010f93 | |
dependabot[bot] | 6f14494078 | |
dependabot[bot] | 71f5a31e0b | |
dependabot[bot] | 7fd15f5b7b | |
dependabot[bot] | c0737a20bc | |
dependabot[bot] | f3efcdd2ce | |
dependabot[bot] | 252c8cf775 | |
dependabot[bot] | db63d6309f | |
dependabot[bot] | 08a70476db | |
dependabot[bot] | 9e1f149983 | |
dependabot[bot] | bedcba0fe5 | |
dependabot[bot] | 7022aa68e1 | |
dependabot[bot] | 49c611a09a | |
dependabot[bot] | a8c14dec7a | |
dependabot[bot] | 3ee670f057 | |
dependabot[bot] | 044f452945 | |
Deepak Prabhakara | 5000983a36 | |
dependabot[bot] | 886b855098 | |
Deepak Prabhakara | 64d9b38ef7 | |
Deepak Prabhakara | 8fbc1e8db7 | |
Utkarsh Mehta | b98ccc68bc | |
dependabot[bot] | 1eb2147802 | |
dependabot[bot] | d18e92a20c | |
dependabot[bot] | fcb746576f | |
dependabot[bot] | 5cb196ea1d | |
dependabot[bot] | 8d875ce14b | |
dependabot[bot] | 2b491b67ba | |
dependabot[bot] | 61d8b0c2b0 | |
dependabot[bot] | ade4466d26 | |
dependabot[bot] | f3290bb0f7 | |
Deepak Prabhakara | 12d6742dce | |
Deepak Prabhakara | cdd984c64a | |
Deepak Prabhakara | 0be094d76d | |
dependabot[bot] | 413f68e45e | |
dependabot[bot] | 6e57c6f906 | |
dependabot[bot] | fbd77b47e7 | |
dependabot[bot] | 75c5637b3f | |
dependabot[bot] | 8559abfe34 | |
dependabot[bot] | 7dd0104471 | |
dependabot[bot] | 52f5ea9b5d | |
Deepak Prabhakara | 19d5671457 | |
Deepak Prabhakara | f10a8b91f1 | |
Deepak Prabhakara | 663eca577f | |
Utkarsh Mehta | 50aa00aec2 | |
Deepak Prabhakara | b756388657 | |
Deepak Prabhakara | c28011590d | |
dependabot[bot] | ad9aaab63b | |
dependabot[bot] | d3c3c8727b | |
dependabot[bot] | df35d3684d | |
dependabot[bot] | 6836859a5a |
|
@ -110,8 +110,12 @@ jobs:
|
|||
image: 'amazon/dynamodb-local:latest'
|
||||
ports:
|
||||
- '8000:8000'
|
||||
turso:
|
||||
image: ghcr.io/tursodatabase/libsql-server:latest
|
||||
ports:
|
||||
- '8080:8080'
|
||||
mocksaml:
|
||||
image: boxyhq/mock-saml:1.2.0
|
||||
image: boxyhq/mock-saml:1.3.9
|
||||
ports:
|
||||
- 4000:4000
|
||||
env:
|
||||
|
@ -266,7 +270,7 @@ jobs:
|
|||
uses: anchore/sbom-action/publish-sbom@v0
|
||||
with:
|
||||
sbom-artifact-match: ".*\\.spdx$"
|
||||
- name: Create NPM Pacakge SBOM Report [CycloneDx]
|
||||
- name: Create NPM Package SBOM Report [CycloneDx]
|
||||
uses: anchore/sbom-action@v0
|
||||
with:
|
||||
format: cyclonedx
|
||||
|
@ -277,23 +281,23 @@ jobs:
|
|||
with:
|
||||
sbom-artifact-match: ".*\\.cyclonedx$"
|
||||
- name: Download artifact for SPDX Report
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/release-disabled'
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: npm_sbom.spdx
|
||||
- name: Download artifact for CycloneDx Report
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/release-disabled'
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: npm_sbom.cyclonedx
|
||||
- name: Remove older SBOMs
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/release-disabled'
|
||||
run: rm -rf ./npm/sbom*.* || true
|
||||
- name: Move SPDX Report
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/release-disabled'
|
||||
run: mv npm_sbom.spdx "./npm/sbom.spdx"
|
||||
- name: Move CycloneDx Report
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/release-disabled'
|
||||
run: mv npm_sbom.cyclonedx "./npm/sbom.cyclonedx"
|
||||
|
||||
- name: Next Js Project SBOM Report [SPDX]
|
||||
|
@ -317,21 +321,21 @@ jobs:
|
|||
with:
|
||||
sbom-artifact-match: ".*\\.cyclonedx$"
|
||||
- name: Remove older SBOMs
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/release-disabled'
|
||||
run: rm -rf sbom*.* || true
|
||||
- name: Download artifact for SPDX Report
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/release-disabled'
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: sbom.spdx
|
||||
- name: Download artifact for CycloneDx Report
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/release-disabled'
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: sbom.cyclonedx
|
||||
|
||||
- name: Create SBOM Report [Docker][SPDX]
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/release-disabled'
|
||||
uses: anchore/sbom-action@v0
|
||||
with:
|
||||
image: ${{ needs.ci.outputs.IMAGE_PATH }}:${{ needs.ci.outputs.PUBLISH_TAG }}
|
||||
|
@ -339,12 +343,12 @@ jobs:
|
|||
artifact-name: docker_sbom.spdx
|
||||
upload-artifact-retention: 1
|
||||
- name: Publish report [Docker][SPDX]
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/release-disabled'
|
||||
uses: anchore/sbom-action/publish-sbom@v0
|
||||
with:
|
||||
sbom-artifact-match: ".*\\.spdx$"
|
||||
- name: Create SBOM Report [Docker][CycloneDx]
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/release-disabled'
|
||||
uses: anchore/sbom-action@v0
|
||||
with:
|
||||
image: ${{ needs.ci.outputs.IMAGE_PATH }}:${{ needs.ci.outputs.PUBLISH_TAG }}
|
||||
|
@ -352,32 +356,32 @@ jobs:
|
|||
artifact-name: docker_sbom.cyclonedx
|
||||
upload-artifact-retention: 1
|
||||
- name: Publish report [Docker][CycloneDx]
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/release-disabled'
|
||||
uses: anchore/sbom-action/publish-sbom@v0
|
||||
with:
|
||||
sbom-artifact-match: ".*\\.cyclonedx$"
|
||||
- name: Download artifact for SPDX Report [Docker]
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/release-disabled'
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docker_sbom.spdx
|
||||
- name: Download artifact for CycloneDx Report [Docker]
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/release-disabled'
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docker_sbom.cyclonedx
|
||||
- name: Create/Clear folder [Docker]
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/release-disabled'
|
||||
run: mkdir -p ./_docker/ && rm -rf ./_docker/*.* || true
|
||||
|
||||
- name: Move Report & cleanup
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/release-disabled'
|
||||
run: |
|
||||
mv docker_sbom.spdx "./_docker/sbom.spdx" || true
|
||||
mv docker_sbom.cyclonedx "./_docker/sbom.cyclonedx" || true
|
||||
|
||||
- name: ORAS Setup
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/release-disabled'
|
||||
run: |
|
||||
ORAS_VERSION="v0.8.1"
|
||||
ORAS_FILENAME="oras_0.8.1_linux_amd64.tar.gz"
|
||||
|
@ -386,7 +390,7 @@ jobs:
|
|||
tar -xvf "${ORAS_FILENAME}" -C oras_install
|
||||
|
||||
- name: Push SBOM reports to GitHub Container Registry & Sign the sbom images
|
||||
if: github.ref == 'refs/heads/release'
|
||||
if: github.ref == 'refs/heads/release-disabled'
|
||||
run: |
|
||||
result=$(./oras_install/oras push ghcr.io/${{github.repository}}/sbom${{ needs.ci.outputs.IMAGE_SUFFIX }}:service-${{ needs.ci.outputs.NPM_VERSION }} ./sbom.*)
|
||||
ORAS_DIGEST=$(echo $result | grep -oE 'sha256:[a-f0-9]{64}')
|
||||
|
|
|
@ -47,4 +47,5 @@ _dev/docker/dynamodb/shared-local-instance.db
|
|||
public/terminus/sprites.png
|
||||
**/.tap/**
|
||||
|
||||
internal-ui/dist
|
||||
internal-ui/dist
|
||||
_dev/docker/libsql/iku.db
|
||||
|
|
|
@ -15,5 +15,4 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
],
|
||||
// importOrderSeparation: true,
|
||||
};
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"hooks": {},
|
||||
"hooks": {
|
||||
"after:bump": "npm run swagger-jsdoc"
|
||||
},
|
||||
"git": {
|
||||
"changelog": "git log --pretty=format:\"* %s (%h)\" ${from}...${to}",
|
||||
"requireCleanWorkingDir": true,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
ARG NODEJS_IMAGE=node:20.12.1-alpine3.19
|
||||
ARG NODEJS_IMAGE=node:20.13.1-alpine3.19
|
||||
FROM --platform=$BUILDPLATFORM $NODEJS_IMAGE AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
|
|
|
@ -51,3 +51,14 @@ services:
|
|||
volumes:
|
||||
- './docker/dynamodb:/home/dynamodblocal/data'
|
||||
working_dir: /home/dynamodblocal
|
||||
turso:
|
||||
image: ghcr.io/tursodatabase/libsql-server:latest
|
||||
platform: linux/amd64
|
||||
ports:
|
||||
- '8080:8080'
|
||||
- '5001:5001'
|
||||
# environment:
|
||||
# - SQLD_NODE=primary
|
||||
command: '/bin/sqld --enable-http-console'
|
||||
volumes:
|
||||
- ./docker/libsql:/var/lib/sqld
|
||||
|
|
|
@ -40,7 +40,7 @@ export const Sidebar = ({ isOpen, setIsOpen }: SidebarProps) => {
|
|||
href: '/admin/sso-connection',
|
||||
text: t('enterprise_sso'),
|
||||
icon: SSOLogo,
|
||||
active: asPath.includes('/admin/sso-connection') || asPath.includes('/admin/federated-saml'),
|
||||
active: asPath.includes('/admin/sso-connection'),
|
||||
items: [
|
||||
{
|
||||
href: '/admin/sso-connection',
|
||||
|
@ -54,14 +54,22 @@ export const Sidebar = ({ isOpen, setIsOpen }: SidebarProps) => {
|
|||
active: asPath.includes('/admin/sso-connection/setup-link'),
|
||||
},
|
||||
{
|
||||
href: '/admin/federated-saml',
|
||||
text: t('saml_federation'),
|
||||
active: asPath.includes('/admin/federated-saml'),
|
||||
href: '/admin/sso-traces',
|
||||
text: t('bui-traces-title'),
|
||||
active: asPath.includes('/admin/sso-traces'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
href: '/admin/identity-federation',
|
||||
text: t('identity_federation'),
|
||||
icon: SSOLogo,
|
||||
active: asPath.includes('/admin/identity-federation'),
|
||||
items: [
|
||||
{
|
||||
href: '/admin/sso-tracer',
|
||||
text: t('bui-tracer-title'),
|
||||
active: asPath.includes('/admin/sso-tracer'),
|
||||
href: '/admin/identity-federation',
|
||||
text: t('apps'),
|
||||
active: asPath.includes('/admin/identity-federation'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -3,7 +3,7 @@ import styles from './BlocklyComponent.module.css';
|
|||
import { useEffect, useRef, createRef } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import Blockly from 'blockly/core';
|
||||
import * as Blockly from 'blockly/core';
|
||||
import 'blockly/blocks';
|
||||
import { maskSetup } from '@components/terminus/blocks/customblocks';
|
||||
import locale from 'blockly/msg/en';
|
||||
|
@ -35,11 +35,11 @@ function BlocklyComponent(props) {
|
|||
|
||||
const uploadModel = async () => {
|
||||
const domToPretty = modelToXML();
|
||||
const cueModel = generateModel(primaryWorkspace.current, roles);
|
||||
const model = generateModel(primaryWorkspace.current, roles);
|
||||
|
||||
const body = {
|
||||
cue_schema: cueModel,
|
||||
blockly_schema: domToPretty,
|
||||
model: model,
|
||||
blockly_model: domToPretty,
|
||||
};
|
||||
|
||||
const requestOptions = {
|
||||
|
|
|
@ -1,45 +1,46 @@
|
|||
import Blockly from 'blockly/core';
|
||||
import * as Blockly from 'blockly/core';
|
||||
|
||||
// TODO wire with CUE configuration files
|
||||
// TODO extract prefixes
|
||||
function getEncryption() {
|
||||
return [
|
||||
['AES_256', 'crypto.#EnAES_256'],
|
||||
['FPE_FF1', 'crypto.#EnFPE_FF1'],
|
||||
['FPE_FF3_1', 'crypto.#EnFPE_FF3_1'],
|
||||
['NoEncryption', 'crypto.#EnNoEncryption'],
|
||||
// ['RSA_2048', 'crypto.#EnRSA_2048'],
|
||||
// ['Blowfish_448', 'crypto.#EnBlowfish_448'],
|
||||
// ['FPE', 'crypto.#EnFPE'],
|
||||
// ['B64', 'crypto.#EnB64'],
|
||||
['AES_256', 'AES_256'],
|
||||
['FPE_FF1', 'FPE_FF1'],
|
||||
['FPE_FF3_1', 'FPE_FF3_1'],
|
||||
['NoEncryption', 'NoEncryption'],
|
||||
// ['RSA_2048', 'RSA_2048'],
|
||||
// ['Blowfish_448', 'Blowfish_448'],
|
||||
// ['FPE', 'FPE'],
|
||||
// ['B64', 'B64'],
|
||||
];
|
||||
}
|
||||
|
||||
function getMasks() {
|
||||
return [
|
||||
['Clear', 'masking.#MClear'],
|
||||
['Generic', 'masking.#MGeneric'],
|
||||
['Redact', 'masking.#MRedact'],
|
||||
['Password', 'masking.#MPassword'],
|
||||
['Name', 'masking.#MName'],
|
||||
['Address', 'masking.#MAddress'],
|
||||
['Email', 'masking.#MEmail'],
|
||||
['Mobile', 'masking.#MMobile'],
|
||||
['Telephone', 'masking.#MTelephone'],
|
||||
['ID', 'masking.#MID'],
|
||||
['CreditCard', 'masking.#MCreditCard'],
|
||||
['Struct', 'masking.#MStruct'],
|
||||
['URL', 'masking.#MURL'],
|
||||
['Clear', 'Clear'],
|
||||
['Redact', 'Redact'],
|
||||
['Generic', 'Generic'],
|
||||
['Password', 'Password'],
|
||||
['Name', 'Name'],
|
||||
['Address', 'Address'],
|
||||
['Email', 'Email'],
|
||||
['Mobile', 'Mobile'],
|
||||
['Telephone', 'Telephone'],
|
||||
['ID', 'ID'],
|
||||
['CreditCard', 'CreditCard'],
|
||||
['Struct', 'Struct'],
|
||||
['URL', 'URL'],
|
||||
];
|
||||
}
|
||||
|
||||
function getPredefinedDataTypes() {
|
||||
return [
|
||||
['Letters', 'defs.#Letters'],
|
||||
['LettersWithSpaces', 'defs.#LettersWithSpaces'],
|
||||
['Alphanumerical', 'defs.#Alphanumerical'],
|
||||
['AlphanumericalWithSpaces', 'defs.#AlphanumericalWithSpaces'],
|
||||
['AlphanumericalNotAccented', 'defs.#AlphanumericalNotAccented'],
|
||||
['AlphanumericalNotAccentedWithSpaces', 'defs.#AlphanumericalNotAccentedWithSpaces'],
|
||||
['SimpleDate (2006-11-24)', 'defs.#SimpleDateFormat'],
|
||||
['String', 'String'],
|
||||
['Letters', 'Letters'],
|
||||
['LettersWithSpaces', 'LettersWithSpaces'],
|
||||
['Alphanumerical', 'Alphanumerical'],
|
||||
['AlphanumericalWithSpaces', 'AlphanumericalWithSpaces'],
|
||||
['AlphanumericalNotAccented', 'AlphanumericalNotAccented'],
|
||||
['AlphanumericalNotAccentedWithSpaces', 'AlphanumericalNotAccentedWithSpaces'],
|
||||
['SimpleDate (2006-11-24)', 'Date'],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -132,32 +133,6 @@ Blockly.Blocks['data_object_field_type'] = {
|
|||
this.jsonInit({
|
||||
type: 'data_object_field_type',
|
||||
message0: 'type %1 %2',
|
||||
args0: [
|
||||
{
|
||||
type: 'field_input',
|
||||
name: 'object_type',
|
||||
text: 'string',
|
||||
},
|
||||
{
|
||||
type: 'input_value',
|
||||
name: 'input',
|
||||
check: ['Boolean', 'String'],
|
||||
},
|
||||
],
|
||||
output: null,
|
||||
colour: 230,
|
||||
tooltip: '',
|
||||
helpUrl: '',
|
||||
});
|
||||
// this.setStyle('loop_blocks');
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks['data_object_field_default_types'] = {
|
||||
init: function () {
|
||||
this.jsonInit({
|
||||
type: 'data_object_field_default_types',
|
||||
message0: 'type %1 %2',
|
||||
args0: [
|
||||
{
|
||||
type: 'field_dropdown',
|
||||
|
@ -167,7 +142,7 @@ Blockly.Blocks['data_object_field_default_types'] = {
|
|||
{
|
||||
type: 'input_value',
|
||||
name: 'input',
|
||||
check: ['Boolean', 'String'],
|
||||
check: 'String',
|
||||
},
|
||||
],
|
||||
output: null,
|
||||
|
@ -193,7 +168,7 @@ Blockly.Blocks['data_object_field_encryption'] = {
|
|||
{
|
||||
type: 'input_value',
|
||||
name: 'input',
|
||||
check: ['Boolean', 'String'],
|
||||
check: 'String',
|
||||
},
|
||||
],
|
||||
output: null,
|
||||
|
@ -204,26 +179,27 @@ Blockly.Blocks['data_object_field_encryption'] = {
|
|||
// this.setStyle('loop_blocks');
|
||||
},
|
||||
};
|
||||
|
||||
Blockly.Blocks['data_object_field_mask'] = {
|
||||
init: function () {
|
||||
this.jsonInit({
|
||||
type: 'data_object_field_mask',
|
||||
message0: 'mask (Admin:%1) (Member:%2) %3',
|
||||
message0: 'mask (admin:%1) (member:%2) %3',
|
||||
args0: [
|
||||
{
|
||||
type: 'field_dropdown',
|
||||
name: 'object_type_ADMIN',
|
||||
name: 'object_type_admin',
|
||||
options: getMasks(),
|
||||
},
|
||||
{
|
||||
type: 'field_dropdown',
|
||||
name: 'object_type_MEMBER',
|
||||
name: 'object_type_member',
|
||||
options: getMasks(),
|
||||
},
|
||||
{
|
||||
type: 'input_value',
|
||||
name: 'object_type',
|
||||
check: ['Boolean', 'String'],
|
||||
check: 'String',
|
||||
},
|
||||
],
|
||||
output: null,
|
||||
|
@ -234,16 +210,11 @@ Blockly.Blocks['data_object_field_mask'] = {
|
|||
},
|
||||
};
|
||||
|
||||
const capitalize = (s: string) => {
|
||||
if (typeof s !== 'string') return '';
|
||||
return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
|
||||
};
|
||||
|
||||
export const maskSetup = (roles: string[]) => {
|
||||
let maskMessage = 'mask (';
|
||||
const args: any[] = [];
|
||||
for (let i = 0; i < roles.length; i++) {
|
||||
maskMessage += `${capitalize(roles[i])}:%${i + 1}`;
|
||||
maskMessage += `${roles[i]}:%${i + 1}`;
|
||||
if (i < roles.length - 1) {
|
||||
maskMessage += ') (';
|
||||
}
|
||||
|
@ -263,7 +234,7 @@ export const maskSetup = (roles: string[]) => {
|
|||
{
|
||||
type: 'input_value',
|
||||
name: 'object_type',
|
||||
check: ['Boolean', 'String'],
|
||||
check: 'String',
|
||||
},
|
||||
],
|
||||
output: null,
|
||||
|
|
|
@ -7,18 +7,6 @@ let currentObject, currentField;
|
|||
|
||||
const IGNORE_FIELDS = [CONST_OBJ_GLB_ENCR];
|
||||
|
||||
// TODO this must come from CUE and the regexp are hacked here for generation
|
||||
const simpleRegex = '^[a-zA-Z]+$';
|
||||
const regexMap = {
|
||||
'defs.#Letters': simpleRegex,
|
||||
'defs.#LettersWithSpaces': simpleRegex,
|
||||
'defs.#Alphanumerical': simpleRegex,
|
||||
'defs.#AlphanumericalWithSpaces': simpleRegex,
|
||||
'defs.#AlphanumericalNotAccented': simpleRegex,
|
||||
'defs.#AlphanumericalNotAccentedWithSpaces': simpleRegex,
|
||||
'defs.#SimpleDateFormat': '^20[0-9]{2}-[0-1][1-2]-[0-2][1-8]$', // regex restricted.
|
||||
};
|
||||
|
||||
export const generateModel = (workspace, roles: string[]) => {
|
||||
ObjectMap.clear();
|
||||
|
||||
|
@ -28,79 +16,45 @@ export const generateModel = (workspace, roles: string[]) => {
|
|||
currentField[2 + i] = objName; // mask
|
||||
}
|
||||
|
||||
javascriptGenerator.statementToCode(block, 'input', javascriptGenerator.ORDER_NONE);
|
||||
javascriptGenerator.statementToCode(block, 'input');
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
// trigger the BLOCKLY processing which will run our custom code generation
|
||||
javascriptGenerator.workspaceToCode(workspace);
|
||||
const ret = generateCUEStructure(roles);
|
||||
const ret = generateStructure(roles);
|
||||
|
||||
// add specific BoxyHQ imports
|
||||
return `
|
||||
EncryptedDefinitions: ${JSON.stringify(ret[1])}
|
||||
${ret[0]}
|
||||
`;
|
||||
return JSON.stringify(ret);
|
||||
};
|
||||
|
||||
// Rudimentary way of generating a CUE file
|
||||
const generateCUEStructure = (roles: string[]) => {
|
||||
let defs = ``;
|
||||
const encrObjects = [];
|
||||
const generateStructure = (roles: string[]) => {
|
||||
const model: any = {};
|
||||
for (const [key, value] of Object.entries(Object.fromEntries(ObjectMap))) {
|
||||
encrObjects.push(key as never);
|
||||
model.name = key;
|
||||
model.attributes = {};
|
||||
const valuesMap = Object.fromEntries(value as any);
|
||||
|
||||
// DEFINITIONS
|
||||
let definitions = ``;
|
||||
for (const [field, values] of Object.entries(valuesMap)) {
|
||||
const rolesMap = {};
|
||||
for (let i = 0; i < roles.length; i++) {
|
||||
rolesMap[roles[i]] = values[i + 2];
|
||||
}
|
||||
|
||||
model.attributes[field] = {
|
||||
type: values[0],
|
||||
encryption: values[1],
|
||||
masking: {
|
||||
roles: rolesMap,
|
||||
},
|
||||
};
|
||||
if (IGNORE_FIELDS.includes(field)) {
|
||||
continue;
|
||||
}
|
||||
definitions += `\n\t\t\t${field}: ${values[0]}`;
|
||||
|
||||
let pattern = regexMap[values[0]];
|
||||
if (pattern == null) {
|
||||
pattern = '.*';
|
||||
}
|
||||
}
|
||||
|
||||
// ENCRYPTION
|
||||
let encryption = ``;
|
||||
if (valuesMap[CONST_OBJ_GLB_ENCR] != null) {
|
||||
encryption += `${valuesMap[CONST_OBJ_GLB_ENCR]}`;
|
||||
} else {
|
||||
for (const [field, values] of Object.entries(valuesMap)) {
|
||||
encryption += `\n\t\t\t${field}: ${values[1]}`;
|
||||
}
|
||||
encryption = `{ ${encryption}
|
||||
}`;
|
||||
}
|
||||
|
||||
// MASKS
|
||||
let maskString = '';
|
||||
for (const [field, values] of Object.entries(valuesMap)) {
|
||||
if (IGNORE_FIELDS.includes(field)) {
|
||||
continue;
|
||||
}
|
||||
let index = 2;
|
||||
for (const role of roles) {
|
||||
const maskKey = `#Mask_${role.toLowerCase()}`;
|
||||
const maskVal = `\n\t\t\t${field}: ${values.length > index ? values[index++] : 'masking.#MRedact'}`;
|
||||
maskString += `\n${maskKey}: { ${maskVal}
|
||||
}`;
|
||||
}
|
||||
}
|
||||
const objectOutput = `\n#${key}: {
|
||||
#Definition: { ${definitions}
|
||||
}
|
||||
#Encryption: ${encryption}${maskString}
|
||||
}`;
|
||||
defs += objectOutput;
|
||||
}
|
||||
|
||||
return [defs, encrObjects];
|
||||
return model;
|
||||
};
|
||||
|
||||
javascriptGenerator['data_object_wrapper'] = function (block) {
|
||||
|
@ -130,7 +84,7 @@ javascriptGenerator['data_object_field_wrapper'] = function (block) {
|
|||
currentField = new Array(3);
|
||||
currentObject.set(objectName, currentField);
|
||||
|
||||
javascriptGenerator.statementToCode(block, 'input', javascriptGenerator.ORDER_NONE);
|
||||
javascriptGenerator.statementToCode(block, 'input');
|
||||
|
||||
return '';
|
||||
};
|
||||
|
@ -138,21 +92,19 @@ javascriptGenerator['data_object_field_wrapper'] = function (block) {
|
|||
javascriptGenerator['data_object_field_type'] = function (block) {
|
||||
const objectName = block.getFieldValue('object_type');
|
||||
currentField[0] = objectName; // type
|
||||
currentField[2] = 'masking.#MRedact'; // mask
|
||||
currentField[3] = 'masking.#MRedact'; // mask
|
||||
currentField[2] = 'Redact'; // mask
|
||||
currentField[3] = 'Redact'; // mask
|
||||
|
||||
javascriptGenerator.statementToCode(block, 'input', javascriptGenerator.ORDER_NONE);
|
||||
javascriptGenerator.statementToCode(block, 'input');
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
javascriptGenerator['data_object_field_default_types'] = javascriptGenerator['data_object_field_type'];
|
||||
|
||||
javascriptGenerator['data_object_field_encryption'] = function (block) {
|
||||
const objectName = block.getFieldValue('object_type');
|
||||
currentField[1] = objectName; // encryption
|
||||
|
||||
javascriptGenerator.statementToCode(block, 'input', javascriptGenerator.ORDER_NONE);
|
||||
javascriptGenerator.statementToCode(block, 'input');
|
||||
|
||||
return '';
|
||||
};
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
export const options = {
|
||||
extraHTTPHeaders: {
|
||||
Authorization: `Api-Key ${process.env.JACKSON_API_KEYS}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
|
@ -1,4 +1,5 @@
|
|||
import { expect, type APIRequestContext } from '@playwright/test';
|
||||
import { Directory } from 'npm/src';
|
||||
|
||||
const directoryBase = {
|
||||
tenant: 'api-boxyhq',
|
||||
|
@ -9,8 +10,8 @@ const directoryBase = {
|
|||
|
||||
export const directoryPayload = {
|
||||
...directoryBase,
|
||||
webhook_url: 'https://example.com',
|
||||
webhook_secret: 'secret',
|
||||
webhook_url: '',
|
||||
webhook_secret: '',
|
||||
};
|
||||
|
||||
export const directoryExpected = {
|
||||
|
@ -22,7 +23,19 @@ export const directoryExpected = {
|
|||
secret: expect.any(String),
|
||||
endpoint: expect.any(String),
|
||||
},
|
||||
webhook: { endpoint: 'https://example.com', secret: 'secret' },
|
||||
webhook: { endpoint: '', secret: '' },
|
||||
};
|
||||
|
||||
export const updateDirectory = async (request: APIRequestContext, directory: Directory, data: any) => {
|
||||
const response = await request.patch(`/api/v1/dsync/${directory.id}`, {
|
||||
data,
|
||||
});
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const { data: updatedDirectory } = await response.json();
|
||||
return updatedDirectory;
|
||||
};
|
||||
|
||||
export const createDirectory = async (request: APIRequestContext, payload: typeof directoryPayload) => {
|
||||
|
@ -59,9 +72,43 @@ export const getDirectory = async (
|
|||
return data;
|
||||
};
|
||||
|
||||
export const getDirectoryByProduct = async (request: APIRequestContext, { product }: { product: string }) => {
|
||||
const response = await request.get('/api/v1/dsync/product', {
|
||||
params: {
|
||||
product,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const deleteDirectory = async (request: APIRequestContext, directoryId: string) => {
|
||||
const response = await request.delete(`/api/v1/dsync/${directoryId}`);
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
};
|
||||
|
||||
export const getDirectoryEvents = async (
|
||||
request: APIRequestContext,
|
||||
params: {
|
||||
tenant?: string;
|
||||
product?: string;
|
||||
directoryId: string;
|
||||
}
|
||||
) => {
|
||||
const response = await request.get(`/api/v1/dsync/events`, {
|
||||
params,
|
||||
});
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const { data } = await response.json();
|
||||
return data;
|
||||
};
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { expect, type APIRequestContext } from '@playwright/test';
|
||||
import type { Directory } from '@boxyhq/saml-jackson';
|
||||
import type { Directory, Group } from '@boxyhq/saml-jackson';
|
||||
import { scimOpUrl } from './utils';
|
||||
|
||||
export const createGroup = async (request: APIRequestContext, directory: Directory, group: any) => {
|
||||
const response = await request.post(`${directory.scim.path}/Groups`, {
|
||||
const scimOpEndpoint = scimOpUrl(directory, 'Groups');
|
||||
const response = await request.post(scimOpEndpoint, {
|
||||
data: group,
|
||||
headers: {
|
||||
Authorization: `Bearer ${directory.scim.secret}`,
|
||||
|
@ -15,6 +17,39 @@ export const createGroup = async (request: APIRequestContext, directory: Directo
|
|||
return await response.json();
|
||||
};
|
||||
|
||||
export const addGroupMember = async (
|
||||
request: APIRequestContext,
|
||||
directory: Directory,
|
||||
group: Group,
|
||||
member: string
|
||||
) => {
|
||||
const scimOpEndpoint = scimOpUrl(directory, `Groups/${group.id}`);
|
||||
const response = await request.patch(scimOpEndpoint, {
|
||||
data: {
|
||||
Operations: [
|
||||
{
|
||||
action: 'addGroupMember',
|
||||
op: 'add',
|
||||
path: 'members',
|
||||
value: [
|
||||
{
|
||||
value: member,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${directory.scim.secret}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
export const getGroupByDisplayName = async (
|
||||
request: APIRequestContext,
|
||||
directory: Directory,
|
||||
|
@ -47,3 +82,57 @@ export const getGroupById = async (request: APIRequestContext, directory: Direct
|
|||
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
export const getGroupsByDirectoryId = async (request: APIRequestContext, directory: Directory) => {
|
||||
const response = await request.get(`${directory.scim.path}/Groups`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${directory.scim.secret}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const data = await response.json();
|
||||
return data.Resources;
|
||||
};
|
||||
|
||||
export const deleteGroup = async (request: APIRequestContext, directory: Directory, groupId: string) => {
|
||||
const scimOpEndpoint = scimOpUrl(directory, `Groups/${groupId}`);
|
||||
|
||||
const response = await request.delete(scimOpEndpoint, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${directory.scim.secret}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
export const updateGroupName = async (
|
||||
request: APIRequestContext,
|
||||
directory: Directory,
|
||||
groupId: string,
|
||||
newName: string
|
||||
) => {
|
||||
const scimOpEndpoint = scimOpUrl(directory, `Groups/${groupId}`);
|
||||
const response = await request.patch(scimOpEndpoint, {
|
||||
data: {
|
||||
Operations: [
|
||||
{
|
||||
op: 'replace',
|
||||
path: 'displayName',
|
||||
value: newName,
|
||||
},
|
||||
],
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${directory.scim.secret}`,
|
||||
},
|
||||
});
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export * from './users';
|
||||
export * from './groups';
|
|
@ -0,0 +1,22 @@
|
|||
import { type APIRequestContext, expect } from '@playwright/test';
|
||||
import { OAuthReq } from 'npm/src';
|
||||
|
||||
// Make oauth autorize request
|
||||
export const oauthAuthorize = async (request: APIRequestContext, data: OAuthReq, isFailure = false) => {
|
||||
try {
|
||||
const response = await request.post('/api/oauth/authorize', {
|
||||
data,
|
||||
});
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
if (!isFailure) {
|
||||
expect(response.status()).toBe(302);
|
||||
}
|
||||
} catch (ex: any) {
|
||||
if (isFailure) {
|
||||
expect(ex.message).toBeDefined();
|
||||
} else {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
};
|
|
@ -71,6 +71,20 @@ export const getConnection = async (
|
|||
return await response.json();
|
||||
};
|
||||
|
||||
// Get connections by product
|
||||
export const getConnectionByProduct = async (request: APIRequestContext, product: string) => {
|
||||
const response = await request.get('/api/v1/sso/product', {
|
||||
params: {
|
||||
product,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
// Delete a connection
|
||||
export const deleteConnection = async (
|
||||
request: APIRequestContext,
|
||||
|
@ -86,3 +100,57 @@ export const deleteConnection = async (
|
|||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(204);
|
||||
};
|
||||
|
||||
// get a sso trace by id
|
||||
export const getSSOTraceById = async (request: APIRequestContext, id: string) => {
|
||||
const response = await request.get('/api/v1/sso-traces', {
|
||||
params: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
// get sso traces by product
|
||||
export const getSSOTracesByProduct = async (request: APIRequestContext, product: string) => {
|
||||
const response = await request.get('/api/v1/sso-traces/product', {
|
||||
params: {
|
||||
product,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
// Delete sso traces by product
|
||||
export const deleteSSOTraces = async (request: APIRequestContext, product: string) => {
|
||||
const response = await request.delete('/api/v1/sso-traces/product', {
|
||||
params: {
|
||||
product,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(204);
|
||||
};
|
||||
|
||||
// Count sso traces by product
|
||||
export const countSSOTracesByProduct = async (request: APIRequestContext, product: string) => {
|
||||
const response = await request.get('/api/v1/sso-traces/product/count', {
|
||||
params: {
|
||||
product,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
return await response.json();
|
||||
};
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { expect, type APIRequestContext } from '@playwright/test';
|
||||
import type { Directory } from '@boxyhq/saml-jackson';
|
||||
import users from '../../../npm/test/dsync/data/users';
|
||||
import { scimOpUrl } from './utils';
|
||||
|
||||
type User = (typeof users)[0];
|
||||
type User = Partial<(typeof users)[0]>;
|
||||
|
||||
export const createUser = async (request: APIRequestContext, directory: Directory, user: User) => {
|
||||
const response = await request.post(`${directory.scim.endpoint}/Users`, {
|
||||
const scimOpEndpoint = scimOpUrl(directory, 'Users');
|
||||
|
||||
const response = await request.post(scimOpEndpoint, {
|
||||
data: user,
|
||||
headers: {
|
||||
Authorization: `Bearer ${directory.scim.secret}`,
|
||||
|
@ -18,8 +21,38 @@ export const createUser = async (request: APIRequestContext, directory: Director
|
|||
return await response.json();
|
||||
};
|
||||
|
||||
export const updateUser = async (
|
||||
request: APIRequestContext,
|
||||
directory: Directory,
|
||||
userId: string,
|
||||
updatedUser: any,
|
||||
isPatch: boolean
|
||||
) => {
|
||||
const scimOpEndpoint = scimOpUrl(directory, `Users/${userId}`);
|
||||
const response = isPatch
|
||||
? await request.patch(scimOpEndpoint, {
|
||||
data: updatedUser,
|
||||
headers: {
|
||||
Authorization: `Bearer ${directory.scim.secret}`,
|
||||
},
|
||||
})
|
||||
: await request.put(scimOpEndpoint, {
|
||||
data: updatedUser,
|
||||
headers: {
|
||||
Authorization: `Bearer ${directory.scim.secret}`,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
export const getUser = async (request: APIRequestContext, directory: Directory, userName: string) => {
|
||||
const response = await request.get(`${directory.scim.path}/Users`, {
|
||||
const scimOpEndpoint = scimOpUrl(directory, 'Users');
|
||||
|
||||
const response = await request.get(scimOpEndpoint, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${directory.scim.secret}`,
|
||||
},
|
||||
|
@ -35,7 +68,9 @@ export const getUser = async (request: APIRequestContext, directory: Directory,
|
|||
};
|
||||
|
||||
export const deleteUser = async (request: APIRequestContext, directory: Directory, userId: string) => {
|
||||
const response = await request.delete(`${directory.scim.path}/Users/${userId}`, {
|
||||
const scimOpEndpoint = scimOpUrl(directory, `Users/${userId}`);
|
||||
|
||||
const response = await request.delete(scimOpEndpoint, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${directory.scim.secret}`,
|
||||
},
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import type { Directory } from '@boxyhq/saml-jackson';
|
||||
|
||||
export function scimOpUrl(directory: Directory, opPath: string) {
|
||||
let endpoint = `${directory.scim.endpoint}/${opPath}`;
|
||||
if (directory.type === 'azure-scim-v2') {
|
||||
const [_main, aadOpts] = directory.scim.endpoint!.split('?');
|
||||
endpoint = `${_main}${opPath}?${aadOpts}`;
|
||||
}
|
||||
return endpoint;
|
||||
}
|
|
@ -4,13 +4,9 @@ import { createUser, getUser } from '../../helpers/users';
|
|||
import { createGroup, getGroupByDisplayName, getGroupById } from '../../helpers/groups';
|
||||
import groups from '../../../../npm/test/dsync/data/groups';
|
||||
import users from '../../../../npm/test/dsync/data/users';
|
||||
import { options } from '../../helpers/api';
|
||||
|
||||
test.use({
|
||||
extraHTTPHeaders: {
|
||||
Authorization: `Api-Key secret`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
test.use(options);
|
||||
|
||||
const { tenant, product } = { ...directoryPayload, tenant: 'api-boxyhq-1' };
|
||||
|
||||
|
|
|
@ -2,13 +2,9 @@ import { test, expect } from '@playwright/test';
|
|||
import users from '../../../../npm/test/dsync/data/users';
|
||||
import { createDirectory, deleteDirectory, directoryPayload, getDirectory } from '../../helpers/directories';
|
||||
import { createUser, getUser } from '../../helpers/users';
|
||||
import { options } from '../../helpers/api';
|
||||
|
||||
test.use({
|
||||
extraHTTPHeaders: {
|
||||
Authorization: `Api-Key secret`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
test.use(options);
|
||||
|
||||
const { tenant, product } = { ...directoryPayload, tenant: 'api-boxyhq-2' };
|
||||
|
||||
|
|
|
@ -5,14 +5,11 @@ import {
|
|||
directoryExpected,
|
||||
directoryPayload,
|
||||
getDirectory,
|
||||
getDirectoryByProduct,
|
||||
} from '../../helpers/directories';
|
||||
import { options } from '../../helpers/api';
|
||||
|
||||
test.use({
|
||||
extraHTTPHeaders: {
|
||||
Authorization: `Api-Key secret`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
test.use(options);
|
||||
|
||||
const { tenant, product } = directoryPayload;
|
||||
|
||||
|
@ -25,6 +22,9 @@ test.beforeAll(async ({ request }) => {
|
|||
test.afterAll(async ({ request }) => {
|
||||
const [directory] = await getDirectory(request, { tenant, product });
|
||||
|
||||
if (!directory) {
|
||||
return;
|
||||
}
|
||||
await deleteDirectory(request, directory.id);
|
||||
});
|
||||
|
||||
|
@ -157,3 +157,15 @@ test.describe('PATCH /api/v1/dsync/{directoryId}', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('GET /api/v1/dsync/product', () => {
|
||||
test('should be able to get a directory by product', async ({ request }) => {
|
||||
let directories = await getDirectoryByProduct(request, { product });
|
||||
expect(directories.length).toBe(1);
|
||||
|
||||
await deleteDirectory(request, directories[0].id);
|
||||
|
||||
directories = await getDirectoryByProduct(request, { product });
|
||||
expect(directories.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
createDirectory,
|
||||
deleteDirectory,
|
||||
directoryPayload,
|
||||
getDirectory,
|
||||
getDirectoryEvents,
|
||||
updateDirectory,
|
||||
} from '../../helpers/directories';
|
||||
import groups from '@boxyhq/saml-jackson/test/dsync/data/groups';
|
||||
import { addGroupMember, createGroup } from '../../helpers/groups';
|
||||
import { options } from '../../helpers/api';
|
||||
|
||||
test.use(options);
|
||||
|
||||
const { tenant, product } = { ...directoryPayload, tenant: 'api-boxyhq-3' };
|
||||
const memberId = 'member1';
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
let directory = await createDirectory(request, {
|
||||
...directoryPayload,
|
||||
webhook_url: 'https://example.com',
|
||||
webhook_secret: 'secret',
|
||||
tenant,
|
||||
});
|
||||
|
||||
directory = await updateDirectory(request, directory, {
|
||||
log_webhook_events: true,
|
||||
});
|
||||
|
||||
const group = await createGroup(request, directory, groups[0]);
|
||||
await addGroupMember(request, directory, group, memberId);
|
||||
});
|
||||
|
||||
test.afterAll(async ({ request }) => {
|
||||
const [directory] = await getDirectory(request, { tenant, product });
|
||||
|
||||
await deleteDirectory(request, directory.id);
|
||||
});
|
||||
|
||||
test.describe('GET /api/v1/dsync/events', () => {
|
||||
test('should be able to get list of events from a directory', async ({ request }) => {
|
||||
const [directory] = await getDirectory(request, { tenant, product });
|
||||
|
||||
// Get events using tenant, product & directoryId
|
||||
const data = await getDirectoryEvents(request, { tenant, product, directoryId: directory.id });
|
||||
|
||||
expect(data.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('GET /api/v1/dsync/events/:event', () => {
|
||||
test('should be able to delete all the events from directory', async ({ request }) => {
|
||||
const [directory] = await getDirectory(request, { tenant, product });
|
||||
|
||||
// Get events using directoryId
|
||||
const events = await getDirectoryEvents(request, {
|
||||
directoryId: directory.id,
|
||||
});
|
||||
|
||||
const response = await request.get(`/api/v1/dsync/events/${events[0].id}`, {
|
||||
params: {
|
||||
tenant,
|
||||
product,
|
||||
directoryId: directory.id,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: event } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(event.status_code).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('DELETE /api/v1/dsync/events', () => {
|
||||
test('should be able to delete all the events from directory', async ({ request }) => {
|
||||
const [directory] = await getDirectory(request, { tenant, product });
|
||||
|
||||
const response = await request.delete(`/api/v1/dsync/events`, {
|
||||
params: {
|
||||
directoryId: directory.id,
|
||||
},
|
||||
});
|
||||
|
||||
const events = await getDirectoryEvents(request, {
|
||||
tenant,
|
||||
product,
|
||||
directoryId: directory.id,
|
||||
});
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(events.length).toBe(0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { createDirectory, deleteDirectory, directoryPayload, getDirectory } from '../../helpers/directories';
|
||||
import groups from '@boxyhq/saml-jackson/test/dsync/data/groups';
|
||||
import { addGroupMember, createGroup, getGroupsByDirectoryId } from '../../helpers/groups';
|
||||
import { options } from '../../helpers/api';
|
||||
|
||||
test.use(options);
|
||||
|
||||
const { tenant, product } = { ...directoryPayload, tenant: 'api-boxyhq-3' };
|
||||
const memberId = 'member1';
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
const directory = await createDirectory(request, {
|
||||
...directoryPayload,
|
||||
tenant,
|
||||
});
|
||||
|
||||
const group = await createGroup(request, directory, groups[0]);
|
||||
await addGroupMember(request, directory, group, memberId);
|
||||
});
|
||||
|
||||
test.afterAll(async ({ request }) => {
|
||||
const [directory] = await getDirectory(request, { tenant, product });
|
||||
|
||||
await deleteDirectory(request, directory.id);
|
||||
});
|
||||
|
||||
test.describe('GET /api/v1/dsync/groups/:id/members', () => {
|
||||
test('should be able to get a group members from a directory', async ({ request }) => {
|
||||
const [directory] = await getDirectory(request, { tenant, product });
|
||||
|
||||
const groups = await getGroupsByDirectoryId(request, directory);
|
||||
const response = await request.get(`/api/v1/dsync/groups/${groups[0].id}/members`, {
|
||||
params: {
|
||||
tenant,
|
||||
product,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: directoryMembers } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(directoryMembers.length).toBe(1);
|
||||
expect(directoryMembers).toMatchObject([
|
||||
{
|
||||
user_id: memberId,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -2,13 +2,9 @@ import { test, expect } from '@playwright/test';
|
|||
import { createDirectory, deleteDirectory, directoryPayload, getDirectory } from '../../helpers/directories';
|
||||
import groups from '@boxyhq/saml-jackson/test/dsync/data/groups';
|
||||
import { createGroup } from '../../helpers/groups';
|
||||
import { options } from '../../helpers/api';
|
||||
|
||||
test.use({
|
||||
extraHTTPHeaders: {
|
||||
Authorization: `Api-Key secret`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
test.use(options);
|
||||
|
||||
const { tenant, product } = { ...directoryPayload, tenant: 'api-boxyhq-3' };
|
||||
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { options } from '../../helpers/api';
|
||||
|
||||
const tenant = 'tenant-1';
|
||||
const product = 'product-1';
|
||||
|
||||
test.use({
|
||||
extraHTTPHeaders: {
|
||||
Authorization: `Api-Key secret`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
test.use(options);
|
||||
|
||||
// POST /api/v1/dsync/setuplinks
|
||||
test('create the setup link', async ({ request }) => {
|
||||
|
|
|
@ -2,13 +2,9 @@ import { test, expect } from '@playwright/test';
|
|||
import users from '../../../../npm/test/dsync/data/users';
|
||||
import { createDirectory, deleteDirectory, directoryPayload, getDirectory } from '../../helpers/directories';
|
||||
import { createUser } from '../../helpers/users';
|
||||
import { options } from '../../helpers/api';
|
||||
|
||||
test.use({
|
||||
extraHTTPHeaders: {
|
||||
Authorization: `Api-Key secret`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
test.use(options);
|
||||
|
||||
const { tenant, product } = { ...directoryPayload, tenant: 'api-boxyhq-4' };
|
||||
|
||||
|
|
|
@ -1,123 +0,0 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { SAMLFederationApp } from '@boxyhq/saml-jackson';
|
||||
|
||||
test.use({
|
||||
extraHTTPHeaders: {
|
||||
Authorization: `Api-Key secret`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const expectedApp = {
|
||||
name: 'Test App',
|
||||
tenant: 'api-boxyhq',
|
||||
product: 'api-saml-jackson',
|
||||
id: expect.any(String),
|
||||
entityId: 'https://boxyhq.com/entity-id',
|
||||
acsUrl: 'https://boxyhq.com/acs',
|
||||
};
|
||||
|
||||
let app = {} as SAMLFederationApp;
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
const response = await request.post('/api/v1/federated-saml', {
|
||||
data: {
|
||||
...expectedApp,
|
||||
},
|
||||
});
|
||||
|
||||
app = (await response.json()).data;
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(201);
|
||||
});
|
||||
|
||||
test.describe('GET /api/v1/federated-saml', () => {
|
||||
test('Fetch app by id', async ({ request }) => {
|
||||
const response = await request.get(`/api/v1/federated-saml?id=${app?.id}`);
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject(app);
|
||||
});
|
||||
|
||||
test('Fetch app by tenant and product', async ({ request }) => {
|
||||
const response = await request.get(
|
||||
`/api/v1/federated-saml?tenant=${app?.tenant}&product=${app?.product}`
|
||||
);
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject(app);
|
||||
});
|
||||
|
||||
test('Fetch app by product', async ({ request }) => {
|
||||
const response = await request.get(`/api/v1/federated-saml/product?product=${app?.product}`);
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject([app]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('PATCH /api/v1/federated-saml', () => {
|
||||
test('Update app by id', async ({ request }) => {
|
||||
const response = await request.patch(`/api/v1/federated-saml`, {
|
||||
data: {
|
||||
id: app?.id,
|
||||
name: 'Updated App',
|
||||
},
|
||||
});
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject({
|
||||
...app,
|
||||
name: 'Updated App',
|
||||
});
|
||||
});
|
||||
|
||||
test('Update app by tenant and product', async ({ request }) => {
|
||||
const response = await request.patch(`/api/v1/federated-saml`, {
|
||||
data: {
|
||||
id: app?.id,
|
||||
name: 'Updated App 2',
|
||||
},
|
||||
});
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject({
|
||||
...app,
|
||||
name: 'Updated App 2',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('DELETE /api/v1/federated-saml', () => {
|
||||
test('Delete app by id', async ({ request }) => {
|
||||
const response = await request.delete(`/api/v1/federated-saml?id=${app?.id}`);
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject({});
|
||||
|
||||
// Confirm app is deleted
|
||||
const response2 = await request.get(`/api/v1/federated-saml?id=${app?.id}`);
|
||||
|
||||
expect(response2.ok()).toBe(false);
|
||||
expect(response2.status()).toBe(404);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,175 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { IdentityFederationApp } from '@boxyhq/saml-jackson';
|
||||
import { options } from '../../helpers/api';
|
||||
|
||||
test.use(options);
|
||||
|
||||
const expectedApp = {
|
||||
name: 'Test App',
|
||||
tenant: 'api-boxyhq',
|
||||
product: 'api-saml-jackson',
|
||||
id: expect.any(String),
|
||||
entityId: 'https://boxyhq.com/entity-id',
|
||||
acsUrl: 'https://boxyhq.com/acs',
|
||||
};
|
||||
const expectedApp1 = {
|
||||
name: 'Test App1',
|
||||
tenant: 'api-boxyhq-1',
|
||||
product: 'api-saml-jackson-1',
|
||||
id: expect.any(String),
|
||||
entityId: 'https://boxyhq.com/entity-id-1',
|
||||
acsUrl: 'https://boxyhq.com/acs-1',
|
||||
};
|
||||
const newUrlPath = 'identity-federation';
|
||||
const oldUrlPath = 'federated-saml';
|
||||
|
||||
let app = {} as IdentityFederationApp;
|
||||
let app1 = {} as IdentityFederationApp;
|
||||
|
||||
const beforeAll = async (urlPath, request) => {
|
||||
const currApp = urlPath === oldUrlPath ? expectedApp : expectedApp1;
|
||||
const response = await request.post(`/api/v1/${urlPath}`, {
|
||||
data: {
|
||||
...currApp,
|
||||
},
|
||||
});
|
||||
|
||||
const localApp = (await response.json()).data;
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(201);
|
||||
|
||||
return localApp;
|
||||
};
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
app = await beforeAll(oldUrlPath, request);
|
||||
app1 = await beforeAll(newUrlPath, request);
|
||||
});
|
||||
|
||||
const testGETById = async (urlPath, request) => {
|
||||
const currApp = urlPath === oldUrlPath ? app : app1;
|
||||
const response = await request.get(`/api/v1/${urlPath}?id=${currApp?.id}`);
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject(currApp);
|
||||
};
|
||||
|
||||
const testGETByTenantProduct = async (urlPath, request) => {
|
||||
const currApp = urlPath === oldUrlPath ? app : app1;
|
||||
const response = await request.get(
|
||||
`/api/v1/${urlPath}?tenant=${currApp?.tenant}&product=${currApp?.product}`
|
||||
);
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject(currApp);
|
||||
};
|
||||
|
||||
const testGETByProduct = async (urlPath, request) => {
|
||||
const currApp = urlPath === oldUrlPath ? app : app1;
|
||||
const response = await request.get(`/api/v1/${urlPath}/product?product=${currApp?.product}`);
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject([currApp]);
|
||||
};
|
||||
|
||||
test.describe('GET /api/v1/identity-federation', () => {
|
||||
test('Fetch app by id', async ({ request }) => {
|
||||
await testGETById(oldUrlPath, request);
|
||||
await testGETById(newUrlPath, request);
|
||||
});
|
||||
|
||||
test('Fetch app by tenant and product', async ({ request }) => {
|
||||
await testGETByTenantProduct(oldUrlPath, request);
|
||||
await testGETByTenantProduct(newUrlPath, request);
|
||||
});
|
||||
|
||||
test('Fetch app by product', async ({ request }) => {
|
||||
await testGETByProduct(oldUrlPath, request);
|
||||
await testGETByProduct(newUrlPath, request);
|
||||
});
|
||||
});
|
||||
|
||||
const testPATCHById = async (urlPath, request) => {
|
||||
const currApp = urlPath === oldUrlPath ? app : app1;
|
||||
const response = await request.patch(`/api/v1/${urlPath}`, {
|
||||
data: {
|
||||
id: currApp?.id,
|
||||
name: 'Updated App',
|
||||
},
|
||||
});
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject({
|
||||
...currApp,
|
||||
name: 'Updated App',
|
||||
});
|
||||
};
|
||||
|
||||
const testPATCHByTenantProduct = async (urlPath, request) => {
|
||||
const currApp = urlPath === oldUrlPath ? app : app1;
|
||||
const response = await request.patch(`/api/v1/${urlPath}`, {
|
||||
data: {
|
||||
tenant: currApp?.tenant,
|
||||
product: currApp?.product,
|
||||
name: 'Updated App 2',
|
||||
},
|
||||
});
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject({
|
||||
...currApp,
|
||||
name: 'Updated App 2',
|
||||
});
|
||||
};
|
||||
|
||||
test.describe('PATCH /api/v1/identity-federation', () => {
|
||||
test('Update app by id', async ({ request }) => {
|
||||
await testPATCHById(oldUrlPath, request);
|
||||
await testPATCHById(newUrlPath, request);
|
||||
});
|
||||
|
||||
test('Update app by tenant and product', async ({ request }) => {
|
||||
await testPATCHByTenantProduct(oldUrlPath, request);
|
||||
await testPATCHByTenantProduct(newUrlPath, request);
|
||||
});
|
||||
});
|
||||
|
||||
const testDELETEById = async (urlPath, request) => {
|
||||
const currApp = urlPath === oldUrlPath ? app : app1;
|
||||
const response = await request.delete(`/api/v1/${urlPath}?id=${currApp?.id}`);
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
expect(response.status()).toBe(200);
|
||||
expect(data).toMatchObject({});
|
||||
|
||||
// Confirm app is deleted
|
||||
const response2 = await request.get(`/api/v1/${urlPath}?id=${currApp?.id}`);
|
||||
|
||||
expect(response2.ok()).toBe(false);
|
||||
expect(response2.status()).toBe(404);
|
||||
};
|
||||
|
||||
test.describe('DELETE /api/v1/identity-federation', () => {
|
||||
test('Delete app by id', async ({ request }) => {
|
||||
await testDELETEById(oldUrlPath, request);
|
||||
await testDELETEById(newUrlPath, request);
|
||||
});
|
||||
});
|
|
@ -1,11 +1,7 @@
|
|||
import { test } from '@playwright/test';
|
||||
import { options } from '../../helpers/api';
|
||||
|
||||
test.use({
|
||||
extraHTTPHeaders: {
|
||||
Authorization: `Api-Key secret`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
test.use(options);
|
||||
|
||||
test.describe('OIDC SSO Connection', () => {
|
||||
//
|
||||
|
|
|
@ -6,14 +6,11 @@ import {
|
|||
getRawMetadata,
|
||||
newConnection,
|
||||
expectedConnection,
|
||||
getConnectionByProduct,
|
||||
} from '../../helpers/sso';
|
||||
import { options } from '../../helpers/api';
|
||||
|
||||
test.use({
|
||||
extraHTTPHeaders: {
|
||||
Authorization: `Api-Key secret`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
test.use(options);
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
const { tenant, product } = newConnection;
|
||||
|
@ -210,3 +207,23 @@ test.describe('GET /api/v1/sso/exists', () => {
|
|||
expect(response.status()).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('GET /api/v1/sso/product', () => {
|
||||
const { product } = newConnection;
|
||||
|
||||
test('should get empty array for SSO connection by product', async ({ request }) => {
|
||||
const response = await getConnectionByProduct(request, product);
|
||||
|
||||
expect(response).toMatchObject([]);
|
||||
expect(response.length).toBe(0);
|
||||
});
|
||||
|
||||
test('should be able to get SSO Connections by product', async ({ request }) => {
|
||||
await createConnection(request, newConnection);
|
||||
|
||||
const response = await getConnectionByProduct(request, product);
|
||||
|
||||
expect(response).toMatchObject([expectedConnection]);
|
||||
expect(response.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { options } from '../../helpers/api';
|
||||
|
||||
const tenant = 'tenant-1';
|
||||
const product = 'product-1';
|
||||
|
||||
test.use({
|
||||
extraHTTPHeaders: {
|
||||
Authorization: `Api-Key secret`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
test.use(options);
|
||||
|
||||
// POST /api/v1/sso/setuplinks
|
||||
test('create the setup link', async ({ request }) => {
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
createConnection,
|
||||
deleteConnection,
|
||||
newConnection,
|
||||
deleteSSOTraces,
|
||||
getSSOTracesByProduct,
|
||||
getSSOTraceById,
|
||||
countSSOTracesByProduct,
|
||||
} from '../../helpers/sso';
|
||||
import { options } from '../../helpers/api';
|
||||
import { oauthAuthorize } from '../../helpers/oauth';
|
||||
|
||||
test.use(options);
|
||||
|
||||
test.beforeEach(async ({ request }) => {
|
||||
await createConnection(request, newConnection);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ request }) => {
|
||||
const { tenant, product } = newConnection;
|
||||
|
||||
// Delete the connection & traces after each test
|
||||
await deleteConnection(request, { tenant, product });
|
||||
await deleteSSOTraces(request, product);
|
||||
});
|
||||
|
||||
test.describe('GET /api/v1/sso-traces/product', () => {
|
||||
test('should be able to get empty list of traces', async ({ request }) => {
|
||||
const list = await getSSOTracesByProduct(request, newConnection.product);
|
||||
|
||||
expect(list.data.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('GET /api/v1/sso-traces/product/count', () => {
|
||||
test('should be able to get non empty list of traces', async ({ request }) => {
|
||||
await oauthAuthorize(
|
||||
request,
|
||||
{
|
||||
client_id: 'dummy',
|
||||
tenant: 'dummy',
|
||||
product: newConnection.product,
|
||||
state: 'Bb-w_AqDxZh90BBVz4PRhtRIRetOgo0AR0pmrhzyICU',
|
||||
response_type: 'code',
|
||||
redirect_uri: newConnection.redirectUrl[0].replaceAll('*', ''),
|
||||
code_challenge: 'OcMni5eZvSrQ2ev7tPICbcE7q1piL8Abi8IfJtWbUtY',
|
||||
code_challenge_method: 'S256',
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
const res = await countSSOTracesByProduct(request, newConnection.product);
|
||||
|
||||
expect(res.count).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('GET /api/v1/sso-traces', () => {
|
||||
test('should be able to get sso trace by Id', async ({ request }) => {
|
||||
await oauthAuthorize(
|
||||
request,
|
||||
{
|
||||
client_id: 'dummy',
|
||||
tenant: 'dummy',
|
||||
product: newConnection.product,
|
||||
state: 'Bb-w_AqDxZh90BBVz4PRhtRIRetOgo0AR0pmrhzyICU',
|
||||
response_type: 'code',
|
||||
redirect_uri: newConnection.redirectUrl[0].replaceAll('*', ''),
|
||||
code_challenge: 'OcMni5eZvSrQ2ev7tPICbcE7q1piL8Abi8IfJtWbUtY',
|
||||
code_challenge_method: 'S256',
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
const list = await getSSOTracesByProduct(request, newConnection.product);
|
||||
expect(list.data.length).toBe(1);
|
||||
|
||||
const trace = await getSSOTraceById(request, list.data[0].traceId);
|
||||
expect(trace.data).toMatchObject(list.data[0]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('DELETE /api/v1/sso-traces/product', () => {
|
||||
test('should be able to delete sso trace by product', async ({ request }) => {
|
||||
await oauthAuthorize(
|
||||
request,
|
||||
{
|
||||
client_id: 'dummy',
|
||||
tenant: 'dummy',
|
||||
product: newConnection.product,
|
||||
state: 'Bb-w_AqDxZh90BBVz4PRhtRIRetOgo0AR0pmrhzyICU',
|
||||
response_type: 'code',
|
||||
redirect_uri: newConnection.redirectUrl[0].replaceAll('*', ''),
|
||||
code_challenge: 'OcMni5eZvSrQ2ev7tPICbcE7q1piL8Abi8IfJtWbUtY',
|
||||
code_challenge_method: 'S256',
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
let res = await countSSOTracesByProduct(request, newConnection.product);
|
||||
expect(res.count).toBeGreaterThan(0);
|
||||
|
||||
await deleteSSOTraces(request, newConnection.product);
|
||||
|
||||
res = await countSSOTracesByProduct(request, newConnection.product);
|
||||
expect(res.count).toBe(0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
// https://learn.microsoft.com/en-us/entra/identity/app-provisioning/use-scim-to-provision-users-and-groups#request
|
||||
export const azureUser = (id: number) => ({
|
||||
schemas: [
|
||||
'urn:ietf:params:scim:schemas:core:2.0:User',
|
||||
'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User',
|
||||
],
|
||||
externalId: `jackson-${id}`,
|
||||
userName: `jackson-${id}@boxyhq.onmicrosoft.com`,
|
||||
active: true,
|
||||
displayName: `Jackson-${id}`,
|
||||
emails: [{ primary: true, type: 'work', value: `jackson-${id}@example.com` }],
|
||||
meta: { resourceType: 'User' },
|
||||
name: { formatted: `samuel-${id} jackson-${id}`, familyName: `jackson-${id}`, givenName: `samuel-${id}` },
|
||||
title: 'Manager',
|
||||
roles: [],
|
||||
});
|
||||
|
||||
export const updatedAzureUser = (id: number) => ({
|
||||
schemas: ['urn:ietf:params:scim:api:messages:2.0:PatchOp'],
|
||||
Operations: [
|
||||
{
|
||||
op: 'Replace',
|
||||
path: 'name.givenName',
|
||||
value: `samuel-${id}-updated`,
|
||||
},
|
||||
{
|
||||
op: 'Replace',
|
||||
path: 'name.familyName',
|
||||
value: `jackson-${id}-updated`,
|
||||
},
|
||||
{
|
||||
op: 'Replace',
|
||||
path: 'emails[type eq "work"].value',
|
||||
value: `jackson-${id}-updated@example.com`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const azureGroup = {
|
||||
schemas: [
|
||||
'urn:ietf:params:scim:schemas:core:2.0:Group',
|
||||
'http://schemas.microsoft.com/2006/11/ResourceManagement/ADSCIM/2.0/Group',
|
||||
],
|
||||
externalId: '8aa1a0c0-c4c3-4bc0-b4a5-2ef676900159',
|
||||
displayName: 'BoxyHQ',
|
||||
meta: {
|
||||
resourceType: 'Group',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
export * from './azure';
|
||||
export * from './okta';
|
||||
|
||||
export enum DirectorySyncProviders {
|
||||
'azure-scim-v2' = 'Azure SCIM v2.0',
|
||||
'onelogin-scim-v2' = 'OneLogin SCIM v2.0',
|
||||
'okta-scim-v2' = 'Okta SCIM v2.0',
|
||||
'jumpcloud-scim-v2' = 'JumpCloud v2.0',
|
||||
'generic-scim-v2' = 'Generic SCIM v2.0',
|
||||
'google' = 'Google',
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
// https://developer.okta.com/docs/reference/scim/scim-20/#create-the-user
|
||||
export const oktaUser = (id: number) => ({
|
||||
schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
|
||||
userName: `jackson-${id}@boxyhq.okta.local`,
|
||||
name: { familyName: `jackson-${id}`, givenName: `samuel-${id}` },
|
||||
emails: [{ primary: true, type: 'work', value: `jackson-${id}@okta.local` }],
|
||||
displayName: `Jackson-${id}`,
|
||||
locale: 'en-US',
|
||||
externalId: `jackson-${id}`,
|
||||
groups: [],
|
||||
password: `jackson-password-${id}`,
|
||||
active: true,
|
||||
});
|
||||
|
||||
export const updatedOktaUser = (id: number) => ({
|
||||
schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
|
||||
userName: `jackson-${id}@boxyhq.okta.local`,
|
||||
name: { familyName: `jackson-${id}-updated`, givenName: `samuel-${id}-updated` },
|
||||
emails: [{ primary: true, type: 'work', value: `jackson-${id}-updated@okta.local` }],
|
||||
displayName: `Jackson-${id}`,
|
||||
locale: 'en-US',
|
||||
externalId: `jackson-${id}`,
|
||||
groups: [],
|
||||
password: `jackson-password-${id}`,
|
||||
active: true,
|
||||
});
|
||||
|
||||
export const oktaGroup = {
|
||||
schemas: [
|
||||
'urn:ietf:params:scim:schemas:core:2.0:Group',
|
||||
'http://schemas.microsoft.com/2006/11/ResourceManagement/ADSCIM/2.0/Group',
|
||||
],
|
||||
externalId: '8aa1a0c0-c4c3-4bc0-b4a5-2ef676900159',
|
||||
displayName: 'BoxyHQ',
|
||||
meta: {
|
||||
resourceType: 'Group',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,85 @@
|
|||
import type { Page } from '@playwright/test';
|
||||
import type { DirectorySyncProviders } from '../data/dsync';
|
||||
|
||||
export class DSyncPage {
|
||||
tenant: string;
|
||||
product: string;
|
||||
constructor(public readonly page: Page) {
|
||||
this.tenant = 'acme.com';
|
||||
this.product = 'demo';
|
||||
}
|
||||
|
||||
async gotoDSync() {
|
||||
await this.page.goto(`/admin/directory-sync`);
|
||||
}
|
||||
|
||||
async addDSyncConnection(provider: keyof typeof DirectorySyncProviders) {
|
||||
await this.gotoDSync();
|
||||
await this.page.getByRole('link', { name: 'New Directory' }).click();
|
||||
await this.page.getByLabel('Directory name').fill('DS-1');
|
||||
await this.page.getByLabel('Directory provider').selectOption({ value: provider });
|
||||
await this.page.getByLabel('Tenant').fill(this.tenant);
|
||||
await this.page.getByLabel('Product').fill(this.product);
|
||||
await this.page.getByLabel('Webhook URL').fill('https://example.com');
|
||||
await this.page.getByLabel('Webhook secret').fill('secret');
|
||||
await this.page.getByRole('button', { name: 'Create Directory' }).click();
|
||||
const scimUrl = await this.page.getByLabel('SCIM Endpoint').inputValue();
|
||||
const scimToken = await this.page.getByLabel('SCIM Token').inputValue();
|
||||
return { scimUrl, scimToken };
|
||||
}
|
||||
|
||||
async deleteConnection() {
|
||||
await this.gotoDSync();
|
||||
await this.page.getByLabel('Loading').waitFor({ state: 'hidden' });
|
||||
const editButton = await this.page.getByRole('button').and(this.page.getByLabel('Edit'));
|
||||
await editButton.click();
|
||||
await this.page.getByRole('button', { name: 'Delete' }).click();
|
||||
await this.page.getByRole('button', { name: 'Confirm' }).click();
|
||||
}
|
||||
|
||||
async switchToDSyncInfoView() {
|
||||
await this.gotoDSync();
|
||||
await this.page.getByLabel('View').click();
|
||||
await this.page.waitForURL('/admin/directory-sync/**');
|
||||
}
|
||||
|
||||
async switchToUsersView({ waitForData }: { waitForData?: boolean } = {}) {
|
||||
await this.page.getByRole('listitem').and(this.page.getByText('Users')).click();
|
||||
await this.page.waitForURL(/\/admin\/directory-sync\/.*\/users$/);
|
||||
if (waitForData) {
|
||||
await this.page.getByRole('table').waitFor();
|
||||
}
|
||||
}
|
||||
// group events navigation done after users view, hence we can skip View click
|
||||
async switchToGroupsView({ waitForData }: { waitForData?: boolean } = {}) {
|
||||
await this.page.getByRole('listitem').and(this.page.getByText('Groups')).click();
|
||||
await this.page.waitForURL(/\/admin\/directory-sync\/.*\/groups$/);
|
||||
if (waitForData) {
|
||||
await this.page.getByRole('table').waitFor();
|
||||
}
|
||||
}
|
||||
async switchToEventsView({ waitForData }: { waitForData?: boolean } = {}) {
|
||||
await this.page.getByRole('listitem').and(this.page.getByText('Webhook Events')).click();
|
||||
await this.page.waitForURL(/\/admin\/directory-sync\/.*\/events$/);
|
||||
if (waitForData) {
|
||||
await this.page.getByRole('table').waitFor();
|
||||
}
|
||||
}
|
||||
async inspectEventRow(id: number, webhookEndpoint: string) {
|
||||
const webhookRowRegex = new RegExp(`${webhookEndpoint}.*View`);
|
||||
await this.page.getByRole('row', { name: webhookRowRegex }).getByRole('button').nth(id).click();
|
||||
await this.page.waitForURL(/\/admin\/directory-sync\/.*\/events\/.*/);
|
||||
await this.page.locator('pre').waitFor();
|
||||
}
|
||||
async setWebHookEventsLogging({ enable }: { enable: boolean }) {
|
||||
await this.gotoDSync();
|
||||
await this.page.getByLabel('Edit').click();
|
||||
const checkBox = this.page.getByLabel('Enable Webhook events logging');
|
||||
if (enable) {
|
||||
await checkBox.check();
|
||||
} else {
|
||||
await checkBox.uncheck();
|
||||
}
|
||||
await this.page.getByRole('button', { name: 'Save' }).click();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export * from './portal';
|
||||
export * from './sso-page';
|
||||
export * from './dsync-page';
|
||||
export * from './setuplink-page';
|
|
@ -0,0 +1,20 @@
|
|||
import { Locator, Page, expect } from '@playwright/test';
|
||||
|
||||
export class Portal {
|
||||
userAvatarLocator: Locator;
|
||||
constructor(public readonly page: Page) {
|
||||
this.userAvatarLocator = this.page.getByTestId('user-avatar');
|
||||
}
|
||||
|
||||
async doCredentialsLogin() {
|
||||
await this.page.goto('/admin/auth/login');
|
||||
await this.page.getByPlaceholder('Email').fill('super@boxyhq.com');
|
||||
await this.page.getByPlaceholder('Password').fill('999login');
|
||||
await this.page.getByRole('button', { name: 'Sign In' }).click();
|
||||
}
|
||||
|
||||
async isLoggedIn() {
|
||||
// assert login state
|
||||
await expect(this.userAvatarLocator).toBeVisible();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import { Page, expect } from '@playwright/test';
|
||||
|
||||
const TEST_SETUPLINK_REDIRECT_URL = 'http://localhost:3366';
|
||||
const TEST_SETUPLINK_DEFAULT_REDIRECT_URL = 'http://localhost:3366/login/saml';
|
||||
const TEST_SETUPLINK_ADMIN_URL = '/admin/sso-connection/setup-link';
|
||||
const TEST_SETUPLINK_URL_LABEL_SELECTOR =
|
||||
'Share this link with your customers to allow them to set up the integrationClose';
|
||||
|
||||
export class SetupLinkPage {
|
||||
setupLinkUrl: string;
|
||||
constructor(
|
||||
public readonly page: Page,
|
||||
public readonly product: string,
|
||||
public readonly tenant: string,
|
||||
public readonly adminPage: string = TEST_SETUPLINK_ADMIN_URL,
|
||||
public readonly redirectUrl: string = TEST_SETUPLINK_REDIRECT_URL,
|
||||
public readonly defaultRedirectUrl: string = TEST_SETUPLINK_DEFAULT_REDIRECT_URL
|
||||
) {
|
||||
this.page = page;
|
||||
this.product = product;
|
||||
this.tenant = tenant;
|
||||
this.adminPage = adminPage;
|
||||
this.redirectUrl = redirectUrl;
|
||||
this.defaultRedirectUrl = defaultRedirectUrl;
|
||||
this.setupLinkUrl = '';
|
||||
}
|
||||
|
||||
async createSetupLink() {
|
||||
// Go to admin/sso-connection/setup-link page and create setup link
|
||||
await this.page.goto(this.adminPage);
|
||||
await this.page.getByRole('button', { name: 'New Setup Link' }).click();
|
||||
await this.page.getByPlaceholder('Acme SSO').fill('acme-test');
|
||||
await this.page.getByLabel('Description (Optional)').fill('acme test');
|
||||
await this.page.getByPlaceholder('acme', { exact: true }).fill(this.tenant);
|
||||
await this.page.getByPlaceholder('MyApp').fill(this.product);
|
||||
await this.page.getByPlaceholder('http://localhost:3366', { exact: true }).fill(this.redirectUrl);
|
||||
await this.page.getByPlaceholder('http://localhost:3366/login/').fill(this.defaultRedirectUrl);
|
||||
|
||||
await this.page.getByRole('button', { name: 'Create Setup Link' }).click();
|
||||
|
||||
// Extract generated setup link
|
||||
this.setupLinkUrl = await this.page
|
||||
.getByText(TEST_SETUPLINK_URL_LABEL_SELECTOR)
|
||||
.locator('input[type="text"]')
|
||||
.first()
|
||||
.inputValue();
|
||||
}
|
||||
|
||||
async getSetupLinkUrl(): Promise<string> {
|
||||
return this.setupLinkUrl;
|
||||
}
|
||||
|
||||
async isSetupLinkCreated() {
|
||||
// Go back to new connections page
|
||||
await this.page.goto(TEST_SETUPLINK_ADMIN_URL);
|
||||
|
||||
// Await for rows loaded
|
||||
await expect(this.page.getByRole('table')).toBeVisible();
|
||||
|
||||
// Check if setup link is created
|
||||
await expect(
|
||||
this.page.getByText(this.tenant, { exact: true }),
|
||||
'Failed to create setup link'
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
this.page.getByText(this.product, { exact: true }),
|
||||
'Failed to create setup link'
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
async removeSetupLink() {
|
||||
// Go back to setup link admin url
|
||||
await this.page.goto(TEST_SETUPLINK_ADMIN_URL);
|
||||
|
||||
// Await for rows loaded
|
||||
await expect(this.page.getByRole('table')).toBeVisible();
|
||||
|
||||
// Delete the created setuplink
|
||||
await this.page.getByRole('button').nth(5).click();
|
||||
await this.page.getByRole('button', { name: 'Delete' }).click();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
import type { Page, Locator } from '@playwright/test';
|
||||
import { adminPortalSSODefaults } from '@lib/env';
|
||||
|
||||
const ADMIN_PORTAL_TENANT = adminPortalSSODefaults.tenant;
|
||||
const ADMIN_PORTAL_PRODUCT = adminPortalSSODefaults.product;
|
||||
|
||||
const MOCKSAML_ORIGIN = process.env.MOCKSAML_ORIGIN || 'https://mocksaml.com';
|
||||
const MOCKSAML_SIGNIN_BUTTON_NAME = 'Sign In';
|
||||
|
||||
const MOCKLAB_ORIGIN = 'https://oauth.wiremockapi.cloud';
|
||||
const MOCKLAB_CLIENT_ID = 'mocklab_oauth2';
|
||||
const MOCKLAB_CLIENT_SECRET = 'mocklab_secret';
|
||||
const MOCKLAB_SIGNIN_BUTTON_NAME = 'Login';
|
||||
const MOCKLAB_DISCOVERY_ENDPOINT = 'https://oauth.wiremockapi.cloud/.well-known/openid-configuration';
|
||||
|
||||
export class SSOPage {
|
||||
private readonly createConnection: Locator;
|
||||
private readonly nameInput: Locator;
|
||||
private readonly tenantInput: Locator;
|
||||
private readonly productInput: Locator;
|
||||
private readonly redirectURLSInput: Locator;
|
||||
private readonly defaultRedirectURLInput: Locator;
|
||||
private readonly metadataUrlInput: Locator;
|
||||
private readonly oidcDiscoveryUrlInput: Locator;
|
||||
private readonly oidcClientIdInput: Locator;
|
||||
private readonly oidcClientSecretInput: Locator;
|
||||
private readonly saveConnection: Locator;
|
||||
private readonly deleteButton: Locator;
|
||||
private readonly confirmButton: Locator;
|
||||
private readonly toggleConnectionStatusCheckbox: Locator;
|
||||
private readonly toggleConnectionStatusLabel: Locator;
|
||||
private connections: string[];
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.connections = [];
|
||||
this.createConnection = this.page.getByTestId('create-connection');
|
||||
this.nameInput = this.page.getByLabel('Connection name (Optional)');
|
||||
this.tenantInput = this.page.getByLabel('Tenant');
|
||||
this.productInput = this.page.getByLabel('Product');
|
||||
this.redirectURLSInput = page
|
||||
.getByRole('group')
|
||||
.filter({ hasText: 'Allowed redirect URLs' })
|
||||
.locator(page.getByRole('textbox').first());
|
||||
this.defaultRedirectURLInput = this.page.getByLabel('Default redirect URL');
|
||||
this.metadataUrlInput = this.page.getByLabel('Metadata URL');
|
||||
this.oidcDiscoveryUrlInput = this.page.getByLabel('Well-known URL of OpenID Provider');
|
||||
this.oidcClientIdInput = this.page.getByLabel('Client ID');
|
||||
this.oidcClientSecretInput = this.page.getByLabel('Client Secret');
|
||||
this.saveConnection = this.page.getByRole('button', { name: /save/i });
|
||||
this.toggleConnectionStatusCheckbox = this.page.getByRole('checkbox', { name: 'Active' });
|
||||
this.toggleConnectionStatusLabel = this.page.locator('label').filter({ hasText: 'Active' });
|
||||
this.deleteButton = this.page.getByRole('button', { name: 'Delete' });
|
||||
this.confirmButton = this.page.getByRole('button', { name: 'Confirm' });
|
||||
}
|
||||
|
||||
async goto() {
|
||||
const url = new URL(this.page.url());
|
||||
if (url.pathname !== '/admin/sso-connection') {
|
||||
await this.page.goto('/admin/sso-connection');
|
||||
}
|
||||
}
|
||||
|
||||
async addSSOConnection({
|
||||
name,
|
||||
type = 'saml',
|
||||
tenant,
|
||||
product,
|
||||
baseURL,
|
||||
}: {
|
||||
name: string;
|
||||
type: 'saml' | 'oidc';
|
||||
tenant?: string;
|
||||
product?: string;
|
||||
baseURL: string;
|
||||
}) {
|
||||
const connectionIndex = this.connections.length + 1;
|
||||
const ssoName = `${name}-${connectionIndex}`;
|
||||
// Find the new connection button and click on it
|
||||
await this.createConnection.click();
|
||||
if (type === 'oidc') {
|
||||
// Toggle connection type to OIDC
|
||||
await this.page.getByLabel('OIDC').check();
|
||||
}
|
||||
// Fill the name for the connection
|
||||
await this.nameInput.fill(ssoName);
|
||||
// Fill the tenant for the connection
|
||||
await this.tenantInput.fill(tenant || ADMIN_PORTAL_TENANT);
|
||||
// Fill the product for the connection
|
||||
await this.productInput.fill(product || ADMIN_PORTAL_PRODUCT);
|
||||
// Fill the Allowed redirect URLs for the connection
|
||||
|
||||
await this.redirectURLSInput.fill(baseURL!);
|
||||
// Fill the default redirect URLs for the connection
|
||||
await this.defaultRedirectURLInput.fill(`${baseURL}/admin/auth/idp-login`);
|
||||
if (type === 'saml') {
|
||||
// Enter the metadata url for mocksaml in the form
|
||||
await this.metadataUrlInput.fill(`${MOCKSAML_ORIGIN}/api/namespace/${ssoName}/saml/metadata`);
|
||||
}
|
||||
if (type === 'oidc') {
|
||||
// Enter the OIDC client credentials for mocklab in the form
|
||||
await this.oidcClientIdInput.fill(`${MOCKLAB_CLIENT_ID}-${connectionIndex}`);
|
||||
await this.oidcClientSecretInput.fill(`${MOCKLAB_CLIENT_SECRET}-${connectionIndex}`);
|
||||
// Enter the OIDC discovery url for mocklab in the form
|
||||
await this.oidcDiscoveryUrlInput.fill(MOCKLAB_DISCOVERY_ENDPOINT);
|
||||
}
|
||||
// submit the form
|
||||
await this.saveConnection.click();
|
||||
this.connections = [...this.connections, ssoName];
|
||||
}
|
||||
|
||||
async gotoEditView(name: string) {
|
||||
await this.goto();
|
||||
const editButton = this.page.getByText(name).locator('xpath=..').getByLabel('Edit');
|
||||
await editButton.click();
|
||||
}
|
||||
|
||||
async toggleConnectionStatus(newStatus: boolean) {
|
||||
const isChecked = await this.toggleConnectionStatusCheckbox.isChecked();
|
||||
if (isChecked && !newStatus) {
|
||||
await this.toggleConnectionStatusLabel.click();
|
||||
await this.confirmButton.click();
|
||||
} else if (!isChecked && newStatus) {
|
||||
await this.toggleConnectionStatusLabel.click();
|
||||
await this.confirmButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
async updateSSOConnection({ name, url, newStatus }: { name: string; url: string; newStatus?: boolean }) {
|
||||
await this.gotoEditView(name);
|
||||
await this.redirectURLSInput.fill(url);
|
||||
await this.saveConnection.click();
|
||||
if (typeof newStatus === 'boolean') {
|
||||
await this.gotoEditView(name);
|
||||
await this.toggleConnectionStatus(newStatus);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteSSOConnection(name: string) {
|
||||
await this.gotoEditView(name);
|
||||
// click the delete and confirm deletion
|
||||
await this.deleteButton.click();
|
||||
await this.confirmButton.click();
|
||||
}
|
||||
|
||||
async deleteAllSSOConnections() {
|
||||
let _connection;
|
||||
while ((_connection = this.connections.shift())) {
|
||||
await this.deleteSSOConnection(_connection);
|
||||
}
|
||||
}
|
||||
|
||||
async logout() {
|
||||
const userAvatarLocator = this.page.getByTestId('user-avatar');
|
||||
// Logout from the magic link authentication
|
||||
await userAvatarLocator.click();
|
||||
await this.page.getByTestId('logout').click();
|
||||
}
|
||||
|
||||
async signInWithSSO() {
|
||||
await this.page.getByTestId('sso-login-button').click();
|
||||
}
|
||||
|
||||
async selectIdP(name: string) {
|
||||
const idpSelectionTitle = 'Select an Identity Provider to continue';
|
||||
await this.page.getByText(idpSelectionTitle).waitFor();
|
||||
await this.page.getByRole('button', { name }).click();
|
||||
}
|
||||
|
||||
async signInWithMockSAML() {
|
||||
// Perform sign in at mocksaml
|
||||
await this.page.waitForURL((url) => url.origin === MOCKSAML_ORIGIN);
|
||||
await this.page.getByPlaceholder('jackson').fill('bob');
|
||||
await this.page.getByRole('button', { name: MOCKSAML_SIGNIN_BUTTON_NAME }).click();
|
||||
}
|
||||
|
||||
async signInWithMockLab() {
|
||||
// Perform sign in at mocklab
|
||||
await this.page.waitForURL((url) => url.origin === MOCKLAB_ORIGIN);
|
||||
await this.page.getByPlaceholder('yours@example.com').fill('bob@oidc.com');
|
||||
await this.page.getByRole('button', { name: MOCKLAB_SIGNIN_BUTTON_NAME }).click();
|
||||
}
|
||||
}
|
|
@ -2,6 +2,15 @@
|
|||
import { chromium, FullConfig } from '@playwright/test';
|
||||
import { IDENTIFIER, TOKEN } from './nextAuth.constants';
|
||||
|
||||
function streamToString(stream): Promise<string> {
|
||||
const chunks: any[] = [];
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
||||
stream.on('error', (err) => reject(err));
|
||||
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
||||
});
|
||||
}
|
||||
|
||||
async function globalSetup(config: FullConfig) {
|
||||
// Env init
|
||||
process.env.MOCKSAML_ORIGIN = process.env.CI ? 'http://localhost:4000' : 'https://mocksaml.com';
|
||||
|
@ -15,6 +24,17 @@ async function globalSetup(config: FullConfig) {
|
|||
const page = await browser.newPage();
|
||||
await page.goto(MAGIC_LINK);
|
||||
await page.context().storageState({ path: storageState as string });
|
||||
|
||||
// Get MockSAML metadata
|
||||
await page.goto(process.env.MOCKSAML_ORIGIN);
|
||||
// Start waiting for download before clicking. Note no await.
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.getByRole('link', { name: 'Download Metadata' }).click();
|
||||
const download = await downloadPromise;
|
||||
const _stream = await download.createReadStream();
|
||||
const _metadata = await streamToString(_stream);
|
||||
process.env.MOCKSAML_METADATA = _metadata;
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
import { test as baseTest, expect } from '@playwright/test';
|
||||
import { DSyncPage } from 'e2e/support/fixtures';
|
||||
import { getDirectory } from 'e2e/api/helpers/directories';
|
||||
import { options } from 'e2e/api/helpers/api';
|
||||
import {
|
||||
addGroupMember,
|
||||
createGroup,
|
||||
createUser,
|
||||
deleteGroup,
|
||||
deleteUser,
|
||||
updateGroupName,
|
||||
updateUser,
|
||||
} from 'e2e/api/helpers';
|
||||
import {
|
||||
DirectorySyncProviders,
|
||||
azureGroup,
|
||||
azureUser,
|
||||
updatedAzureUser,
|
||||
oktaGroup,
|
||||
oktaUser,
|
||||
updatedOktaUser,
|
||||
} from 'e2e/support/data/dsync';
|
||||
|
||||
type MyFixtures = {
|
||||
dsyncPage: DSyncPage;
|
||||
};
|
||||
|
||||
export const test = baseTest.extend<MyFixtures>({
|
||||
dsyncPage: async ({ page }, use) => {
|
||||
const dsyncPage = new DSyncPage(page);
|
||||
await use(dsyncPage);
|
||||
await dsyncPage.deleteConnection();
|
||||
},
|
||||
});
|
||||
|
||||
test.use(options);
|
||||
|
||||
const providers = [
|
||||
{ type: 'azure-scim-v2', generators: { user: azureUser, group: azureGroup, updateUser: updatedAzureUser } },
|
||||
{ type: 'okta-scim-v2', generators: { user: oktaUser, group: oktaGroup, updateUser: updatedOktaUser } },
|
||||
];
|
||||
|
||||
for (const provider of providers) {
|
||||
test(`${provider.type} SCIM connection`, async ({ dsyncPage, request, page }) => {
|
||||
await dsyncPage.addDSyncConnection(provider.type as keyof typeof DirectorySyncProviders);
|
||||
// Send API requests to user/groups endpoint
|
||||
const [directory] = await getDirectory(request, { tenant: dsyncPage.tenant, product: dsyncPage.product });
|
||||
const providerUser1 = provider.generators.user(1);
|
||||
const user1 = await createUser(request, directory, providerUser1);
|
||||
const group = await createGroup(request, directory, provider.generators.group);
|
||||
await addGroupMember(request, directory, group, user1.id);
|
||||
// Assert created user
|
||||
await dsyncPage.switchToDSyncInfoView();
|
||||
await dsyncPage.switchToUsersView({ waitForData: true });
|
||||
await expect(page.getByRole('cell', { name: providerUser1.name.givenName, exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: providerUser1.name.familyName, exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: providerUser1.emails[0].value, exact: true })).toBeVisible();
|
||||
// Assert created group
|
||||
await dsyncPage.switchToGroupsView({ waitForData: true });
|
||||
await expect(await page.getByRole('cell', { name: 'BoxyHQ' })).toBeVisible();
|
||||
// Enable webhook logs
|
||||
await dsyncPage.setWebHookEventsLogging({ enable: true });
|
||||
const providerUser2 = provider.generators.user(2);
|
||||
const user2 = await createUser(request, directory, providerUser2);
|
||||
await addGroupMember(request, directory, group, user2.id);
|
||||
// Assert created user
|
||||
await dsyncPage.switchToDSyncInfoView();
|
||||
await dsyncPage.switchToUsersView({ waitForData: true });
|
||||
await expect(page.getByRole('cell', { name: providerUser2.name.givenName, exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: providerUser2.name.familyName, exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: providerUser2.emails[0].value, exact: true })).toBeVisible();
|
||||
// Patch second user
|
||||
const providerUser2Updated = provider.generators.updateUser(2);
|
||||
await updateUser(
|
||||
request,
|
||||
directory,
|
||||
user2.id,
|
||||
providerUser2Updated,
|
||||
providerUser2Updated.schemas[0] === 'urn:ietf:params:scim:api:messages:2.0:PatchOp'
|
||||
);
|
||||
await page.reload();
|
||||
// Assert updated attributes for user
|
||||
const { givenName, familyName, email } =
|
||||
'Operations' in providerUser2Updated
|
||||
? {
|
||||
givenName: providerUser2Updated.Operations[0].value,
|
||||
familyName: providerUser2Updated.Operations[1].value,
|
||||
email: providerUser2Updated.Operations[2].value,
|
||||
}
|
||||
: {
|
||||
givenName: providerUser2Updated.name.givenName,
|
||||
familyName: providerUser2Updated.name.familyName,
|
||||
email: providerUser2Updated.emails[0].value,
|
||||
};
|
||||
await expect(
|
||||
page.getByRole('cell', {
|
||||
name: givenName,
|
||||
exact: true,
|
||||
})
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: familyName, exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: email, exact: true })).toBeVisible();
|
||||
// Assert webhook logs
|
||||
await dsyncPage.switchToEventsView({ waitForData: true });
|
||||
await dsyncPage.inspectEventRow(0, directory.webhook.endpoint);
|
||||
await expect(page.getByText('"user.updated"')).toBeVisible();
|
||||
await dsyncPage.switchToEventsView({ waitForData: true });
|
||||
await dsyncPage.inspectEventRow(1, directory.webhook.endpoint);
|
||||
await expect(page.getByText('"group.user_added"')).toBeVisible();
|
||||
await dsyncPage.switchToEventsView({ waitForData: true });
|
||||
await dsyncPage.inspectEventRow(2, directory.webhook.endpoint);
|
||||
await expect(page.getByText('"user.created"')).toBeVisible();
|
||||
// Delete webhook logs
|
||||
await dsyncPage.switchToEventsView();
|
||||
await page.getByRole('button', { name: 'Remove Events' }).click();
|
||||
await page.getByTestId('confirm-delete').click();
|
||||
await page.getByRole('table').waitFor({ state: 'detached' });
|
||||
await expect(
|
||||
await page.getByRole('heading', { name: 'No webhook events found for this directory.' })
|
||||
).toBeVisible();
|
||||
await dsyncPage.setWebHookEventsLogging({ enable: false });
|
||||
// User deletion
|
||||
await deleteUser(request, directory, user1.id);
|
||||
await deleteUser(request, directory, user2.id);
|
||||
await dsyncPage.switchToDSyncInfoView();
|
||||
await dsyncPage.switchToUsersView();
|
||||
await expect(page.getByRole('heading', { name: 'No users found for this directory.' })).toBeVisible();
|
||||
await updateGroupName(request, directory, group.id, 'BoxyHQ-updated');
|
||||
await dsyncPage.switchToGroupsView({ waitForData: true });
|
||||
await expect(page.getByRole('cell', { name: 'BoxyHQ-updated' })).toBeVisible();
|
||||
// Group deletion
|
||||
await deleteGroup(request, directory, group.id);
|
||||
await page.reload();
|
||||
await expect(page.getByRole('heading', { name: 'No groups found for this directory.' })).toBeVisible();
|
||||
await dsyncPage.switchToEventsView();
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'No webhook events found for this directory.' })
|
||||
).toBeVisible();
|
||||
});
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
import { test as baseTest, expect } from '@playwright/test';
|
||||
import { Portal, SSOPage } from 'e2e/support/fixtures';
|
||||
|
||||
type MyFixtures = {
|
||||
ssoPage: SSOPage;
|
||||
portal: Portal;
|
||||
};
|
||||
|
||||
export const test = baseTest.extend<MyFixtures>({
|
||||
portal: async ({ page }, use) => {
|
||||
const portal = new Portal(page);
|
||||
await use(portal);
|
||||
},
|
||||
ssoPage: async ({ page, portal }, use) => {
|
||||
const ssoPage = new SSOPage(page);
|
||||
await ssoPage.goto();
|
||||
await use(ssoPage);
|
||||
await portal.doCredentialsLogin();
|
||||
await portal.isLoggedIn();
|
||||
await ssoPage.deleteAllSSOConnections();
|
||||
},
|
||||
});
|
||||
|
||||
test('OAuth2 wrapper + SAML provider + wrong redirectUrl', async ({ ssoPage, page, baseURL }, testInfo) => {
|
||||
const ssoName = `saml-${testInfo.workerIndex}`;
|
||||
await ssoPage.addSSOConnection({ name: ssoName, type: 'saml', baseURL: baseURL! });
|
||||
// check if the first added connection appears in the connection list
|
||||
await expect(page.getByText(`${ssoName}-1`)).toBeVisible();
|
||||
await ssoPage.updateSSOConnection({
|
||||
name: `${ssoName}-1`,
|
||||
url: 'https://invalid-url.com',
|
||||
});
|
||||
// Logout of magic link login
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
// Wait for browser to redirect to error page
|
||||
await page.waitForURL((url) => url.origin === baseURL && url.pathname === '/error');
|
||||
// Assert error text
|
||||
await expect(page.getByText(`SSO error: Redirect URL is not allowed.`)).toBeVisible();
|
||||
});
|
||||
|
||||
test('OAuth2 wrapper + SAML provider + inactive connection', async ({ ssoPage, page, baseURL }, testInfo) => {
|
||||
const ssoName = `saml-${testInfo.workerIndex}`;
|
||||
await ssoPage.addSSOConnection({ name: ssoName, type: 'saml', baseURL: baseURL! });
|
||||
// check if the first added connection appears in the connection list
|
||||
await expect(page.getByText(`${ssoName}-1`)).toBeVisible();
|
||||
await ssoPage.updateSSOConnection({
|
||||
name: `${ssoName}-1`,
|
||||
url: baseURL!,
|
||||
newStatus: false,
|
||||
});
|
||||
// Confirm connection label inactive is displayed
|
||||
await expect(
|
||||
page.getByText(`${ssoName}-1`).locator('xpath=..').getByRole('cell', { name: 'Inactive', exact: true })
|
||||
).toBeVisible();
|
||||
// Logout and try to sign in with connection
|
||||
// Logout of magic link login
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
// Wait for browser to redirect to error page
|
||||
await page.waitForURL((url) => url.origin === baseURL && url.pathname === '/error');
|
||||
// Assert error text
|
||||
await expect(
|
||||
page.getByText('SSO error: SSO connection is deactivated. Please contact your administrator.')
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('OAuth2 wrapper + OIDC provider + wrong redirectUrl', async ({ ssoPage, page, baseURL }, testInfo) => {
|
||||
const ssoName = `oidc-${testInfo.workerIndex}`;
|
||||
await ssoPage.addSSOConnection({ name: ssoName, type: 'oidc', baseURL: baseURL! });
|
||||
// check if the oidc connection appears in the connection list
|
||||
await expect(page.getByText(`${ssoName}-1`)).toBeVisible();
|
||||
await ssoPage.updateSSOConnection({
|
||||
name: `${ssoName}-1`,
|
||||
url: 'https://invalid-url.com',
|
||||
});
|
||||
// Logout of magic link login
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
// Wait for browser to redirect to error page
|
||||
await page.waitForURL((url) => url.origin === baseURL && url.pathname === '/error');
|
||||
// Assert error text
|
||||
await expect(page.getByText('SSO error: Redirect URL is not allowed.')).toBeVisible();
|
||||
});
|
||||
|
||||
test('OAuth2 wrapper + OIDC provider + inactive connection', async ({ ssoPage, page, baseURL }, testInfo) => {
|
||||
const ssoName = `oidc-${testInfo.workerIndex}`;
|
||||
await ssoPage.addSSOConnection({ name: ssoName, type: 'oidc', baseURL: baseURL! });
|
||||
// check if the oidc connection appears in the connection list
|
||||
await expect(page.getByText(`${ssoName}-1`)).toBeVisible();
|
||||
await ssoPage.updateSSOConnection({
|
||||
name: `${ssoName}-1`,
|
||||
url: baseURL!,
|
||||
newStatus: false,
|
||||
});
|
||||
// Confirm connection label inactive is displayed
|
||||
await expect(
|
||||
page.getByText(`${ssoName}-1`).locator('xpath=..').getByRole('cell', { name: 'Inactive', exact: true })
|
||||
).toBeVisible();
|
||||
// Logout and try to sign in with connection
|
||||
// Logout of magic link login
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
// Wait for browser to redirect to error page
|
||||
await page.waitForURL((url) => url.origin === baseURL && url.pathname === '/error');
|
||||
// Assert error text
|
||||
await expect(
|
||||
page.getByText('SSO error: SSO connection is deactivated. Please contact your administrator.')
|
||||
).toBeVisible();
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
import { test as baseTest, expect } from '@playwright/test';
|
||||
import { Portal, SSOPage } from 'e2e/support/fixtures';
|
||||
|
||||
type MyFixtures = {
|
||||
ssoPage: SSOPage;
|
||||
portal: Portal;
|
||||
};
|
||||
|
||||
export const test = baseTest.extend<MyFixtures>({
|
||||
ssoPage: async ({ page, baseURL }, use, testInfo) => {
|
||||
const ssoPage = new SSOPage(page);
|
||||
const ssoName = `oidc-${testInfo.workerIndex}`;
|
||||
await ssoPage.goto();
|
||||
await ssoPage.addSSOConnection({ name: ssoName, type: 'oidc', baseURL: baseURL! });
|
||||
await use(ssoPage);
|
||||
await ssoPage.deleteAllSSOConnections();
|
||||
},
|
||||
portal: async ({ page }, use) => {
|
||||
const portal = new Portal(page);
|
||||
await use(portal);
|
||||
},
|
||||
});
|
||||
|
||||
test('OAuth2 wrapper + OIDC provider', async ({ ssoPage, portal, page, baseURL }, testInfo) => {
|
||||
// check if the first added connection appears in the connection list
|
||||
await expect(page.getByText(`oidc-${testInfo.workerIndex}-1`)).toBeVisible();
|
||||
// Logout of magic link login
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
// Login using MockLab
|
||||
await ssoPage.signInWithMockLab();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
// Assert logged in state
|
||||
await portal.isLoggedIn();
|
||||
});
|
||||
|
||||
test('OAuth2 wrapper + 2 OIDC providers', async ({ ssoPage, portal, page, baseURL }, testInfo) => {
|
||||
const ssoName = `oidc-${testInfo.workerIndex}`;
|
||||
// check if the first added connection appears in the connection list
|
||||
await expect(page.getByText(`${ssoName}-1`)).toBeVisible();
|
||||
// Add second OIDC connection
|
||||
await ssoPage.addSSOConnection({ name: ssoName, type: 'oidc', baseURL: baseURL! });
|
||||
// check if the second added connection appears in the connection list
|
||||
await expect(page.getByText(`${ssoName}-2`)).toBeVisible();
|
||||
// Logout of magic link login
|
||||
await ssoPage.logout();
|
||||
// Login using MockLab
|
||||
await ssoPage.signInWithSSO();
|
||||
// Select IdP from selection screen
|
||||
await ssoPage.selectIdP(`${ssoName}-1`);
|
||||
await ssoPage.signInWithMockLab();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
// Assert logged in state
|
||||
await portal.isLoggedIn();
|
||||
// Logout of magic link login
|
||||
await ssoPage.logout();
|
||||
// Login using MockLab
|
||||
await ssoPage.signInWithSSO();
|
||||
// Select IdP from selection screen
|
||||
await ssoPage.selectIdP(`${ssoName}-2`);
|
||||
await ssoPage.signInWithMockLab();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
// Assert logged in state
|
||||
await portal.isLoggedIn();
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
import { test as baseTest, expect } from '@playwright/test';
|
||||
import { Portal, SSOPage } from 'e2e/support/fixtures';
|
||||
|
||||
type MyFixtures = {
|
||||
ssoPage: SSOPage;
|
||||
portal: Portal;
|
||||
};
|
||||
|
||||
export const test = baseTest.extend<MyFixtures>({
|
||||
ssoPage: async ({ page, baseURL }, use, testInfo) => {
|
||||
const ssoPage = new SSOPage(page);
|
||||
let ssoName = `saml-${testInfo.workerIndex}`;
|
||||
await ssoPage.goto();
|
||||
await ssoPage.addSSOConnection({ name: ssoName, type: 'saml', baseURL: baseURL! });
|
||||
await ssoPage.goto();
|
||||
ssoName = `oidc-${testInfo.workerIndex}`;
|
||||
await ssoPage.addSSOConnection({ name: ssoName, type: 'oidc', baseURL: baseURL! });
|
||||
await use(ssoPage);
|
||||
await ssoPage.deleteAllSSOConnections();
|
||||
},
|
||||
portal: async ({ page }, use) => {
|
||||
const portal = new Portal(page);
|
||||
await use(portal);
|
||||
},
|
||||
});
|
||||
|
||||
test('OAuth2 wrapper + SAML provider + OIDC provider', async ({
|
||||
ssoPage,
|
||||
portal,
|
||||
page,
|
||||
baseURL,
|
||||
}, testInfo) => {
|
||||
// check if the first added connection appears in the connection list
|
||||
await expect(page.getByText(`saml-${testInfo.workerIndex}-1`)).toBeVisible();
|
||||
// check if the second added connection appears in the connection list
|
||||
await expect(page.getByText(`oidc-${testInfo.workerIndex}-2`)).toBeVisible();
|
||||
// Logout of magic link login
|
||||
await ssoPage.logout();
|
||||
// Login using MockSAML
|
||||
await ssoPage.signInWithSSO();
|
||||
// Select IdP from selection screen
|
||||
await ssoPage.selectIdP(`saml-${testInfo.workerIndex}-1`);
|
||||
// Login using MockSAML
|
||||
await ssoPage.signInWithMockSAML();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
// Assert logged in state
|
||||
await portal.isLoggedIn();
|
||||
|
||||
// Logout of SAML login
|
||||
await ssoPage.logout();
|
||||
// Login using MockLab
|
||||
await ssoPage.signInWithSSO();
|
||||
// Select IdP from selection screen
|
||||
await ssoPage.selectIdP(`oidc-${testInfo.workerIndex}-2`);
|
||||
// Login using MockLab
|
||||
await ssoPage.signInWithMockLab();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
// Assert logged in state
|
||||
await portal.isLoggedIn();
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
import { test as baseTest, expect } from '@playwright/test';
|
||||
import { Portal, SSOPage } from 'e2e/support/fixtures';
|
||||
|
||||
type MyFixtures = {
|
||||
ssoPage: SSOPage;
|
||||
portal: Portal;
|
||||
};
|
||||
|
||||
export const test = baseTest.extend<MyFixtures>({
|
||||
ssoPage: async ({ page, baseURL }, use, testInfo) => {
|
||||
const ssoPage = new SSOPage(page);
|
||||
const ssoName = `saml-${testInfo.workerIndex}`;
|
||||
await ssoPage.goto();
|
||||
await ssoPage.addSSOConnection({ name: ssoName, type: 'saml', baseURL: baseURL! });
|
||||
await use(ssoPage);
|
||||
await ssoPage.deleteAllSSOConnections();
|
||||
},
|
||||
portal: async ({ page }, use) => {
|
||||
const portal = new Portal(page);
|
||||
await use(portal);
|
||||
},
|
||||
});
|
||||
|
||||
test('OAuth2 wrapper + SAML provider', async ({ ssoPage, portal, page, baseURL }, testInfo) => {
|
||||
// check if the first added connection appears in the connection list
|
||||
await expect(page.getByText(`saml-${testInfo.workerIndex}-1`)).toBeVisible();
|
||||
// Logout of magic link login
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
// Login using MockSAML
|
||||
await ssoPage.signInWithMockSAML();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
// Assert logged in state
|
||||
await portal.isLoggedIn();
|
||||
});
|
||||
|
||||
test('OAuth2 wrapper + 2 SAML providers', async ({ ssoPage, portal, page, baseURL }, testInfo) => {
|
||||
const ssoName = `saml-${testInfo.workerIndex}`;
|
||||
// check if the first added connection appears in the connection list
|
||||
await expect(page.getByText(`${ssoName}-1`)).toBeVisible();
|
||||
// Add second SAML connection
|
||||
await ssoPage.addSSOConnection({ name: ssoName, type: 'saml', baseURL: baseURL! });
|
||||
// check if the second added connection appears in the connection list
|
||||
await expect(page.getByText(`${ssoName}-2`)).toBeVisible();
|
||||
// Logout of magic link login
|
||||
await ssoPage.logout();
|
||||
// Login using MockSAML
|
||||
await ssoPage.signInWithSSO();
|
||||
// Select IdP from selection screen
|
||||
await ssoPage.selectIdP(`${ssoName}-1`);
|
||||
await ssoPage.signInWithMockSAML();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
// Assert logged in state
|
||||
await portal.isLoggedIn();
|
||||
// Logout of magic link login
|
||||
await ssoPage.logout();
|
||||
// Login using MockSAML
|
||||
await ssoPage.signInWithSSO();
|
||||
// Select IdP from selection screen
|
||||
await ssoPage.selectIdP(`${ssoName}-2`);
|
||||
await ssoPage.signInWithMockSAML();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
// Assert logged in state
|
||||
await portal.isLoggedIn();
|
||||
});
|
|
@ -0,0 +1,116 @@
|
|||
import { expect, test as baseTest } from '@playwright/test';
|
||||
import { Portal, SetupLinkPage } from 'e2e/support/fixtures';
|
||||
|
||||
const TEST_SETUPLINK_MOCKSAML_ORIGIN = process.env.MOCKSAML_ORIGIN || 'https://mocksaml.com';
|
||||
const TEST_SETUPLINK_MOCK_METADATA_URL = `${TEST_SETUPLINK_MOCKSAML_ORIGIN}/api/saml/metadata`;
|
||||
|
||||
const TEST_SETUPLINK_ADMIN_CONNECTION = '/admin/sso-connection';
|
||||
const TENANT = 'acme-setuplink-test.com';
|
||||
const PRODUCT = 'acme-setuplink-test';
|
||||
|
||||
type MyFixtures = {
|
||||
portal: Portal;
|
||||
setuplinkPage: SetupLinkPage;
|
||||
};
|
||||
|
||||
export const test = baseTest.extend<MyFixtures>({
|
||||
portal: async ({ page }, use) => {
|
||||
const portal = new Portal(page);
|
||||
await portal.doCredentialsLogin();
|
||||
await use(portal);
|
||||
},
|
||||
setuplinkPage: async ({ page }, use) => {
|
||||
const setuplinkPage = new SetupLinkPage(page, PRODUCT, TENANT);
|
||||
await use(setuplinkPage);
|
||||
},
|
||||
});
|
||||
|
||||
test.describe('Admin Portal Enterprise SSO SetupLink using generic SAML 2.0', () => {
|
||||
test('should be able to create setup link and sso connection using generic SAML 2.0', async ({
|
||||
page,
|
||||
setuplinkPage,
|
||||
}) => {
|
||||
// Create setup link
|
||||
await setuplinkPage.createSetupLink();
|
||||
|
||||
// get setuplink url
|
||||
const linkContent = await setuplinkPage.getSetupLinkUrl();
|
||||
|
||||
// Open new tab and go to setup link page
|
||||
const context = page.context();
|
||||
const setupLinkPage = await context.newPage();
|
||||
await setupLinkPage.goto(linkContent);
|
||||
|
||||
// Create SSO connection using generic SAML 2.0 workflow
|
||||
await setupLinkPage.getByRole('button', { name: 'Generic SAML 2.0' }).click();
|
||||
|
||||
// check mdx generated content using remart-gfm plugin for step1
|
||||
await expect(setupLinkPage.getByRole('heading', { name: 'Step 1: Configuration SAML' })).toBeVisible();
|
||||
let p1 = await setupLinkPage.getByText('Your Identity Provider (IdP)').textContent();
|
||||
expect(
|
||||
p1 ===
|
||||
'Your Identity Provider (IdP) will ask for the following information while configuring the SAML application.'
|
||||
).toBeTruthy();
|
||||
|
||||
let p2 = await setupLinkPage.getByText('Please do not add a trailing').textContent();
|
||||
expect(p2 === 'Please do not add a trailing slash at the end of the URLs.').toBeTruthy();
|
||||
|
||||
const p3 = await setupLinkPage.getByText('Create them exactly as shown').textContent();
|
||||
expect(p3 === 'Create them exactly as shown below:').toBeTruthy();
|
||||
|
||||
await setupLinkPage.getByRole('button', { name: 'Next Step' }).click();
|
||||
|
||||
// check mdx generated content using remart-gfm plugin for step2
|
||||
await expect(setupLinkPage.getByRole('heading', { name: 'Step 2: SAML Profile/Claims/' })).toBeVisible();
|
||||
p1 = await setupLinkPage.getByText('We try and support 4').textContent();
|
||||
expect(p1 === 'We try and support 4 attributes in the SAML claims:').toBeTruthy();
|
||||
|
||||
p2 = await setupLinkPage.getByText('This is how the common SAML').textContent();
|
||||
expect(
|
||||
p2 ===
|
||||
'This is how the common SAML attributes map over for most providers, but some providers have custom mappings. Please refer to the documentation on Identity Provider to understand the exact mapping.'
|
||||
).toBeTruthy();
|
||||
|
||||
await setupLinkPage.getByRole('button', { name: 'Next Step' }).click();
|
||||
|
||||
// check mdx generated content using remart-gfm plugin for step3
|
||||
await expect(
|
||||
setupLinkPage.getByRole('heading', { name: 'Step 3: Create SAML Connection' })
|
||||
).toBeVisible();
|
||||
p1 = await setupLinkPage.getByText('Enter the Identity Provider').textContent();
|
||||
expect(
|
||||
p1 ===
|
||||
'Enter the Identity Provider Metadata below. You can either enter the metadata URL or paste the XML file content directly.'
|
||||
).toBeTruthy();
|
||||
|
||||
await setupLinkPage
|
||||
.getByPlaceholder('Paste the Metadata URL here')
|
||||
.fill(TEST_SETUPLINK_MOCK_METADATA_URL);
|
||||
await setupLinkPage.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await setupLinkPage.waitForURL(/\/setup\/.+\/sso-connection$/);
|
||||
await expect(setupLinkPage.getByRole('cell', { name: 'saml.example.com' })).toBeVisible();
|
||||
await setupLinkPage.close();
|
||||
|
||||
// Go to connections page
|
||||
await page.goto(TEST_SETUPLINK_ADMIN_CONNECTION);
|
||||
|
||||
// Check if new SSO connection is created
|
||||
await expect(
|
||||
page.getByText(TENANT, { exact: true }),
|
||||
'Failed to create new sso connection from setup-link'
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(PRODUCT, { exact: true }),
|
||||
'Failed to create new sso connection from setup-link'
|
||||
).toBeVisible();
|
||||
|
||||
// Delete the SSO connection
|
||||
await page.getByLabel('Edit').click();
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
|
||||
// remove setup link
|
||||
await setuplinkPage.removeSetupLink();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,199 @@
|
|||
import { test as baseTest, expect } from '@playwright/test';
|
||||
import { Portal, SSOPage } from 'e2e/support/fixtures';
|
||||
|
||||
type MyFixtures = {
|
||||
ssoPage: SSOPage;
|
||||
portal: Portal;
|
||||
};
|
||||
|
||||
let oidcClientId;
|
||||
let oidcClientSecret;
|
||||
|
||||
const test = baseTest.extend<MyFixtures>({
|
||||
ssoPage: async ({ page }, use) => {
|
||||
const ssoPage = new SSOPage(page);
|
||||
await use(ssoPage);
|
||||
// Delete SSO Connections mapped to OIDC federation
|
||||
await ssoPage.deleteSSOConnection('SSO-via-OIDC-Fed');
|
||||
await ssoPage.deleteAllSSOConnections();
|
||||
},
|
||||
portal: async ({ page }, use) => {
|
||||
const portal = new Portal(page);
|
||||
// Create OIDC Federated connection
|
||||
await page.goto('/admin/settings');
|
||||
await page.getByRole('link', { name: 'Apps' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation$/);
|
||||
await page.getByRole('button', { name: 'New App' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation\/new$/);
|
||||
// Toggle connection type to OIDC
|
||||
await page.getByLabel('OIDC').check();
|
||||
await page.getByPlaceholder('Your app').and(page.getByLabel('Name')).fill('OF-1');
|
||||
await page.getByPlaceholder('example.com').and(page.getByLabel('Tenant')).fill('acme.com');
|
||||
await page.getByLabel('Product').fill('_jackson_admin_portal');
|
||||
await page.locator('input[name="item"]').fill('http://localhost:5225');
|
||||
await page.getByRole('button', { name: 'Create App' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation\/.*\/edit$/);
|
||||
oidcClientId = await page
|
||||
.locator('label')
|
||||
.filter({ hasText: 'Client ID' })
|
||||
.getByRole('textbox')
|
||||
.inputValue();
|
||||
oidcClientSecret = await page
|
||||
.locator('label')
|
||||
.filter({ hasText: 'Client Secret' })
|
||||
.getByRole('textbox')
|
||||
.inputValue();
|
||||
await page.getByRole('link', { name: 'Back' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation$/);
|
||||
await expect(page.getByRole('cell', { name: 'OF-1' })).toBeVisible();
|
||||
|
||||
// Add OIDC Connection via OIDC Fed for Admin portal
|
||||
await page.getByRole('link', { name: 'Single Sign-On' }).click();
|
||||
await page.getByTestId('create-connection').click();
|
||||
await page.getByLabel('OIDC').check();
|
||||
await page.getByLabel('Connection name (Optional)').fill('SSO-via-OIDC-Fed');
|
||||
await page.getByLabel('Client ID').fill(oidcClientId);
|
||||
await page.getByLabel('Client Secret').fill(oidcClientSecret);
|
||||
await page
|
||||
.getByLabel('Well-known URL of OpenID Provider')
|
||||
.fill('http://localhost:5225/.well-known/openid-configuration');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(page.getByRole('cell', { name: 'SSO-via-OIDC-Fed' })).toBeVisible();
|
||||
await use(portal);
|
||||
// Delete Saml Fed connection
|
||||
await page.goto('/admin/settings');
|
||||
await page.getByRole('link', { name: 'Apps' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation$/);
|
||||
await page.getByRole('cell', { name: 'Edit' }).getByRole('button').click();
|
||||
await page.getByLabel('Card').getByRole('button', { name: 'Delete' }).click();
|
||||
await page.getByTestId('confirm-delete').click();
|
||||
},
|
||||
});
|
||||
|
||||
test('OIDC Federated app + 1 SAML & 1 OIDC providers', async ({ ssoPage, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'OF-SAML',
|
||||
type: 'saml',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'OF-OIDC',
|
||||
type: 'oidc',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
// Login using MockSAML
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('OF-SAML-1');
|
||||
await ssoPage.signInWithMockSAML();
|
||||
await portal.isLoggedIn();
|
||||
// Login using MockLab
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('OF-OIDC-2');
|
||||
await ssoPage.signInWithMockLab();
|
||||
await portal.isLoggedIn();
|
||||
});
|
||||
|
||||
test('OIDC Federated app + 2 SAML providers', async ({ ssoPage, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'OF-SAML',
|
||||
type: 'saml',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'OF-SAML',
|
||||
type: 'saml',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
|
||||
// Login using MockSAML-1
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('OF-SAML-1');
|
||||
await ssoPage.signInWithMockSAML();
|
||||
await portal.isLoggedIn();
|
||||
// Login using MockSAML-2
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('OF-SAML-2');
|
||||
await ssoPage.signInWithMockSAML();
|
||||
await portal.isLoggedIn();
|
||||
});
|
||||
|
||||
test('OIDC Federated app + 2 OIDC providers', async ({ ssoPage, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'OF-OIDC',
|
||||
type: 'oidc',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'OF-OIDC',
|
||||
type: 'oidc',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
|
||||
// Login using MockLab-1
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('OF-OIDC-1');
|
||||
await ssoPage.signInWithMockLab();
|
||||
await portal.isLoggedIn();
|
||||
// Login using MockLab-2
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('OF-OIDC-2');
|
||||
await ssoPage.signInWithMockLab();
|
||||
await portal.isLoggedIn();
|
||||
});
|
||||
|
||||
test('OIDC Federated app + 1 SAML provider', async ({ ssoPage, page, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'OF-SAML',
|
||||
type: 'saml',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
// Login using MockSAML-1
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.signInWithMockSAML();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
await portal.isLoggedIn();
|
||||
});
|
||||
|
||||
test('OIDC Federated app + 1 OIDC provider', async ({ ssoPage, page, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'OF-OIDC',
|
||||
type: 'oidc',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
// Login using MockLab-1
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.signInWithMockLab();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
await portal.isLoggedIn();
|
||||
});
|
|
@ -0,0 +1,181 @@
|
|||
import { test as baseTest, expect } from '@playwright/test';
|
||||
import { Portal, SSOPage } from 'e2e/support/fixtures';
|
||||
|
||||
type MyFixtures = {
|
||||
ssoPage: SSOPage;
|
||||
portal: Portal;
|
||||
};
|
||||
|
||||
const test = baseTest.extend<MyFixtures>({
|
||||
ssoPage: async ({ page }, use) => {
|
||||
const ssoPage = new SSOPage(page);
|
||||
await use(ssoPage);
|
||||
// Delete SSO Connections mapped to SAML federation
|
||||
await ssoPage.deleteSSOConnection('SSO-via-SAML-Fed');
|
||||
await ssoPage.deleteAllSSOConnections();
|
||||
},
|
||||
portal: async ({ page }, use) => {
|
||||
const portal = new Portal(page);
|
||||
// Create SAML Federated connection
|
||||
await page.goto('/admin/settings');
|
||||
await page.getByRole('link', { name: 'Apps' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation$/);
|
||||
await page.getByRole('button', { name: 'New App' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation\/new$/);
|
||||
await page.getByPlaceholder('Your app').and(page.getByLabel('Name')).fill('SF-1');
|
||||
await page.getByPlaceholder('example.com').and(page.getByLabel('Tenant')).fill('acme.com');
|
||||
await page.getByLabel('Product').fill('_jackson_admin_portal');
|
||||
await page.getByLabel('ACS URL').fill('http://localhost:5225/api/oauth/saml');
|
||||
await page.getByLabel('Entity ID / Audience URI / Audience Restriction').fill('https://saml.boxyhq.com');
|
||||
await page.getByRole('button', { name: 'Create App' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation\/.*\/edit$/);
|
||||
await page.getByRole('link', { name: 'Back' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation$/);
|
||||
await expect(page.getByRole('cell', { name: 'SF-1' })).toBeVisible();
|
||||
// Add SAML connection via SAML Fed for Admin portal
|
||||
await page.getByRole('link', { name: 'Single Sign-On' }).click();
|
||||
await page.getByTestId('create-connection').click();
|
||||
await page.getByLabel('Connection name (Optional)').fill('SSO-via-SAML-Fed');
|
||||
await page
|
||||
.getByPlaceholder('Paste the Metadata URL here')
|
||||
.fill('http://localhost:5225/.well-known/idp-metadata');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(page.getByRole('cell', { name: 'SSO-via-SAML-Fed' })).toBeVisible();
|
||||
await use(portal);
|
||||
// Delete Saml Fed connection
|
||||
await page.goto('/admin/settings');
|
||||
await page.getByRole('link', { name: 'Apps' }).click();
|
||||
await page.waitForURL(/.*admin\/identity-federation$/);
|
||||
await page.getByRole('cell', { name: 'Edit' }).getByRole('button').click();
|
||||
await page.getByLabel('Card').getByRole('button', { name: 'Delete' }).click();
|
||||
await page.getByTestId('confirm-delete').click();
|
||||
},
|
||||
});
|
||||
|
||||
test('SAML Federated app + 1 SAML & 1 OIDC providers', async ({ ssoPage, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'SF-SAML',
|
||||
type: 'saml',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'SF-OIDC',
|
||||
type: 'oidc',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
// Login using MockSAML
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('SF-SAML-1');
|
||||
await ssoPage.signInWithMockSAML();
|
||||
await portal.isLoggedIn();
|
||||
// Login using MockLab
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('SF-OIDC-2');
|
||||
await ssoPage.signInWithMockLab();
|
||||
await portal.isLoggedIn();
|
||||
});
|
||||
|
||||
test('SAML Federated app + 2 SAML providers', async ({ ssoPage, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'SF-SAML',
|
||||
type: 'saml',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'SF-SAML',
|
||||
type: 'saml',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
|
||||
// Login using MockSAML-1
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('SF-SAML-1');
|
||||
await ssoPage.signInWithMockSAML();
|
||||
await portal.isLoggedIn();
|
||||
// Login using MockSAML-2
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('SF-SAML-2');
|
||||
await ssoPage.signInWithMockSAML();
|
||||
await portal.isLoggedIn();
|
||||
});
|
||||
|
||||
test('SAML Federated app + 2 OIDC providers', async ({ ssoPage, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'SF-OIDC',
|
||||
type: 'oidc',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'SF-OIDC',
|
||||
type: 'oidc',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
|
||||
// Login using MockLab-1
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('SF-OIDC-1');
|
||||
await ssoPage.signInWithMockLab();
|
||||
await portal.isLoggedIn();
|
||||
// Login using MockLab-2
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.selectIdP('SF-OIDC-2');
|
||||
await ssoPage.signInWithMockLab();
|
||||
await portal.isLoggedIn();
|
||||
});
|
||||
|
||||
test('SAML Federated app + 1 SAML provider', async ({ ssoPage, page, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'SF-SAML',
|
||||
type: 'saml',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
// Login using MockSAML-1
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.signInWithMockSAML();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
await portal.isLoggedIn();
|
||||
});
|
||||
|
||||
test('SAML Federated app + 1 OIDC provider', async ({ ssoPage, page, portal, baseURL }) => {
|
||||
// Add SSO connection for tenants
|
||||
await ssoPage.addSSOConnection({
|
||||
name: 'SF-OIDC',
|
||||
type: 'oidc',
|
||||
baseURL: baseURL!,
|
||||
tenant: 'acme.com',
|
||||
product: '_jackson_admin_portal',
|
||||
});
|
||||
// Login using MockLab-1
|
||||
await ssoPage.logout();
|
||||
await ssoPage.signInWithSSO();
|
||||
await ssoPage.signInWithMockLab();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
await portal.isLoggedIn();
|
||||
});
|
|
@ -19,70 +19,86 @@ const MOCKLAB_CLIENT_SECRET = 'mocklab_secret';
|
|||
const MOCKLAB_SIGNIN_BUTTON_NAME = 'Login';
|
||||
|
||||
test.describe('Admin Portal SSO - SAML', () => {
|
||||
test('should be able to add SSO connection to mocksaml', async ({ page }) => {
|
||||
await page.goto('/admin/settings');
|
||||
// Find the new connection button and click on it
|
||||
await page.getByTestId('create-connection').click();
|
||||
// Fill the name for the connection
|
||||
const nameInput = page.getByLabel('Connection name (Optional)');
|
||||
await nameInput.fill(TEST_SAML_SSO_CONNECTION_NAME);
|
||||
// Enter the metadata url for mocksaml in the form
|
||||
const metadataUrlInput = page.getByLabel('Metadata URL');
|
||||
await metadataUrlInput.fill(MOCKSAML_METADATA_URL);
|
||||
// submit the form
|
||||
await page.getByRole('button', { name: /save/i }).click();
|
||||
// check if the added connection appears in the connection list
|
||||
await expect(page.getByText(TEST_SAML_SSO_CONNECTION_NAME)).toBeVisible();
|
||||
});
|
||||
const samlMetadataMode = ['metadataUrl', 'rawMetadata'];
|
||||
for (const mode of samlMetadataMode) {
|
||||
test.describe(`SSO Connection via ${mode}`, () => {
|
||||
test('should be able to add SSO connection to mocksaml', async ({ page }) => {
|
||||
await page.goto('/admin/settings');
|
||||
// Find the new connection button and click on it
|
||||
await page.getByTestId('create-connection').click();
|
||||
// Fill the name for the connection
|
||||
const nameInput = page.getByLabel('Connection name (Optional)');
|
||||
await nameInput.fill(TEST_SAML_SSO_CONNECTION_NAME);
|
||||
if (mode === 'metadataUrl') {
|
||||
// Enter the metadata url for mocksaml in the form
|
||||
const metadataUrlInput = page.getByLabel('Metadata URL');
|
||||
await metadataUrlInput.fill(MOCKSAML_METADATA_URL);
|
||||
} else {
|
||||
// Enter the raw metadata
|
||||
const rawMetadataInput = page.getByLabel('Raw IdP XML');
|
||||
await rawMetadataInput.fill(process.env.MOCKSAML_METADATA!);
|
||||
}
|
||||
// submit the form
|
||||
await page.getByRole('button', { name: /save/i }).click();
|
||||
// check if the added connection appears in the connection list
|
||||
await expect(page.getByText(TEST_SAML_SSO_CONNECTION_NAME)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should be able to login with mocksaml via SP initiated SSO', async ({ page, baseURL }) => {
|
||||
const userAvatarLocator = page.getByTestId('user-avatar');
|
||||
// Logout from the magic link authentication
|
||||
await page.goto('/');
|
||||
await userAvatarLocator.click();
|
||||
await page.getByTestId('logout').click();
|
||||
// Click on login with sso button
|
||||
await page.getByTestId('sso-login-button').click();
|
||||
// Perform sign in at mocksaml
|
||||
await page.waitForURL((url) => url.origin === MOCKSAML_ORIGIN);
|
||||
await page.getByPlaceholder('jackson').fill('bob');
|
||||
await page.getByRole('button', { name: MOCKSAML_SIGNIN_BUTTON_NAME }).click();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
// assert login state
|
||||
await expect(userAvatarLocator).toBeVisible();
|
||||
});
|
||||
test('should be able to login with mocksaml via SP initiated SSO', async ({ page, baseURL }) => {
|
||||
const userAvatarLocator = page.getByTestId('user-avatar');
|
||||
// Logout from the magic link authentication
|
||||
await page.goto('/');
|
||||
await userAvatarLocator.click();
|
||||
await page.getByTestId('logout').click();
|
||||
// Click on login with sso button
|
||||
await page.getByTestId('sso-login-button').click();
|
||||
// Perform sign in at mocksaml
|
||||
await page.waitForURL((url) => url.origin === MOCKSAML_ORIGIN);
|
||||
await page.getByPlaceholder('jackson').fill('bob');
|
||||
await page.getByRole('button', { name: MOCKSAML_SIGNIN_BUTTON_NAME }).click();
|
||||
// Wait for browser to redirect back to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
// assert login state
|
||||
await expect(userAvatarLocator).toBeVisible();
|
||||
});
|
||||
|
||||
test('should be able to login with mocksaml via IdP initiated SSO', async ({ page, baseURL }) => {
|
||||
const userAvatarLocator = page.getByTestId('user-avatar');
|
||||
// Logout from the magic link authentication
|
||||
await page.goto('/');
|
||||
await userAvatarLocator.click();
|
||||
await page.getByTestId('logout').click();
|
||||
await expect(page.getByTestId('sso-login-button')).toBeVisible();
|
||||
// Go directly to mocksaml hosting
|
||||
await page.goto(MOCKSAML_ORIGIN);
|
||||
await page.getByRole('link', { name: 'Test IdP Login' }).click();
|
||||
await page.getByPlaceholder('https://sso.eu.boxyhq.com/api/oauth/saml').fill(`${baseURL}/api/oauth/saml`);
|
||||
await page.getByRole('textbox', { name: 'Please provide a mock email address' }).fill('bob');
|
||||
await page.getByRole('button', { name: MOCKSAML_SIGNIN_BUTTON_NAME }).click();
|
||||
// Wait for browser to redirect to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
// assert login state
|
||||
await expect(userAvatarLocator).toBeVisible();
|
||||
});
|
||||
test('should be able to login with mocksaml via IdP initiated SSO', async ({ page, baseURL }) => {
|
||||
const userAvatarLocator = page.getByTestId('user-avatar');
|
||||
// Logout from the magic link authentication
|
||||
await page.goto('/');
|
||||
await userAvatarLocator.click();
|
||||
await page.getByTestId('logout').click();
|
||||
await expect(page.getByTestId('sso-login-button')).toBeVisible();
|
||||
// Go directly to mocksaml hosting
|
||||
await page.goto(MOCKSAML_ORIGIN);
|
||||
await page.getByRole('link', { name: 'Test IdP Login' }).click();
|
||||
await page
|
||||
.getByPlaceholder('https://sso.eu.boxyhq.com/api/oauth/saml')
|
||||
.fill(`${baseURL}/api/oauth/saml`);
|
||||
await page.getByRole('textbox', { name: 'Please provide a mock email address' }).fill('bob');
|
||||
await page.getByRole('button', { name: MOCKSAML_SIGNIN_BUTTON_NAME }).click();
|
||||
// Wait for browser to redirect to admin portal
|
||||
await page.waitForURL((url) => url.origin === baseURL);
|
||||
// assert login state
|
||||
await expect(userAvatarLocator).toBeVisible();
|
||||
});
|
||||
|
||||
test('delete the SAML SSO connection', async ({ page }) => {
|
||||
await page.goto('/admin/settings');
|
||||
// select the row of the connection list table, then locate the edit button
|
||||
const editButton = page.getByText(TEST_SAML_SSO_CONNECTION_NAME).locator('..').getByLabel('Edit');
|
||||
await editButton.click();
|
||||
// click the delete and confirm deletion
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
// check that the SSO connection is deleted from the connection list
|
||||
await expect(page.getByText(TEST_SAML_SSO_CONNECTION_NAME)).not.toBeVisible();
|
||||
});
|
||||
test('delete the SAML SSO connection', async ({ page }) => {
|
||||
await page.goto('/admin/settings');
|
||||
// select the row of the connection list table, then locate the edit button
|
||||
const editButton = page
|
||||
.getByText(TEST_SAML_SSO_CONNECTION_NAME)
|
||||
.locator('xpath=..')
|
||||
.getByLabel('Edit');
|
||||
await editButton.click();
|
||||
// click the delete and confirm deletion
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
// check that the SSO connection is deleted from the connection list
|
||||
await expect(page.getByText(TEST_SAML_SSO_CONNECTION_NAME)).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('Admin Portal SSO - OIDC', () => {
|
||||
|
@ -138,7 +154,7 @@ test.describe('Admin Portal SSO - OIDC', () => {
|
|||
await page.getByTestId('logout').click();
|
||||
// Click on login with sso button
|
||||
await page.getByTestId('sso-login-button').click();
|
||||
// Perform sign in at mocksaml
|
||||
// Perform sign in at mocklab
|
||||
await page.waitForURL((url) => url.origin === MOCKLAB_ORIGIN);
|
||||
await page.getByPlaceholder('yours@example.com').fill('bob@oidc.com');
|
||||
await page.getByRole('button', { name: MOCKLAB_SIGNIN_BUTTON_NAME }).click();
|
||||
|
|
|
@ -136,7 +136,12 @@ const Branding = ({ hasValidLicense }: { hasValidLicense: boolean }) => {
|
|||
<label className='label'>
|
||||
<span className='label-text'>{t('bui-shared-primary-color')}</span>
|
||||
</label>
|
||||
<input type='color' id='primaryColor' onChange={onChange} value={branding.primaryColor || ''} />
|
||||
<input
|
||||
type='color'
|
||||
id='primaryColor'
|
||||
onChange={onChange}
|
||||
value={branding.primaryColor || '#25c2a0'}
|
||||
/>
|
||||
<label className='label'>
|
||||
<span className='label-text-alt'>{t('bui-shared-primary-color-desc')}</span>
|
||||
</label>
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
import { AppRequestParams } from '@boxyhq/saml-jackson';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import jackson from '@lib/jackson';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
switch (req.method) {
|
||||
case 'POST':
|
||||
await handlePOST(req, res);
|
||||
break;
|
||||
case 'GET':
|
||||
await handleGET(req, res);
|
||||
break;
|
||||
case 'PATCH':
|
||||
await handlePATCH(req, res);
|
||||
break;
|
||||
case 'DELETE':
|
||||
await handleDELETE(req, res);
|
||||
break;
|
||||
default:
|
||||
res.setHeader('Allow', 'POST, GET, PATCH, DELETE');
|
||||
res.status(405).json({ error: { message: `Method ${req.method} Not Allowed` } });
|
||||
}
|
||||
} catch (error: any) {
|
||||
const { message, statusCode = 500 } = error;
|
||||
res.status(statusCode).json({ error: { message } });
|
||||
}
|
||||
}
|
||||
|
||||
// Create a SAML federated app
|
||||
const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { samlFederatedController } = await jackson();
|
||||
|
||||
const app = await samlFederatedController.app.create(req.body);
|
||||
|
||||
res.status(201).json({ data: app });
|
||||
};
|
||||
|
||||
// Get a SAML federated app by ID
|
||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { samlFederatedController } = await jackson();
|
||||
|
||||
const app = await samlFederatedController.app.get(req.query as AppRequestParams);
|
||||
|
||||
res.json({ data: app });
|
||||
};
|
||||
|
||||
// Update a SAML federated app
|
||||
const handlePATCH = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { samlFederatedController } = await jackson();
|
||||
|
||||
const app = await samlFederatedController.app.update(req.body);
|
||||
|
||||
res.json({ data: app });
|
||||
};
|
||||
|
||||
// Delete a SAML federated app
|
||||
const handleDELETE = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { samlFederatedController } = await jackson();
|
||||
|
||||
await samlFederatedController.app.delete(req.query as AppRequestParams);
|
||||
|
||||
res.json({ data: {} });
|
||||
};
|
|
@ -13,12 +13,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
|
||||
// Get Identity Federation app by id
|
||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { samlFederatedController } = await jackson();
|
||||
const { identityFederationController } = await jackson();
|
||||
|
||||
const { id } = req.query as { id: string };
|
||||
|
||||
const app = await samlFederatedController.app.get({ id });
|
||||
const metadata = await samlFederatedController.app.getMetadata();
|
||||
const app = await identityFederationController.app.get({ id });
|
||||
const metadata = await identityFederationController.app.getMetadata();
|
||||
|
||||
res.json({
|
||||
data: {
|
||||
|
@ -30,20 +30,20 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
|
||||
// Update Identity Federation app
|
||||
const handlePATCH = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { samlFederatedController } = await jackson();
|
||||
const { identityFederationController } = await jackson();
|
||||
|
||||
const updatedApp = await samlFederatedController.app.update(req.body);
|
||||
const updatedApp = await identityFederationController.app.update(req.body);
|
||||
|
||||
res.json({ data: updatedApp });
|
||||
};
|
||||
|
||||
// Delete the Identity Federation app
|
||||
const handleDELETE = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { samlFederatedController } = await jackson();
|
||||
const { identityFederationController } = await jackson();
|
||||
|
||||
const { id } = req.query as { id: string };
|
||||
|
||||
await samlFederatedController.app.delete({ id });
|
||||
await identityFederationController.app.delete({ id });
|
||||
|
||||
res.json({ data: null });
|
||||
};
|
|
@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||
import jackson from '@lib/jackson';
|
||||
import { defaultHandler } from '@lib/api';
|
||||
import { parsePaginateApiParams } from '@lib/utils';
|
||||
import { validateDevelopmentModeLimits } from '@lib/development-mode';
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await defaultHandler(req, res, {
|
||||
|
@ -13,20 +14,26 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
|
||||
// Create new Identity Federation app
|
||||
const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { samlFederatedController } = await jackson();
|
||||
const { identityFederationController } = await jackson();
|
||||
|
||||
const app = await samlFederatedController.app.create(req.body);
|
||||
await validateDevelopmentModeLimits(
|
||||
req.body.product,
|
||||
'identityFederation',
|
||||
'Maximum number of federation apps reached'
|
||||
);
|
||||
|
||||
const app = await identityFederationController.app.create(req.body);
|
||||
|
||||
res.status(201).json({ data: app });
|
||||
};
|
||||
|
||||
// Get Identity Federation apps
|
||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { samlFederatedController } = await jackson();
|
||||
const { identityFederationController } = await jackson();
|
||||
|
||||
const { pageOffset, pageLimit, pageToken } = parsePaginateApiParams(req.query);
|
||||
|
||||
const apps = await samlFederatedController.app.getAll({
|
||||
const apps = await identityFederationController.app.getAll({
|
||||
pageOffset,
|
||||
pageLimit,
|
||||
pageToken,
|
|
@ -3,29 +3,24 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||
import jackson from '@lib/jackson';
|
||||
import stream from 'stream';
|
||||
import { promisify } from 'util';
|
||||
import { defaultHandler } from '@lib/api';
|
||||
|
||||
const pipeline = promisify(stream.pipeline);
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { method } = req;
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return await handleGET(req, res);
|
||||
default:
|
||||
res.setHeader('Allow', 'GET');
|
||||
res.status(405).json({ error: { message: `Method ${method} Not Allowed` } });
|
||||
}
|
||||
await defaultHandler(req, res, {
|
||||
GET: handleGET,
|
||||
});
|
||||
}
|
||||
|
||||
// Display the metadata for the SAML federation
|
||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { samlFederatedController } = await jackson();
|
||||
const { identityFederationController } = await jackson();
|
||||
|
||||
const { download } = req.query as { download: any };
|
||||
|
||||
try {
|
||||
const metadata = await samlFederatedController.app.getMetadata();
|
||||
const metadata = await identityFederationController.app.getMetadata();
|
||||
|
||||
res.setHeader('Content-type', 'text/xml');
|
||||
|
|
@ -65,9 +65,9 @@ async function handleSAMLRequest(req: NextApiRequest, res: NextApiResponse, bind
|
|||
throw new Error('SAMLRequest is required to proceed.');
|
||||
}
|
||||
|
||||
const { samlFederatedController } = await jackson();
|
||||
const { identityFederationController } = await jackson();
|
||||
|
||||
const response = await samlFederatedController.sso.getAuthorizeUrl({
|
||||
const response = await identityFederationController.sso.getAuthorizeUrl({
|
||||
request: samlRequest,
|
||||
relayState,
|
||||
idp_hint: idpHint,
|
|
@ -0,0 +1,57 @@
|
|||
import { AppRequestParams } from '@boxyhq/saml-jackson';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import jackson from '@lib/jackson';
|
||||
import { validateDevelopmentModeLimits } from '@lib/development-mode';
|
||||
import { defaultHandler } from '@lib/api';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
await defaultHandler(req, res, {
|
||||
POST: handlePOST,
|
||||
GET: handleGET,
|
||||
PATCH: handlePATCH,
|
||||
DELETE: handleDELETE,
|
||||
});
|
||||
}
|
||||
|
||||
// Create a SAML federated app
|
||||
const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { identityFederationController } = await jackson();
|
||||
|
||||
await validateDevelopmentModeLimits(
|
||||
req.body.product,
|
||||
'identityFederation',
|
||||
'Maximum number of federation apps reached'
|
||||
);
|
||||
|
||||
const app = await identityFederationController.app.create(req.body);
|
||||
|
||||
res.status(201).json({ data: app });
|
||||
};
|
||||
|
||||
// Get a SAML federated app by ID
|
||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { identityFederationController } = await jackson();
|
||||
|
||||
const app = await identityFederationController.app.get(req.query as AppRequestParams);
|
||||
|
||||
res.json({ data: app });
|
||||
};
|
||||
|
||||
// Update a SAML federated app
|
||||
const handlePATCH = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { identityFederationController } = await jackson();
|
||||
|
||||
const app = await identityFederationController.app.update(req.body);
|
||||
|
||||
res.json({ data: app });
|
||||
};
|
||||
|
||||
// Delete a SAML federated app
|
||||
const handleDELETE = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { identityFederationController } = await jackson();
|
||||
|
||||
await identityFederationController.app.delete(req.query as AppRequestParams);
|
||||
|
||||
res.json({ data: {} });
|
||||
};
|
|
@ -2,26 +2,17 @@ import { NextApiRequest, NextApiResponse } from 'next';
|
|||
|
||||
import jackson from '@lib/jackson';
|
||||
import { parsePaginateApiParams } from '@lib/utils';
|
||||
import { defaultHandler } from '@lib/api';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
switch (req.method) {
|
||||
case 'GET':
|
||||
await handleGET(req, res);
|
||||
break;
|
||||
default:
|
||||
res.setHeader('Allow', 'GET');
|
||||
res.status(405).json({ error: { message: `Method ${req.method} Not Allowed` } });
|
||||
}
|
||||
} catch (error: any) {
|
||||
const { message, statusCode = 500 } = error;
|
||||
res.status(statusCode).json({ error: { message } });
|
||||
}
|
||||
await defaultHandler(req, res, {
|
||||
GET: handleGET,
|
||||
});
|
||||
}
|
||||
|
||||
// Get SAML federated apps filtered by the product
|
||||
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { samlFederatedController } = await jackson();
|
||||
const { identityFederationController } = await jackson();
|
||||
|
||||
const { product } = req.query as {
|
||||
product: string;
|
||||
|
@ -29,7 +20,7 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
|
||||
const { pageOffset, pageLimit, pageToken } = parsePaginateApiParams(req.query);
|
||||
|
||||
const apps = await samlFederatedController.app.getByProduct({
|
||||
const apps = await identityFederationController.app.getByProduct({
|
||||
product,
|
||||
pageOffset,
|
||||
pageLimit,
|
|
@ -2,7 +2,7 @@ import { useRouter } from 'next/router';
|
|||
import { useTranslation } from 'next-i18next';
|
||||
import { errorToast, successToast } from '@components/Toaster';
|
||||
import LicenseRequired from '@components/LicenseRequired';
|
||||
import { EditFederatedSAMLApp, LinkBack } from '@boxyhq/internal-ui';
|
||||
import { EditIdentityFederationApp, LinkBack } from '@boxyhq/internal-ui';
|
||||
|
||||
import 'react-tagsinput/react-tagsinput.css';
|
||||
|
||||
|
@ -18,19 +18,19 @@ const UpdateApp = ({ hasValidLicense }: { hasValidLicense: boolean }) => {
|
|||
|
||||
return (
|
||||
<div className='space-y-4'>
|
||||
<LinkBack href='/admin/federated-saml' />
|
||||
<EditFederatedSAMLApp
|
||||
<LinkBack href='/admin/identity-federation' />
|
||||
<EditIdentityFederationApp
|
||||
urls={{
|
||||
getApp: `/api/admin/federated-saml/${id}`,
|
||||
updateApp: `/api/admin/federated-saml/${id}`,
|
||||
deleteApp: `/api/admin/federated-saml/${id}`,
|
||||
getApp: `/api/admin/identity-federation/${id}`,
|
||||
updateApp: `/api/admin/identity-federation/${id}`,
|
||||
deleteApp: `/api/admin/identity-federation/${id}`,
|
||||
}}
|
||||
onUpdate={() => {
|
||||
successToast(t('saml_federation_update_success'));
|
||||
successToast(t('identity_federation_update_success'));
|
||||
}}
|
||||
onDelete={() => {
|
||||
successToast(t('saml_federation_delete_success'));
|
||||
router.push('/admin/federated-saml');
|
||||
successToast(t('identity_federation_delete_success'));
|
||||
router.push('/admin/identity-federation');
|
||||
}}
|
||||
onError={(error) => {
|
||||
errorToast(error.message);
|
|
@ -1,6 +1,6 @@
|
|||
import router from 'next/router';
|
||||
import LicenseRequired from '@components/LicenseRequired';
|
||||
import { FederatedSAMLApps } from '@boxyhq/internal-ui';
|
||||
import { IdentityFederationApps } from '@boxyhq/internal-ui';
|
||||
|
||||
const AppsList = ({ hasValidLicense }: { hasValidLicense: boolean }) => {
|
||||
if (!hasValidLicense) {
|
||||
|
@ -8,11 +8,11 @@ const AppsList = ({ hasValidLicense }: { hasValidLicense: boolean }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<FederatedSAMLApps
|
||||
urls={{ getApps: '/api/admin/federated-saml' }}
|
||||
onEdit={(app) => router.push(`/admin/federated-saml/${app.id}/edit`)}
|
||||
<IdentityFederationApps
|
||||
urls={{ getApps: '/api/admin/identity-federation' }}
|
||||
onEdit={(app) => router.push(`/admin/identity-federation/${app.id}/edit`)}
|
||||
actions={{
|
||||
newApp: '/admin/federated-saml/new',
|
||||
newApp: '/admin/identity-federation/new',
|
||||
samlConfiguration: '/.well-known/idp-configuration',
|
||||
oidcConfiguration: '/.well-known/openid-configuration',
|
||||
}}
|
|
@ -1,12 +1,12 @@
|
|||
import Link from 'next/link';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { SAMLFederationAppWithMetadata } from '@boxyhq/saml-jackson';
|
||||
import type { IdentityFederationAppWithMetadata } from '@boxyhq/saml-jackson';
|
||||
import { Toaster } from '@components/Toaster';
|
||||
import { InputWithCopyButton, CopyToClipboardButton, LinkOutline } from '@boxyhq/internal-ui';
|
||||
import LicenseRequired from '@components/LicenseRequired';
|
||||
|
||||
type MetadataProps = {
|
||||
metadata: Pick<SAMLFederationAppWithMetadata, 'metadata'>['metadata'];
|
||||
metadata: Pick<IdentityFederationAppWithMetadata, 'metadata'>['metadata'];
|
||||
hasValidLicense: boolean;
|
||||
};
|
||||
|
|
@ -2,7 +2,7 @@ import { useRouter } from 'next/router';
|
|||
import { useTranslation } from 'next-i18next';
|
||||
import { errorToast, successToast } from '@components/Toaster';
|
||||
import LicenseRequired from '@components/LicenseRequired';
|
||||
import { NewFederatedSAMLApp, LinkBack } from '@boxyhq/internal-ui';
|
||||
import { NewIdentityFederationApp, LinkBack } from '@boxyhq/internal-ui';
|
||||
|
||||
import 'react-tagsinput/react-tagsinput.css';
|
||||
|
||||
|
@ -16,12 +16,12 @@ const NewApp = ({ hasValidLicense, samlAudience }: { hasValidLicense: boolean; s
|
|||
|
||||
return (
|
||||
<div className='space-y-4'>
|
||||
<LinkBack href='/admin/federated-saml' />
|
||||
<NewFederatedSAMLApp
|
||||
urls={{ createApp: '/api/admin/federated-saml' }}
|
||||
<LinkBack href='/admin/identity-federation' />
|
||||
<NewIdentityFederationApp
|
||||
urls={{ createApp: '/api/admin/identity-federation' }}
|
||||
onSuccess={(data) => {
|
||||
successToast(t('saml_federation_new_success'));
|
||||
router.replace(`/admin/federated-saml/${data.id}/edit`);
|
||||
successToast(t('identity_federation_new_success'));
|
||||
router.replace(`/admin/identity-federation/${data.id}/edit`);
|
||||
}}
|
||||
onError={(error) => {
|
||||
errorToast(error.message);
|
File diff suppressed because it is too large
Load Diff
|
@ -26,21 +26,21 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-typescript": "11.1.6",
|
||||
"@types/node": "20.12.7",
|
||||
"@types/react": "18.2.79",
|
||||
"@typescript-eslint/eslint-plugin": "7.7.1",
|
||||
"@typescript-eslint/parser": "7.7.1",
|
||||
"@vitejs/plugin-react": "4.2.1",
|
||||
"@types/node": "20.12.12",
|
||||
"@types/react": "18.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "7.11.0",
|
||||
"@typescript-eslint/parser": "7.11.0",
|
||||
"@vitejs/plugin-react": "4.3.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-react-refresh": "0.4.6",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-react-refresh": "0.4.7",
|
||||
"prettier": "3.2.5",
|
||||
"react-daisyui": "5.0.0",
|
||||
"typescript": "5.4.5",
|
||||
"vite": "5.2.10"
|
||||
"vite": "5.2.11"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-x64-gnu": "4.16.3"
|
||||
"@rollup/rollup-linux-x64-gnu": "4.18.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@boxyhq/react-ui": ">=3.3.42",
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
export { NewFederatedSAMLApp } from './NewFederatedSAMLApp';
|
||||
export { EditFederatedSAMLApp } from './EditFederatedSAMLApp';
|
||||
export { FederatedSAMLApps } from './FederatedSAMLApps';
|
|
@ -1,3 +1,4 @@
|
|||
export { usePaginate } from './usePaginate';
|
||||
export { useDirectory } from './useDirectory';
|
||||
export { useRouter } from './useRouter';
|
||||
export { useFetch } from './useFetch';
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
type RefetchFunction = () => void;
|
||||
|
||||
async function parseResponseContent(response: Response) {
|
||||
const responseText = await response.text();
|
||||
|
||||
try {
|
||||
return responseText.length ? JSON.parse(responseText) : '';
|
||||
} catch (err) {
|
||||
return responseText;
|
||||
}
|
||||
}
|
||||
|
||||
export function useFetch<T>({ url }: { url: string }): {
|
||||
data?: T;
|
||||
isLoading: boolean;
|
||||
error: any;
|
||||
refetch: RefetchFunction;
|
||||
} {
|
||||
const [data, setData] = useState<T>();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<any>(null);
|
||||
const [refetchIndex, setRefetchIndex] = useState<number>(0);
|
||||
|
||||
const refetch = () => setRefetchIndex((prevRefetchIndex) => prevRefetchIndex + 1);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
setIsLoading(true);
|
||||
const res = await fetch(url);
|
||||
setIsLoading(false);
|
||||
const resContent = await parseResponseContent(res);
|
||||
|
||||
if (res.ok) {
|
||||
const pageToken = res.headers.get('jackson-pagetoken');
|
||||
if (pageToken !== null) {
|
||||
setData({ ...resContent, pageToken });
|
||||
} else {
|
||||
setData(resContent);
|
||||
}
|
||||
} else {
|
||||
setError(resContent.error);
|
||||
}
|
||||
}
|
||||
fetchData();
|
||||
}, [url, refetchIndex]);
|
||||
|
||||
return { data, isLoading, error, refetch };
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from 'react';
|
||||
import { Button } from 'react-daisyui';
|
||||
import type { SAMLFederationApp } from '../types';
|
||||
import type { IdentityFederationApp } from '../types';
|
||||
import TagsInput from 'react-tagsinput';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useFormik } from 'formik';
|
||||
|
@ -13,7 +13,7 @@ import { ItemList } from '@boxyhq/react-ui/shared';
|
|||
import { CopyToClipboardButton } from '../shared/InputWithCopyButton';
|
||||
import { IconButton } from '../shared/IconButton';
|
||||
|
||||
type EditApp = Pick<SAMLFederationApp, 'name' | 'acsUrl' | 'tenants' | 'redirectUrl'>;
|
||||
type EditApp = Pick<IdentityFederationApp, 'name' | 'acsUrl' | 'tenants' | 'redirectUrl'>;
|
||||
|
||||
export const Edit = ({
|
||||
app,
|
||||
|
@ -22,9 +22,9 @@ export const Edit = ({
|
|||
onUpdate,
|
||||
excludeFields,
|
||||
}: {
|
||||
app: SAMLFederationApp;
|
||||
app: IdentityFederationApp;
|
||||
urls: { patch: string };
|
||||
onUpdate?: (data: SAMLFederationApp) => void;
|
||||
onUpdate?: (data: IdentityFederationApp) => void;
|
||||
onError?: (error: Error) => void;
|
||||
excludeFields?: 'product'[];
|
||||
}) => {
|
|
@ -1,12 +1,12 @@
|
|||
import { Button } from 'react-daisyui';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { SAMLFederationApp } from '../types';
|
||||
import type { IdentityFederationApp } from '../types';
|
||||
import { useFormik } from 'formik';
|
||||
import { defaultHeaders } from '../utils';
|
||||
import { Card } from '../shared';
|
||||
import { AttributesMapping } from './AttributesMapping';
|
||||
|
||||
type Mappings = Pick<SAMLFederationApp, 'mappings'>;
|
||||
type Mappings = Pick<IdentityFederationApp, 'mappings'>;
|
||||
|
||||
export const EditAttributesMapping = ({
|
||||
app,
|
||||
|
@ -14,9 +14,9 @@ export const EditAttributesMapping = ({
|
|||
onUpdate,
|
||||
onError,
|
||||
}: {
|
||||
app: SAMLFederationApp;
|
||||
app: IdentityFederationApp;
|
||||
urls: { patch: string };
|
||||
onUpdate?: (data: SAMLFederationApp) => void;
|
||||
onUpdate?: (data: IdentityFederationApp) => void;
|
||||
onError?: (error: Error) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation('common');
|
|
@ -1,11 +1,11 @@
|
|||
import { Button } from 'react-daisyui';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { SAMLFederationApp } from '../types';
|
||||
import type { IdentityFederationApp } from '../types';
|
||||
import { useFormik } from 'formik';
|
||||
import { defaultHeaders } from '../utils';
|
||||
import { Card } from '../shared';
|
||||
|
||||
type Branding = Pick<SAMLFederationApp, 'logoUrl' | 'faviconUrl' | 'primaryColor'>;
|
||||
type Branding = Pick<IdentityFederationApp, 'logoUrl' | 'faviconUrl' | 'primaryColor'>;
|
||||
|
||||
export const EditBranding = ({
|
||||
app,
|
||||
|
@ -13,9 +13,9 @@ export const EditBranding = ({
|
|||
onUpdate,
|
||||
onError,
|
||||
}: {
|
||||
app: SAMLFederationApp;
|
||||
app: IdentityFederationApp;
|
||||
urls: { patch: string };
|
||||
onUpdate?: (data: SAMLFederationApp) => void;
|
||||
onUpdate?: (data: IdentityFederationApp) => void;
|
||||
onError?: (error: Error) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation('common');
|
|
@ -1,5 +1,5 @@
|
|||
import useSWR from 'swr';
|
||||
import type { SAMLFederationApp } from '../types';
|
||||
import type { IdentityFederationApp } from '../types';
|
||||
import { EditBranding } from './EditBranding';
|
||||
import { Edit } from './Edit';
|
||||
import { EditAttributesMapping } from './EditAttributesMapping';
|
||||
|
@ -9,7 +9,7 @@ import { useEffect, useState } from 'react';
|
|||
import { defaultHeaders, fetcher } from '../utils';
|
||||
import { PageHeader } from '../shared';
|
||||
|
||||
export const EditFederatedSAMLApp = ({
|
||||
export const EditIdentityFederationApp = ({
|
||||
urls,
|
||||
onError,
|
||||
onUpdate,
|
||||
|
@ -17,7 +17,7 @@ export const EditFederatedSAMLApp = ({
|
|||
excludeFields,
|
||||
}: {
|
||||
urls: { getApp: string; updateApp: string; deleteApp: string };
|
||||
onUpdate?: (data: SAMLFederationApp) => void;
|
||||
onUpdate?: (data: IdentityFederationApp) => void;
|
||||
onError?: (error: Error) => void;
|
||||
onDelete?: () => void;
|
||||
excludeFields?: 'product'[];
|
||||
|
@ -25,7 +25,7 @@ export const EditFederatedSAMLApp = ({
|
|||
const { t } = useTranslation('common');
|
||||
const [delModalVisible, setDelModalVisible] = useState(false);
|
||||
|
||||
const { data, isLoading, error, mutate } = useSWR<{ data: SAMLFederationApp }>(urls.getApp, fetcher);
|
||||
const { data, isLoading, error, mutate } = useSWR<{ data: IdentityFederationApp }>(urls.getApp, fetcher);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
|
@ -1,5 +1,3 @@
|
|||
import useSWR from 'swr';
|
||||
import { fetcher } from '../utils';
|
||||
import {
|
||||
Loading,
|
||||
Table,
|
||||
|
@ -11,17 +9,17 @@ import {
|
|||
ButtonPrimary,
|
||||
} from '../shared';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { SAMLFederationApp } from '../types';
|
||||
import type { IdentityFederationApp } from '../types';
|
||||
import PencilIcon from '@heroicons/react/24/outline/PencilIcon';
|
||||
import { TableBodyType } from '../shared/Table';
|
||||
import { pageLimit } from '../shared/Pagination';
|
||||
import { usePaginate } from '../hooks';
|
||||
import { useFetch, usePaginate } from '../hooks';
|
||||
import { useRouter } from '../hooks';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
type ExcludeFields = keyof Pick<SAMLFederationApp, 'product'>;
|
||||
type ExcludeFields = keyof Pick<IdentityFederationApp, 'product'>;
|
||||
|
||||
export const FederatedSAMLApps = ({
|
||||
export const IdentityFederationApps = ({
|
||||
urls,
|
||||
excludeFields,
|
||||
onEdit,
|
||||
|
@ -30,9 +28,9 @@ export const FederatedSAMLApps = ({
|
|||
}: {
|
||||
urls: { getApps: string };
|
||||
excludeFields?: ExcludeFields[];
|
||||
onEdit?: (app: SAMLFederationApp) => void;
|
||||
onEdit?: (app: IdentityFederationApp) => void;
|
||||
actions: { newApp: string; samlConfiguration: string; oidcConfiguration: string };
|
||||
actionCols?: { text: string; onClick: (app: SAMLFederationApp) => void; icon: JSX.Element }[];
|
||||
actionCols?: { text: string; onClick: (app: IdentityFederationApp) => void; icon: JSX.Element }[];
|
||||
}) => {
|
||||
const { router } = useRouter();
|
||||
const { t } = useTranslation('common');
|
||||
|
@ -45,10 +43,9 @@ export const FederatedSAMLApps = ({
|
|||
getAppsUrl += `&pageToken=${pageTokenMap[paginate.offset - pageLimit]}`;
|
||||
}
|
||||
|
||||
const { data, isLoading, error } = useSWR<{ data: SAMLFederationApp[]; pageToken?: string }>(
|
||||
getAppsUrl,
|
||||
fetcher
|
||||
);
|
||||
const { data, isLoading, error } = useFetch<{ data: IdentityFederationApp[]; pageToken?: string }>({
|
||||
url: getAppsUrl,
|
||||
});
|
||||
|
||||
const nextPageToken = data?.pageToken;
|
||||
|
|
@ -2,19 +2,19 @@ import { useFormik } from 'formik';
|
|||
import TagsInput from 'react-tagsinput';
|
||||
import { Card, Button } from 'react-daisyui';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { SAMLFederationApp } from '../types';
|
||||
import type { IdentityFederationApp } from '../types';
|
||||
import QuestionMarkCircleIcon from '@heroicons/react/24/outline/QuestionMarkCircleIcon';
|
||||
import { defaultHeaders } from '../utils';
|
||||
import { AttributesMapping } from './AttributesMapping';
|
||||
import { PageHeader } from '../shared';
|
||||
import { ItemList } from '@boxyhq/react-ui/shared';
|
||||
|
||||
type NewSAMLFederationApp = Pick<
|
||||
SAMLFederationApp,
|
||||
type NewIdentityFederationApp = Pick<
|
||||
IdentityFederationApp,
|
||||
'name' | 'tenant' | 'product' | 'acsUrl' | 'entityId' | 'tenants' | 'mappings' | 'type' | 'redirectUrl'
|
||||
>;
|
||||
|
||||
export const NewFederatedSAMLApp = ({
|
||||
export const NewIdentityFederationApp = ({
|
||||
samlAudience = 'https://saml.boxyhq.com',
|
||||
urls,
|
||||
onSuccess,
|
||||
|
@ -24,14 +24,14 @@ export const NewFederatedSAMLApp = ({
|
|||
}: {
|
||||
samlAudience?: string;
|
||||
urls: { createApp: string };
|
||||
onSuccess?: (data: SAMLFederationApp) => void;
|
||||
onSuccess?: (data: IdentityFederationApp) => void;
|
||||
onError?: (error: Error) => void;
|
||||
onEntityIdGenerated?: (entityId: string) => void;
|
||||
excludeFields?: 'product'[];
|
||||
}) => {
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
const initialValues: NewSAMLFederationApp = {
|
||||
const initialValues: NewIdentityFederationApp = {
|
||||
type: 'saml',
|
||||
name: '',
|
||||
tenant: '',
|
||||
|
@ -44,11 +44,11 @@ export const NewFederatedSAMLApp = ({
|
|||
|
||||
if (excludeFields) {
|
||||
excludeFields.forEach((key) => {
|
||||
delete initialValues[key as keyof NewSAMLFederationApp];
|
||||
delete initialValues[key as keyof NewIdentityFederationApp];
|
||||
});
|
||||
}
|
||||
|
||||
const formik = useFormik<NewSAMLFederationApp>({
|
||||
const formik = useFormik<NewIdentityFederationApp>({
|
||||
initialValues: initialValues,
|
||||
onSubmit: async (values) => {
|
||||
const rawResponse = await fetch(urls.createApp, {
|
|
@ -0,0 +1,3 @@
|
|||
export { NewIdentityFederationApp } from './NewIdentityFederationApp';
|
||||
export { EditIdentityFederationApp } from './EditIdentityFederationApp';
|
||||
export { IdentityFederationApps } from './IdentityFederationApps';
|
|
@ -1,7 +1,7 @@
|
|||
export * from './well-known';
|
||||
export * from './federated-saml';
|
||||
export * from './identity-federation';
|
||||
export * from './shared';
|
||||
export * from './dsync';
|
||||
export * from './provider';
|
||||
export * from './sso-tracer';
|
||||
export * from './sso-traces';
|
||||
export * from './setup-link';
|
||||
|
|
|
@ -13,7 +13,7 @@ export const SetupLinkInfo = ({ setupLink, onClose }: { setupLink: SetupLink; on
|
|||
<Card className='border-primary'>
|
||||
<Card.Body>
|
||||
<div>
|
||||
<InputWithCopyButton text={setupLink.url} label={t('bui-sl-share-info')} />
|
||||
<InputWithCopyButton text={setupLink.url} label={t('bui-sl-share-info')} autofocus={true} />
|
||||
</div>
|
||||
<div>
|
||||
<Button size='sm' color='primary' onClick={onClose}>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import useSWR from 'swr';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import EyeIcon from '@heroicons/react/24/outline/EyeIcon';
|
||||
|
@ -6,11 +5,11 @@ import TrashIcon from '@heroicons/react/24/outline/TrashIcon';
|
|||
import ArrowPathIcon from '@heroicons/react/24/outline/ArrowPathIcon';
|
||||
import ClipboardDocumentIcon from '@heroicons/react/24/outline/ClipboardDocumentIcon';
|
||||
|
||||
import { addQueryParamsToPath, copyToClipboard, fetcher } from '../utils';
|
||||
import { addQueryParamsToPath, copyToClipboard } from '../utils';
|
||||
import { TableBodyType } from '../shared/Table';
|
||||
import { pageLimit } from '../shared/Pagination';
|
||||
import { usePaginate, useRouter } from '../hooks';
|
||||
import type { SAMLFederationApp, SetupLink, SetupLinkService } from '../types';
|
||||
import { usePaginate, useRouter, useFetch } from '../hooks';
|
||||
import type { IdentityFederationApp, SetupLink, SetupLinkService } from '../types';
|
||||
import {
|
||||
Loading,
|
||||
Table,
|
||||
|
@ -24,7 +23,7 @@ import {
|
|||
} from '../shared';
|
||||
import { SetupLinkInfoModal } from './SetupLinkInfoModal';
|
||||
|
||||
type ExcludeFields = keyof Pick<SAMLFederationApp, 'product'>;
|
||||
type ExcludeFields = keyof Pick<IdentityFederationApp, 'product'>;
|
||||
|
||||
export const SetupLinks = ({
|
||||
urls,
|
||||
|
@ -65,10 +64,9 @@ export const SetupLinks = ({
|
|||
}
|
||||
|
||||
const getLinksUrl = addQueryParamsToPath(urls.getLinks, params);
|
||||
const { data, isLoading, error, mutate } = useSWR<{ data: SetupLink[]; pageToken?: string }>(
|
||||
getLinksUrl,
|
||||
fetcher
|
||||
);
|
||||
const { data, isLoading, error, refetch } = useFetch<{ data: SetupLink[]; pageToken?: string }>({
|
||||
url: getLinksUrl,
|
||||
});
|
||||
|
||||
const nextPageToken = data?.pageToken;
|
||||
|
||||
|
@ -201,7 +199,7 @@ export const SetupLinks = ({
|
|||
setDelModal(false);
|
||||
setSetupLink(null);
|
||||
onDelete(setupLink);
|
||||
await mutate();
|
||||
refetch();
|
||||
} else {
|
||||
onError(response.error);
|
||||
}
|
||||
|
@ -229,7 +227,7 @@ export const SetupLinks = ({
|
|||
if (rawResponse.ok) {
|
||||
onRegenerate(response.data);
|
||||
setShowRegenModal(false);
|
||||
await mutate();
|
||||
refetch();
|
||||
setSetupLink(response.data);
|
||||
setShowSetupLink(true);
|
||||
} else {
|
||||
|
|
|
@ -19,19 +19,32 @@ export const CopyToClipboardButton = ({ text }: { text: string }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const InputWithCopyButton = ({ text, label }: { text: string; label: string }) => {
|
||||
export const InputWithCopyButton = ({
|
||||
text,
|
||||
label,
|
||||
autofocus = false,
|
||||
}: {
|
||||
text: string;
|
||||
label: string;
|
||||
autofocus?: boolean;
|
||||
}) => {
|
||||
const id = label.replace(/ /g, '');
|
||||
return (
|
||||
<>
|
||||
<div className='flex justify-between'>
|
||||
<label className='mb-2 block text-sm font-medium text-gray-900 dark:text-gray-300'>{label}</label>
|
||||
<label className='mb-2 block text-sm font-medium text-gray-900 dark:text-gray-300' htmlFor={id}>
|
||||
{label}
|
||||
</label>
|
||||
<CopyToClipboardButton text={text} />
|
||||
</div>
|
||||
<input
|
||||
id={id}
|
||||
type='text'
|
||||
defaultValue={text}
|
||||
key={text}
|
||||
readOnly
|
||||
className='input-bordered input w-full text-sm'
|
||||
autoFocus={autofocus}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
export { SSOTracers } from './SSOTracers';
|
||||
export { SSOTracerInfo } from './SSOTracerInfo';
|
|
@ -15,10 +15,10 @@ const ListItem = ({ term, value }: { term: string; value: string | JSX.Element }
|
|||
</div>
|
||||
);
|
||||
|
||||
export const SSOTracerInfo = ({ urls }: { urls: { getTracer: string } }) => {
|
||||
export const SSOTraceInfo = ({ urls }: { urls: { getTraces: string } }) => {
|
||||
const { t } = useTranslation('common');
|
||||
const { data, isLoading, error } = useSWR<{ data: SSOTrace & { traceId: string } }>(
|
||||
urls.getTracer,
|
||||
urls.getTraces,
|
||||
fetcher
|
||||
);
|
||||
|
||||
|
@ -42,81 +42,81 @@ export const SSOTracerInfo = ({ urls }: { urls: { getTracer: string } }) => {
|
|||
if (trace.context.requestedOIDCFlow) {
|
||||
badgeText = t('bui-shared-oidc-federation');
|
||||
} else {
|
||||
badgeText = t('bui-tracer-oauth2-federation');
|
||||
badgeText = t('bui-traces-oauth2-federation');
|
||||
}
|
||||
} else if (trace.context.isSAMLFederated) {
|
||||
badgeText = t('bui-tracer-saml-federation');
|
||||
badgeText = t('bui-traces-saml-federation');
|
||||
} else if (trace.context.isIdPFlow) {
|
||||
badgeText = t('bui-tracer-idp-login');
|
||||
badgeText = t('bui-traces-idp-login');
|
||||
} else if (trace.context.requestedOIDCFlow) {
|
||||
badgeText = t('bui-shared-oidc');
|
||||
} else {
|
||||
badgeText = t('bui-tracer-oauth2');
|
||||
badgeText = t('bui-traces-oauth2');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='space-y-3'>
|
||||
<PageHeader title={t('bui-tracer-title')} />
|
||||
<PageHeader title={t('bui-traces-title')} />
|
||||
<dl className='divide-y'>
|
||||
<ListItem term={t('bui-tracer-id')} value={trace.traceId} />
|
||||
<ListItem term={t('bui-traces-id')} value={trace.traceId} />
|
||||
|
||||
<ListItem term={t('bui-tracer-assertion-type')} value={assertionType} />
|
||||
<ListItem term={t('bui-traces-assertion-type')} value={assertionType} />
|
||||
|
||||
<ListItem
|
||||
term={t('bui-tracer-sp-protocol')}
|
||||
term={t('bui-traces-sp-protocol')}
|
||||
value={
|
||||
<Badge
|
||||
color='primary'
|
||||
size='md'
|
||||
className='font-mono uppercase text-white'
|
||||
aria-label={t('bui-tracer-sp-protocol')!}>
|
||||
aria-label={t('bui-traces-sp-protocol')!}>
|
||||
{badgeText}
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
|
||||
{typeof trace.timestamp === 'number' && (
|
||||
<ListItem term={t('bui-tracer-timestamp')} value={new Date(trace.timestamp).toLocaleString()} />
|
||||
<ListItem term={t('bui-traces-timestamp')} value={new Date(trace.timestamp).toLocaleString()} />
|
||||
)}
|
||||
|
||||
<ListItem term={t('bui-tracer-error')} value={trace.error} />
|
||||
<ListItem term={t('bui-traces-error')} value={trace.error} />
|
||||
|
||||
{trace.context.tenant && <ListItem term={t('bui-shared-tenant')} value={trace.context.tenant} />}
|
||||
|
||||
{trace.context.product && <ListItem term={t('bui-shared-product')} value={trace.context.product} />}
|
||||
|
||||
{trace.context.relayState && (
|
||||
<ListItem term={t('bui-tracer-relay-state')} value={trace.context.relayState} />
|
||||
<ListItem term={t('bui-traces-relay-state')} value={trace.context.relayState} />
|
||||
)}
|
||||
|
||||
{trace.context.redirectUri && (
|
||||
<ListItem
|
||||
term={
|
||||
trace.context.isIdPFlow ? t('bui-tracer-default-redirect-url') : t('bui-tracer-redirect-uri')
|
||||
trace.context.isIdPFlow ? t('bui-traces-default-redirect-url') : t('bui-traces-redirect-uri')
|
||||
}
|
||||
value={trace.context.redirectUri}
|
||||
/>
|
||||
)}
|
||||
|
||||
{trace.context.clientID && (
|
||||
<ListItem term={t('bui-tracer-sso-connection-client-id')} value={trace.context.clientID} />
|
||||
<ListItem term={t('bui-traces-sso-connection-client-id')} value={trace.context.clientID} />
|
||||
)}
|
||||
|
||||
{trace.context.issuer && <ListItem term={t('bui-tracer-issuer')} value={trace.context.issuer} />}
|
||||
{trace.context.issuer && <ListItem term={t('bui-traces-issuer')} value={trace.context.issuer} />}
|
||||
|
||||
{trace.context.acsUrl && <ListItem term={t('bui-shared-acs-url')} value={trace.context.acsUrl} />}
|
||||
|
||||
{trace.context.entityId && (
|
||||
<ListItem term={t('bui-tracer-entity-id')} value={trace.context.entityId} />
|
||||
<ListItem term={t('bui-traces-entity-id')} value={trace.context.entityId} />
|
||||
)}
|
||||
|
||||
{trace.context.providerName && (
|
||||
<ListItem term={t('bui-tracer-provider')} value={trace.context.providerName} />
|
||||
<ListItem term={t('bui-traces-provider')} value={trace.context.providerName} />
|
||||
)}
|
||||
|
||||
{assertionType === 'Response' && trace.context.samlResponse && (
|
||||
<ListItem
|
||||
term={t('bui-tracer-saml-response')}
|
||||
term={t('bui-traces-saml-response')}
|
||||
value={
|
||||
<>
|
||||
<CopyToClipboardButton text={trace.context.samlResponse}></CopyToClipboardButton>
|
||||
|
@ -130,7 +130,7 @@ export const SSOTracerInfo = ({ urls }: { urls: { getTracer: string } }) => {
|
|||
|
||||
{assertionType === 'Request' && trace.context.samlRequest && (
|
||||
<ListItem
|
||||
term={t('bui-tracer-saml-request')}
|
||||
term={t('bui-traces-saml-request')}
|
||||
value={
|
||||
<>
|
||||
<CopyToClipboardButton text={trace.context.samlRequest}></CopyToClipboardButton>
|
||||
|
@ -144,7 +144,7 @@ export const SSOTracerInfo = ({ urls }: { urls: { getTracer: string } }) => {
|
|||
|
||||
{typeof trace.context.profile === 'object' && trace.context.profile && (
|
||||
<ListItem
|
||||
term={t('bui-tracer-profile')}
|
||||
term={t('bui-traces-profile')}
|
||||
value={
|
||||
<SyntaxHighlighter language='json' style={materialOceanic}>
|
||||
{JSON.stringify(trace.context.profile)}
|
||||
|
@ -155,18 +155,18 @@ export const SSOTracerInfo = ({ urls }: { urls: { getTracer: string } }) => {
|
|||
|
||||
{trace.context.error_description && (
|
||||
<ListItem
|
||||
term={t('bui-tracer-error-description-from-oidc-idp')}
|
||||
term={t('bui-traces-error-description-from-oidc-idp')}
|
||||
value={trace.context.error_description}
|
||||
/>
|
||||
)}
|
||||
|
||||
{trace.context.error_uri && (
|
||||
<ListItem term={t('bui-tracer-error-uri')} value={trace.context.error_uri} />
|
||||
<ListItem term={t('bui-traces-error-uri')} value={trace.context.error_uri} />
|
||||
)}
|
||||
|
||||
{trace.context.oidcTokenSet?.id_token && (
|
||||
<ListItem
|
||||
term={t('bui-tracer-id-token-from-oidc-idp')}
|
||||
term={t('bui-traces-id-token-from-oidc-idp')}
|
||||
value={
|
||||
<>
|
||||
<CopyToClipboardButton text={trace.context.oidcTokenSet.id_token}></CopyToClipboardButton>
|
||||
|
@ -180,7 +180,7 @@ export const SSOTracerInfo = ({ urls }: { urls: { getTracer: string } }) => {
|
|||
|
||||
{trace.context.oidcTokenSet?.access_token && (
|
||||
<ListItem
|
||||
term={t('bui-tracer-access-token-from-oidc-idp')}
|
||||
term={t('bui-traces-access-token-from-oidc-idp')}
|
||||
value={
|
||||
<>
|
||||
<CopyToClipboardButton text={trace.context.oidcTokenSet.access_token}></CopyToClipboardButton>
|
||||
|
@ -194,7 +194,7 @@ export const SSOTracerInfo = ({ urls }: { urls: { getTracer: string } }) => {
|
|||
|
||||
{trace.context.stack && (
|
||||
<ListItem
|
||||
term={t('bui-tracer-stack-trace')}
|
||||
term={t('bui-traces-stack-trace')}
|
||||
value={
|
||||
<SyntaxHighlighter language='shell' style={materialOceanic}>
|
||||
{trace.context.stack}
|
||||
|
@ -205,13 +205,13 @@ export const SSOTracerInfo = ({ urls }: { urls: { getTracer: string } }) => {
|
|||
|
||||
{trace.context.session_state_from_op_error && (
|
||||
<ListItem
|
||||
term={t('bui-tracer-session-state-from-oidc-idp')}
|
||||
term={t('bui-traces-session-state-from-oidc-idp')}
|
||||
value={trace.context.session_state_from_op_error}
|
||||
/>
|
||||
)}
|
||||
|
||||
{trace.context.scope_from_op_error && (
|
||||
<ListItem term={t('bui-tracer-scope-from-op-error')} value={trace.context.scope_from_op_error} />
|
||||
<ListItem term={t('bui-traces-scope-from-op-error')} value={trace.context.scope_from_op_error} />
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
|
@ -8,11 +8,11 @@ import type { ApiError, ApiSuccess } from '../types';
|
|||
import { addQueryParamsToPath, fetcher } from '../utils';
|
||||
import { Loading, Table, EmptyState, Error, Pagination, PageHeader, pageLimit } from '../shared';
|
||||
|
||||
export const SSOTracers = ({
|
||||
export const SSOTraces = ({
|
||||
urls,
|
||||
onView,
|
||||
}: {
|
||||
urls: { getTracers: string };
|
||||
urls: { getTraces: string };
|
||||
onView: (user: Trace) => void;
|
||||
}) => {
|
||||
const { router } = useRouter();
|
||||
|
@ -29,7 +29,7 @@ export const SSOTracers = ({
|
|||
params['pageToken'] = pageTokenMap[paginate.offset - pageLimit];
|
||||
}
|
||||
|
||||
const getUrl = addQueryParamsToPath(urls.getTracers, params);
|
||||
const getUrl = addQueryParamsToPath(urls.getTraces, params);
|
||||
const { data, isLoading, error } = useSWR<ApiSuccess<Trace[]>, ApiError>(getUrl, fetcher);
|
||||
|
||||
const nextPageToken = data?.pageToken;
|
||||
|
@ -57,10 +57,10 @@ export const SSOTracers = ({
|
|||
const noMoreResults = traces.length === 0 && paginate.offset > 0;
|
||||
|
||||
const cols = [
|
||||
t('bui-tracer-id'),
|
||||
t('bui-tracer-description'),
|
||||
t('bui-tracer-assertion-type'),
|
||||
t('bui-tracer-timestamp'),
|
||||
t('bui-traces-id'),
|
||||
t('bui-traces-description'),
|
||||
t('bui-traces-assertion-type'),
|
||||
t('bui-traces-timestamp'),
|
||||
];
|
||||
|
||||
const body = traces.map((trace) => {
|
||||
|
@ -82,9 +82,9 @@ export const SSOTracers = ({
|
|||
{
|
||||
wrap: true,
|
||||
text: trace.context?.samlResponse
|
||||
? t('bui-tracer-response')
|
||||
? t('bui-traces-response')
|
||||
: trace?.context.samlRequest
|
||||
? t('bui-tracer-request')
|
||||
? t('bui-traces-request')
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
|
@ -97,9 +97,9 @@ export const SSOTracers = ({
|
|||
|
||||
return (
|
||||
<div className='space-y-3'>
|
||||
<PageHeader title={t('bui-tracer-title')} />
|
||||
<PageHeader title={t('bui-traces-title')} />
|
||||
{noTraces ? (
|
||||
<EmptyState title={t('bui-tracer-no-traces')} />
|
||||
<EmptyState title={t('bui-traces-no-traces')} />
|
||||
) : (
|
||||
<>
|
||||
<Table noMoreResults={noMoreResults} cols={cols} body={body} />
|
|
@ -0,0 +1,2 @@
|
|||
export { SSOTraces } from './SSOTraces';
|
||||
export { SSOTraceInfo } from './SSOTraceInfo';
|
|
@ -90,7 +90,7 @@ export type AttributeMapping = {
|
|||
value: string;
|
||||
};
|
||||
|
||||
export type SAMLFederationApp = {
|
||||
export type IdentityFederationApp = {
|
||||
id: string;
|
||||
type?: string;
|
||||
clientID?: string;
|
||||
|
|
|
@ -22,4 +22,4 @@ patches:
|
|||
|
||||
images:
|
||||
- name: boxyhq/jackson
|
||||
newTag: 1.22.1
|
||||
newTag: 1.25.1
|
||||
|
|
|
@ -22,4 +22,4 @@ patches:
|
|||
|
||||
images:
|
||||
- name: boxyhq/jackson
|
||||
newTag: 1.22.1
|
||||
newTag: 1.25.1
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import jackson from './jackson';
|
||||
import { IndexNames } from 'npm/src/controller/utils';
|
||||
import { jacksonOptions } from '@lib/env';
|
||||
|
||||
type Module = 'sso' | 'dsync' | 'identityFederation';
|
||||
|
||||
export const validateDevelopmentModeLimits = async (
|
||||
productId: string,
|
||||
type: Module,
|
||||
message: string = 'Maximum number of connections reached'
|
||||
) => {
|
||||
if (productId && jacksonOptions.boxyhqHosted) {
|
||||
const {
|
||||
productController,
|
||||
connectionAPIController,
|
||||
directorySyncController,
|
||||
identityFederationController,
|
||||
} = await jackson();
|
||||
|
||||
const getController = async (type: Module) => {
|
||||
switch (type) {
|
||||
case 'sso':
|
||||
return connectionAPIController;
|
||||
case 'dsync':
|
||||
return directorySyncController.directories;
|
||||
case 'identityFederation':
|
||||
return identityFederationController.app;
|
||||
default:
|
||||
return {
|
||||
getCount: () => null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const product = await productController.get(productId);
|
||||
if (product?.development) {
|
||||
const controller = await getController(type);
|
||||
const count = await controller.getCount({
|
||||
name: IndexNames.Product,
|
||||
value: productId,
|
||||
});
|
||||
if (count) {
|
||||
if (count >= 3) {
|
||||
throw { message, statusCode: 400 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"apps": "Apps",
|
||||
"error_loading_page": "Unable to load this page. Maybe you don't have enough rights.",
|
||||
"documentation": "Documentation",
|
||||
"back": "Back",
|
||||
|
@ -29,9 +30,9 @@
|
|||
"send_magic_link": "Send Magic Link",
|
||||
"setup_links": "Setup Links",
|
||||
"edit_directory": "Edit Directory",
|
||||
"saml_federation_new_success": "Identity Federation app created successfully.",
|
||||
"saml_federation_update_success": "Identity Federation app updated successfully.",
|
||||
"saml_federation_delete_success": "Identity federation app deleted successfully",
|
||||
"identity_federation_new_success": "Identity Federation app created successfully.",
|
||||
"identity_federation_update_success": "Identity Federation app updated successfully.",
|
||||
"identity_federation_delete_success": "Identity Federation app deleted successfully",
|
||||
"saml_federation_app_info": "SAML Federation App Information",
|
||||
"saml_federation_app_info_details": "Choose from the following options to configure your SAML Federation on the service provider side",
|
||||
"download_metadata": "Download Metadata",
|
||||
|
@ -41,7 +42,7 @@
|
|||
"directory_created_successfully": "Directory created successfully",
|
||||
"directory_updated_successfully": "Directory updated successfully",
|
||||
"dashboard": "Dashboard",
|
||||
"saml_federation": "Identity Federation",
|
||||
"identity_federation": "Identity Federation",
|
||||
"settings": "Settings",
|
||||
"admin_portal_sso": "SSO for Admin Portal",
|
||||
"configuration": "Configuration",
|
||||
|
@ -224,37 +225,37 @@
|
|||
"bui-dsync-authorization-google": "Authorize Google Workspace",
|
||||
"bui-dsync-authorization-google-desc": "You should authorize Google Workspace to sync your directory. Click the button below start the authorization process. Make sure you have the necessary permissions to authorize Google Workspace.",
|
||||
"bui-dsync-google-domain": "Google Domain",
|
||||
"bui-tracer-title": "SSO Tracer",
|
||||
"bui-tracer-id": "Trace ID",
|
||||
"bui-tracer-description": "Description",
|
||||
"bui-tracer-assertion-type": "Assertion Type",
|
||||
"bui-tracer-timestamp": "Timestamp",
|
||||
"bui-tracer-response": "Response",
|
||||
"bui-tracer-request": "Request",
|
||||
"bui-tracer-no-traces": "No SSO Traces recorded yet.",
|
||||
"bui-tracer-sp-protocol": "SP Protocol",
|
||||
"bui-tracer-saml-federation": "SAML Federation",
|
||||
"bui-tracer-oauth2-federation": "OAuth 2.0 Federation",
|
||||
"bui-tracer-idp-login": "IdP Login",
|
||||
"bui-tracer-oauth2": "OAuth 2.0",
|
||||
"bui-tracer-error": "Error",
|
||||
"bui-tracer-relay-state": "Relay State",
|
||||
"bui-tracer-default-redirect-url": "Default Redirect URL",
|
||||
"bui-tracer-redirect-uri": "Redirect URI",
|
||||
"bui-tracer-sso-connection-client-id": "SSO Connection Client ID",
|
||||
"bui-tracer-issuer": "Issuer",
|
||||
"bui-tracer-entity-id": "Entity ID",
|
||||
"bui-tracer-provider": "Provider",
|
||||
"bui-tracer-saml-response": "SAML Response",
|
||||
"bui-tracer-saml-request": "SAML Request",
|
||||
"bui-tracer-profile": "Profile",
|
||||
"bui-tracer-error-description-from-oidc-idp": "Error Description (from OIDC Provider)",
|
||||
"bui-tracer-error-uri": "Error URI",
|
||||
"bui-tracer-id-token-from-oidc-idp": "ID Token (from OIDC Provider)",
|
||||
"bui-tracer-access-token-from-oidc-idp": "Access Token (from OIDC Provider)",
|
||||
"bui-tracer-stack-trace": "Stack Trace",
|
||||
"bui-tracer-session-state-from-oidc-idp": "Session State (from OIDC Provider)",
|
||||
"bui-tracer-scope-from-op-error": "Scope (from OIDC Provider)",
|
||||
"bui-traces-title": "SSO Traces",
|
||||
"bui-traces-id": "Trace ID",
|
||||
"bui-traces-description": "Description",
|
||||
"bui-traces-assertion-type": "Assertion Type",
|
||||
"bui-traces-timestamp": "Timestamp",
|
||||
"bui-traces-response": "Response",
|
||||
"bui-traces-request": "Request",
|
||||
"bui-traces-no-traces": "No SSO Traces recorded yet.",
|
||||
"bui-traces-sp-protocol": "SP Protocol",
|
||||
"bui-traces-saml-federation": "SAML Federation",
|
||||
"bui-traces-oauth2-federation": "OAuth 2.0 Federation",
|
||||
"bui-traces-idp-login": "IdP Login",
|
||||
"bui-traces-oauth2": "OAuth 2.0",
|
||||
"bui-traces-error": "Error",
|
||||
"bui-traces-relay-state": "Relay State",
|
||||
"bui-traces-default-redirect-url": "Default Redirect URL",
|
||||
"bui-traces-redirect-uri": "Redirect URI",
|
||||
"bui-traces-sso-connection-client-id": "SSO Connection Client ID",
|
||||
"bui-traces-issuer": "Issuer",
|
||||
"bui-traces-entity-id": "Entity ID",
|
||||
"bui-traces-provider": "Provider",
|
||||
"bui-traces-saml-response": "SAML Response",
|
||||
"bui-traces-saml-request": "SAML Request",
|
||||
"bui-traces-profile": "Profile",
|
||||
"bui-traces-error-description-from-oidc-idp": "Error Description (from OIDC Provider)",
|
||||
"bui-traces-error-uri": "Error URI",
|
||||
"bui-traces-id-token-from-oidc-idp": "ID Token (from OIDC Provider)",
|
||||
"bui-traces-access-token-from-oidc-idp": "Access Token (from OIDC Provider)",
|
||||
"bui-traces-stack-trace": "Stack Trace",
|
||||
"bui-traces-session-state-from-oidc-idp": "Session State (from OIDC Provider)",
|
||||
"bui-traces-scope-from-op-error": "Scope (from OIDC Provider)",
|
||||
"bui-sl-name": "Name (Optional)",
|
||||
"bui-sl-sso-description": "Description (Optional)",
|
||||
"bui-sl-create-link": "Create Setup Link",
|
||||
|
|
|
@ -11,6 +11,7 @@ const unAuthenticatedApiRoutes = [
|
|||
'/api/hello',
|
||||
'/api/auth/**',
|
||||
'/api/federated-saml/**',
|
||||
'/api/identity-federation/**',
|
||||
'/api/logout/**',
|
||||
'/api/oauth/**',
|
||||
'/api/scim/v2.0/**',
|
||||
|
|
|
@ -19,7 +19,7 @@ module.exports = {
|
|||
config.plugins.push(
|
||||
new webpack.IgnorePlugin({
|
||||
resourceRegExp:
|
||||
/(^@google-cloud\/spanner|^@mongodb-js\/zstd|^aws-crt|^aws4$|^pg-native$|^mongodb-client-encryption$|^@sap\/hana-client$|^@sap\/hana-client\/extension\/Stream$|^snappy$|^react-native-sqlite-storage$|^bson-ext$|^cardinal$|^kerberos$|^hdb-pool$|^sql.js$|^sqlite3$|^better-sqlite3$|^ioredis$|^typeorm-aurora-data-api-driver$|^pg-query-stream$|^oracledb$|^mysql$|^snappy\/package\.json$|^cloudflare:sockets$)/,
|
||||
/(^@google-cloud\/spanner|^@mongodb-js\/zstd|^aws-crt|^aws4$|^pg-native$|^mongodb-client-encryption$|^@sap\/hana-client$|^@sap\/hana-client\/extension\/Stream$|^snappy$|^react-native-sqlite-storage$|^bson-ext$|^cardinal$|^kerberos$|^hdb-pool$|^sql.js$|^better-sqlite3$|^ioredis$|^typeorm-aurora-data-api-driver$|^pg-query-stream$|^oracledb$|^mysql$|^snappy\/package\.json$|^cloudflare:sockets$)/,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -101,6 +101,10 @@ module.exports = {
|
|||
source: '/api/v1/saml-traces/:path*',
|
||||
destination: '/api/v1/sso-traces/:path*',
|
||||
},
|
||||
{
|
||||
source: '/api/v1/federated-saml/:path*',
|
||||
destination: '/api/v1/identity-federation/:path*',
|
||||
},
|
||||
];
|
||||
},
|
||||
images: {
|
||||
|
|
|
@ -26,8 +26,8 @@ const map = {
|
|||
'src/directory-sync/request.ts',
|
||||
],
|
||||
'test/dsync/events.test.ts': ['src/directory-sync/events.ts'],
|
||||
'test/federated-saml/app.test.ts': ['src/ee/federated-saml/app.ts'],
|
||||
'test/federated-saml/sso.test.ts': ['src/ee/federated-saml/sso.ts'],
|
||||
'test/identity-federation/app.test.ts': ['src/ee/identity-federation/app.ts'],
|
||||
'test/identity-federation/sso.test.ts': ['src/ee/identity-federation/sso.ts'],
|
||||
'test/event/index.test.ts': ['src/event/*'],
|
||||
'test/dsync/google_oauth.test.ts': [
|
||||
'src/directory-sync/non-scim/google/oauth.ts',
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MdNamespace1714417013715 implements MigrationInterface {
|
||||
name = 'MdNamespace1714417013715'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE \`jackson_store\` MODIFY \`namespace\` varchar(256) NULL`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE \`jackson_store\` MODIFY \`namespace\` varchar(64) NULL`);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MssNamespace1714421718208 implements MigrationInterface {
|
||||
name = 'MssNamespace1714421718208'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "jackson_store" ALTER COLUMN "namespace" varchar(256)`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "jackson_store" ALTER COLUMN "namespace" varchar(64)`);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MsNamespace1714419315556 implements MigrationInterface {
|
||||
name = 'MsNamespace1714419315556'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE \`jackson_store\` MODIFY \`namespace\` varchar(256) NULL`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE \`jackson_store\` MODIFY \`namespace\` varchar(64) NULL`);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MsNamespace1714457285484 implements MigrationInterface {
|
||||
name = 'MsNamespace1714457285484'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE \`jackson_store\` MODIFY \`namespace\` varchar(256) NULL`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE \`jackson_store\` MODIFY \`namespace\` varchar(64) NULL`);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class PgNamespace1714452929542 implements MigrationInterface {
|
||||
name = 'PgNamespace1714452929542'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "jackson_store" ALTER COLUMN "namespace" TYPE VARCHAR(256)`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "jackson_store" ALTER COLUMN "namespace" TYPE VARCHAR(64)`);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class SqliteInitial1716476500487 implements MigrationInterface {
|
||||
name = 'SqliteInitial1716476500487'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "jackson_ttl" ("key" varchar(250) PRIMARY KEY NOT NULL, "expiresAt" bigint NOT NULL)`);
|
||||
await queryRunner.query(`CREATE INDEX "_jackson_ttl_expires_at" ON "jackson_ttl" ("expiresAt") `);
|
||||
await queryRunner.query(`CREATE TABLE "jackson_store" ("key" varchar(1500) PRIMARY KEY NOT NULL, "value" text NOT NULL, "iv" varchar(64), "tag" varchar(64), "createdAt" datetime NOT NULL DEFAULT ((STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW'))), "modifiedAt" datetime, "namespace" varchar(256))`);
|
||||
await queryRunner.query(`CREATE INDEX "_jackson_store_namespace" ON "jackson_store" ("namespace") `);
|
||||
await queryRunner.query(`CREATE TABLE "jackson_index" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "key" varchar(250) NOT NULL, "storeKey" varchar(250) NOT NULL)`);
|
||||
await queryRunner.query(`CREATE INDEX "_jackson_index_key" ON "jackson_index" ("key") `);
|
||||
await queryRunner.query(`CREATE INDEX "_jackson_index_key_store" ON "jackson_index" ("key", "storeKey") `);
|
||||
await queryRunner.query(`DROP INDEX "_jackson_index_key"`);
|
||||
await queryRunner.query(`DROP INDEX "_jackson_index_key_store"`);
|
||||
await queryRunner.query(`CREATE TABLE "temporary_jackson_index" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "key" varchar(250) NOT NULL, "storeKey" varchar(250) NOT NULL, CONSTRAINT "FK_937b040fb2592b4671cbde09e83" FOREIGN KEY ("storeKey") REFERENCES "jackson_store" ("key") ON DELETE CASCADE ON UPDATE NO ACTION)`);
|
||||
await queryRunner.query(`INSERT INTO "temporary_jackson_index"("id", "key", "storeKey") SELECT "id", "key", "storeKey" FROM "jackson_index"`);
|
||||
await queryRunner.query(`DROP TABLE "jackson_index"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_jackson_index" RENAME TO "jackson_index"`);
|
||||
await queryRunner.query(`CREATE INDEX "_jackson_index_key" ON "jackson_index" ("key") `);
|
||||
await queryRunner.query(`CREATE INDEX "_jackson_index_key_store" ON "jackson_index" ("key", "storeKey") `);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "_jackson_index_key_store"`);
|
||||
await queryRunner.query(`DROP INDEX "_jackson_index_key"`);
|
||||
await queryRunner.query(`ALTER TABLE "jackson_index" RENAME TO "temporary_jackson_index"`);
|
||||
await queryRunner.query(`CREATE TABLE "jackson_index" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "key" varchar(250) NOT NULL, "storeKey" varchar(250) NOT NULL)`);
|
||||
await queryRunner.query(`INSERT INTO "jackson_index"("id", "key", "storeKey") SELECT "id", "key", "storeKey" FROM "temporary_jackson_index"`);
|
||||
await queryRunner.query(`DROP TABLE "temporary_jackson_index"`);
|
||||
await queryRunner.query(`CREATE INDEX "_jackson_index_key_store" ON "jackson_index" ("key", "storeKey") `);
|
||||
await queryRunner.query(`CREATE INDEX "_jackson_index_key" ON "jackson_index" ("key") `);
|
||||
await queryRunner.query(`DROP INDEX "_jackson_index_key_store"`);
|
||||
await queryRunner.query(`DROP INDEX "_jackson_index_key"`);
|
||||
await queryRunner.query(`DROP TABLE "jackson_index"`);
|
||||
await queryRunner.query(`DROP INDEX "_jackson_store_namespace"`);
|
||||
await queryRunner.query(`DROP TABLE "jackson_store"`);
|
||||
await queryRunner.query(`DROP INDEX "_jackson_ttl_expires_at"`);
|
||||
await queryRunner.query(`DROP TABLE "jackson_ttl"`);
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -18,19 +18,21 @@
|
|||
],
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"db:migration:generate:postgres": "ts-node --transpile-only ./node_modules/typeorm/cli.js migration:generate -d typeorm.ts migration/postgres/pg_${MIGRATION_NAME}",
|
||||
"db:migration:generate:mysql": "cross-env DB_TYPE=mysql DB_URL=mysql://root:mysql@localhost:3307/mysql ts-node --transpile-only ./node_modules/typeorm/cli.js migration:generate -d typeorm.ts migration/mysql/ms_${MIGRATION_NAME}",
|
||||
"db:migration:generate:planetscale": "cross-env DB_ENGINE=planetscale DB_URL=mysql://root:mysql@localhost:3307/mysql ts-node --transpile-only ./node_modules/typeorm/cli.js migration:generate -d typeorm.ts migration/planetscale/ms_${MIGRATION_NAME}",
|
||||
"db:migration:generate:mariadb": "cross-env DB_TYPE=mariadb DB_URL=mariadb://root@localhost:3306/mysql ts-node --transpile-only ./node_modules/typeorm/cli.js migration:generate -d typeorm.ts migration/mariadb/md_${MIGRATION_NAME}",
|
||||
"db:migration:generate:postgres": "ts-node --transpile-only ../node_modules/typeorm/cli.js migration:generate -d typeorm.ts migration/postgres/pg_${MIGRATION_NAME}",
|
||||
"db:migration:generate:mysql": "cross-env DB_TYPE=mysql DB_URL=mysql://root:mysql@localhost:3307/mysql ts-node --transpile-only ../node_modules/typeorm/cli.js migration:generate -d typeorm.ts migration/mysql/ms_${MIGRATION_NAME}",
|
||||
"db:migration:generate:planetscale": "cross-env DB_ENGINE=planetscale DB_URL=mysql://root:mysql@localhost:3307/mysql ts-node --transpile-only ../node_modules/typeorm/cli.js migration:generate -d typeorm.ts migration/planetscale/ms_${MIGRATION_NAME}",
|
||||
"db:migration:generate:mariadb": "cross-env DB_TYPE=mariadb DB_URL=mariadb://root@localhost:3306/mysql ts-node --transpile-only ../node_modules/typeorm/cli.js migration:generate -d typeorm.ts migration/mariadb/md_${MIGRATION_NAME}",
|
||||
"db:migration:generate:mongo": "migrate-mongo create ${MIGRATION_NAME}",
|
||||
"db:migration:generate:mssql": "cross-env DB_TYPE=mssql DB_URL='sqlserver://localhost:1433;database=master;username=sa;password=123ABabc!' ts-node --transpile-only ./node_modules/typeorm/cli.js migration:generate -d typeorm.ts migration/mssql/mss_${MIGRATION_NAME}",
|
||||
"db:migration:run:postgres": "ts-node --transpile-only ./node_modules/typeorm/cli.js migration:run -d typeorm.ts",
|
||||
"db:migration:run:mysql": "cross-env DB_TYPE=mysql DB_URL=mysql://root:mysql@localhost:3307/mysql ts-node --transpile-only ./node_modules/typeorm/cli.js migration:run -d typeorm.ts",
|
||||
"db:migration:run:planetscale": "cross-env DB_ENGINE=planetscale DB_URL=${PLANETSCALE_URL} ts-node --transpile-only ./node_modules/typeorm/cli.js migration:run -d typeorm.ts",
|
||||
"db:migration:run:mariadb": "cross-env DB_TYPE=mariadb DB_URL=mariadb://root@localhost:3306/mysql ts-node --transpile-only ./node_modules/typeorm/cli.js migration:run -d typeorm.ts",
|
||||
"db:migration:generate:mssql": "cross-env DB_TYPE=mssql DB_URL='sqlserver://localhost:1433;database=master;username=sa;password=123ABabc!' ts-node --transpile-only ../node_modules/typeorm/cli.js migration:generate -d typeorm.ts migration/mssql/mss_${MIGRATION_NAME}",
|
||||
"db:migration:generate:sqlite": "cross-env DB_TYPE=sqlite DB_URL='file:///tmp/migration-sqlite.db' ts-node --transpile-only ../node_modules/typeorm/cli.js migration:generate -d typeorm.ts migration/sqlite/sqlite_${MIGRATION_NAME}",
|
||||
"db:migration:run:postgres": "ts-node --transpile-only ../node_modules/typeorm/cli.js migration:run -d typeorm.ts",
|
||||
"db:migration:run:mysql": "cross-env DB_TYPE=mysql DB_URL=mysql://root:mysql@localhost:3307/mysql ts-node --transpile-only ../node_modules/typeorm/cli.js migration:run -d typeorm.ts",
|
||||
"db:migration:run:planetscale": "cross-env DB_ENGINE=planetscale DB_URL=${PLANETSCALE_URL} ts-node --transpile-only ../node_modules/typeorm/cli.js migration:run -d typeorm.ts",
|
||||
"db:migration:run:mariadb": "cross-env DB_TYPE=mariadb DB_URL=mariadb://root@localhost:3306/mysql ts-node --transpile-only ../node_modules/typeorm/cli.js migration:run -d typeorm.ts",
|
||||
"db:migration:run:mongo": "cross-env DB_URL='mongodb://localhost:27017/jackson' migrate-mongo up",
|
||||
"db:migration:revert:mongo": "cross-env DB_URL='mongodb://localhost:27017/jackson migrate-mongo down",
|
||||
"db:migration:run:mssql": "cross-env DB_TYPE=mssql DB_URL='sqlserver://localhost:1433;database=master;username=sa;password=123ABabc!' ts-node --transpile-only ./node_modules/typeorm/cli.js migration:run -d typeorm.ts",
|
||||
"db:migration:run:mssql": "cross-env DB_TYPE=mssql DB_URL='sqlserver://localhost:1433;database=master;username=sa;password=123ABabc!' ts-node --transpile-only ../node_modules/typeorm/cli.js migration:run -d typeorm.ts",
|
||||
"db:migration:run:sqlite": "cross-env DB_TYPE=sqlite DB_URL='file:///tmp/migration-sqlite.db' ts-node --transpile-only ../node_modules/typeorm/cli.js migration:run -d typeorm.ts",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "cross-env BOXYHQ_NO_ANALYTICS=1 tap --timeout=0 --allow-incomplete-coverage --allow-empty-coverage test/**/*.test.ts",
|
||||
"sort": "npx sort-package-json"
|
||||
|
@ -39,40 +41,42 @@
|
|||
"coverage-map": "map.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-dynamodb": "3.556.0",
|
||||
"@aws-sdk/credential-providers": "3.556.0",
|
||||
"@aws-sdk/util-dynamodb": "3.556.0",
|
||||
"@aws-sdk/client-dynamodb": "3.584.0",
|
||||
"@aws-sdk/credential-providers": "3.583.0",
|
||||
"@aws-sdk/util-dynamodb": "3.584.0",
|
||||
"@boxyhq/error-code-mnemonic": "0.1.1",
|
||||
"@boxyhq/metrics": "0.2.6",
|
||||
"@boxyhq/saml20": "1.5.0",
|
||||
"@googleapis/admin": "16.0.0",
|
||||
"axios": "1.6.8",
|
||||
"@boxyhq/metrics": "0.2.7",
|
||||
"@boxyhq/saml20": "1.5.1",
|
||||
"@googleapis/admin": "19.0.0",
|
||||
"@libsql/sqlite3": "0.3.1",
|
||||
"axios": "1.7.2",
|
||||
"encoding": "0.1.13",
|
||||
"jose": "5.2.4",
|
||||
"jose": "5.3.0",
|
||||
"lodash": "4.17.21",
|
||||
"mixpanel": "0.18.0",
|
||||
"mongodb": "6.5.0",
|
||||
"mongodb": "6.6.2",
|
||||
"mssql": "10.0.2",
|
||||
"mysql2": "3.9.7",
|
||||
"mysql2": "3.9.8",
|
||||
"node-forge": "1.3.1",
|
||||
"openid-client": "5.6.5",
|
||||
"pg": "8.11.5",
|
||||
"redis": "4.6.13",
|
||||
"redis": "4.6.14",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"ripemd160": "2.0.2",
|
||||
"sqlite3": "5.1.7",
|
||||
"typeorm": "0.3.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "8.4.1",
|
||||
"@types/lodash": "4.17.0",
|
||||
"@types/node": "20.12.7",
|
||||
"@types/lodash": "4.17.4",
|
||||
"@types/node": "20.12.12",
|
||||
"@types/sinon": "17.0.3",
|
||||
"@types/tap": "15.0.11",
|
||||
"cross-env": "7.0.3",
|
||||
"migrate-mongo": "11.0.0",
|
||||
"nock": "13.5.4",
|
||||
"sinon": "17.0.1",
|
||||
"tap": "18.7.2",
|
||||
"sinon": "18.0.0",
|
||||
"tap": "19.0.2",
|
||||
"ts-node": "10.9.2",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "5.4.5"
|
||||
|
|
|
@ -3,7 +3,7 @@ import {
|
|||
Storable,
|
||||
SAMLSSORecord,
|
||||
OIDCSSORecord,
|
||||
SSOTracerInstance,
|
||||
SSOTracesInstance,
|
||||
Records,
|
||||
Trace,
|
||||
} from '../typings';
|
||||
|
@ -12,11 +12,11 @@ import { transformConnections } from './utils';
|
|||
|
||||
export class AdminController implements IAdminController {
|
||||
private connectionStore: Storable;
|
||||
private ssoTracer: SSOTracerInstance;
|
||||
private ssoTraces: SSOTracesInstance;
|
||||
|
||||
constructor({ connectionStore, ssoTracer }) {
|
||||
constructor({ connectionStore, ssoTraces }) {
|
||||
this.connectionStore = connectionStore;
|
||||
this.ssoTracer = ssoTracer;
|
||||
this.ssoTraces = ssoTraces;
|
||||
}
|
||||
|
||||
public async getAllConnection(pageOffset?: number, pageLimit?: number, pageToken?: string) {
|
||||
|
@ -34,7 +34,7 @@ export class AdminController implements IAdminController {
|
|||
}
|
||||
|
||||
public async getAllSSOTraces(pageOffset: number, pageLimit: number, pageToken?: string) {
|
||||
const { data: traces, pageToken: nextPageToken } = (await this.ssoTracer.getAllTraces(
|
||||
const { data: traces, pageToken: nextPageToken } = (await this.ssoTraces.getAllTraces(
|
||||
pageOffset,
|
||||
pageLimit,
|
||||
pageToken
|
||||
|
@ -48,7 +48,7 @@ export class AdminController implements IAdminController {
|
|||
}
|
||||
|
||||
public async getSSOTraceById(traceId: string) {
|
||||
const trace = await this.ssoTracer.getByTraceId(traceId);
|
||||
const trace = await this.ssoTraces.getByTraceId(traceId);
|
||||
|
||||
if (!trace) {
|
||||
throw new JacksonError(`Trace with id ${traceId} not found`, 404);
|
||||
|
@ -63,6 +63,14 @@ export class AdminController implements IAdminController {
|
|||
pageLimit: number,
|
||||
pageToken?: string
|
||||
) {
|
||||
return await this.ssoTracer.getTracesByProduct({ product, pageOffset, pageLimit, pageToken });
|
||||
return await this.ssoTraces.getTracesByProduct({ product, pageOffset, pageLimit, pageToken });
|
||||
}
|
||||
|
||||
public async deleteTracesByProduct(product: string) {
|
||||
return await this.ssoTraces.deleteTracesByProduct(product);
|
||||
}
|
||||
|
||||
public async countByProduct(product: string) {
|
||||
return await this.ssoTraces.countByProduct(product);
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue