Merge branch 'main' into feature/issue-61/jackson-docker-compose

This commit is contained in:
Melissa Tamplin 2023-10-18 10:15:51 -04:00 committed by GitHub
commit 680dec30ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
90 changed files with 3492 additions and 6475 deletions

View File

@ -18,6 +18,8 @@ DB_CLEANUP_LIMIT=1000
DB_PAGE_LIMIT=50
# You can use openssl to generate a random 32 character key: openssl rand -base64 24
DB_ENCRYPTION_KEY=
# Uncomment below if you wish to run DB migrations manually.
#DB_MANUAL_MIGRATION=true
# Admin Portal settings
# SMTP details for Magic Links

4
.eslintignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
dist
npm/dist
npm/migration

View File

@ -1,9 +1,9 @@
---
name: Bug report
about: Report any issues with the platform
title: ""
title: ''
labels: bug
assignees: ""
assignees: ''
---
Found a bug? Please fill out the sections below. 👍

View File

@ -1,9 +1,9 @@
---
name: Feature request
about: Suggest a feature or idea
title: ""
title: ''
labels: enhancement
assignees: ""
assignees: ''
---
> Please check if your Feature Request has not been already raised in the [Discussions Tab](https://github.com/boxyhq/jackson/discussions), as we would like to reduce duplicates. If it has been already raised, simply upvote it 🔼.
@ -40,4 +40,4 @@ assignees: ""
You might want to link to related issues here, if you haven't already.
-->
(Write your answer here.)
(Write your answer here.)

View File

@ -139,7 +139,8 @@ jobs:
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
- run: npm run custom-install
- run: npm run lint
- run: npm run check-lint
- run: npm run check-format
- run: npm run build
- run: npm run test
working-directory: ./npm

View File

@ -5,6 +5,11 @@ public
**/**/.next
**/**/public
npm/migration/**
npm/dist/**
npm/.nyc_output/**
.vscode/**
npm/package-lock.json
*.lock
*.log

View File

@ -3,9 +3,9 @@ module.exports = {
bracketSameLine: true,
singleQuote: true,
jsxSingleQuote: true,
trailingComma: "es5",
trailingComma: 'es5',
semi: true,
printWidth: 110,
arrowParens: "always",
importOrderSeparation: true,
arrowParens: 'always',
// importOrderSeparation: true,
};

View File

@ -20,9 +20,7 @@
"tagAnnotation": "Release ${version}",
"tagArgs": [],
"push": true,
"pushArgs": [
"--follow-tags"
],
"pushArgs": ["--follow-tags"],
"pushRepo": ""
},
"npm": {
@ -48,4 +46,4 @@
"pr": ":rocket: _This pull request is included in v${version}. See [${releaseName}](${releaseUrl}) for release notes._"
}
}
}
}

View File

@ -1,4 +1,4 @@
ARG NODEJS_IMAGE=node:18.18.0-alpine3.18
ARG NODEJS_IMAGE=node:18.18.2-alpine3.18
FROM --platform=$BUILDPLATFORM $NODEJS_IMAGE AS base
# Install dependencies only when needed
@ -10,7 +10,7 @@ WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json package-lock.json ./
COPY npm npm
COPY prebuild.ts prebuild.ts
COPY migrate.sh prebuild.ts ./
RUN npm run custom-install
@ -31,7 +31,6 @@ ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build
# Production image, copy all the files and run next
FROM $NODEJS_IMAGE AS runner
WORKDIR /app
@ -53,8 +52,12 @@ COPY --from=builder /app/public ./public
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Support for DB migration
COPY --from=builder --chown=nextjs:nodejs /app/migrate.sh ./migrate.sh
COPY npm npm
RUN chmod +x migrate.sh
# mongodb peer dependency would be automatically installed for migrate-mongo
RUN npm install -g ts-node migrate-mongo typeorm reflect-metadata mssql mysql2 pg
USER nextjs
EXPOSE 5225

View File

@ -17,9 +17,10 @@
<h3 align="center">
[⬆️ Take a look at our Issues ⬆️](https://github.com/boxyhq/jackson/issues)
</h3>
___
---
<h3 align="center" >
<a href="https://boxyhq.com/docs/jackson/overview" rel="dofollow"><strong>· Explore the docs »</strong></a>
@ -27,8 +28,6 @@ ___
<a href="https://boxyhq.com/saas-registration" rel="dofollow"><strong>· SaaS Early Access »</strong></a>
</h3>
# ⭐️ SAML Jackson: Enterprise SSO made simple
<p>

View File

@ -1,4 +1,5 @@
import { useRouter } from 'next/router';
import { CreateSAMLConnection as CreateSAML, CreateOIDCConnection as CreateOIDC } from '@boxyhq/react-ui/sso';
import { errorToast } from '@components/Toaster';
@ -26,7 +27,7 @@ const CreateSSOConnection = ({ setupLinkToken, idpType }: CreateSSOConnectionPro
save: `/api/setup/${setupLinkToken}/sso-connection`,
};
const _CSS = { input: 'input input-bordered', button: { ctoa: 'btn btn-primary' } };
return idpType === 'saml' ? (
<CreateSAML
@ -34,6 +35,8 @@ const CreateSSOConnection = ({ setupLinkToken, idpType }: CreateSSOConnectionPro
urls={urls}
successCallback={onSuccess}
errorCallback={onError}
classNames={_CSS}
displayHeader={false}
/>
) : (
<CreateOIDC
@ -41,6 +44,8 @@ const CreateSSOConnection = ({ setupLinkToken, idpType }: CreateSSOConnectionPro
urls={urls}
successCallback={onSuccess}
errorCallback={onError}
classNames={_CSS}
displayHeader={false}
/>
);
};

View File

@ -34,12 +34,16 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
idp_hint: string;
};
const { redirectUrl } = await samlFederatedController.sso.getAuthorizeUrl({
const { redirect_url, authorize_form } = await samlFederatedController.sso.getAuthorizeUrl({
request: SAMLRequest,
relayState: RelayState,
idp_hint,
});
res.redirect(302, redirectUrl);
return;
if (redirect_url) {
res.redirect(302, redirect_url);
} else {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.send(authorize_form);
}
};

View File

@ -16,7 +16,7 @@ spec:
spec:
containers:
- name: jackson
image: boxyhq/jackson:tagwillbereplaced
image: boxyhq/jackson-local
imagePullPolicy: IfNotPresent
startupProbe:
httpGet:

View File

@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: jackson-internal
namespace: '{{repl ConfigOption "namespace"}}'
labels:
app: jackson-internal
tier: jackson-internal
spec:
ports:
- name: original
port: 5225
targetPort: 5225
selector:
app: jackson
tier: jackson

View File

@ -0,0 +1,4 @@
resources:
- ./jackson-internal-service.yaml
namespace: default

View File

@ -1,3 +1,6 @@
bases:
- ./internal
resources:
- ./jackson-service.yaml

View File

@ -1,2 +1,2 @@
*-secrets.yaml
secrets.yaml
**/*-secrets.yaml
**/secrets.yaml

View File

@ -0,0 +1,12 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: jackson
spec:
replicas: 1
template:
spec:
containers:
- name: jackson
image: boxyhq/jackson:tagwillbereplaced
imagePullPolicy: IfNotPresent

View File

@ -4,7 +4,15 @@ bases:
resources:
- ./secrets.yaml
- ./mocksaml-secrets.yaml
- ./mocksaml-deployment.yaml
- ./migratepg-job.yaml
patches:
- ./jackson-deployment.yaml
images:
- name: boxyhq/jackson
newTag: 1.13.0
newTag: 1.14.0
- name: boxyhq/mock-saml
newTag: 1.1.7

View File

@ -0,0 +1,18 @@
apiVersion: batch/v1
kind: Job
metadata:
name: jackson-migrate-pg
spec:
template:
spec:
restartPolicy: 'OnFailure'
containers:
- name: db
image: boxyhq/jackson:tagwillbereplaced
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- migrate.sh
envFrom:
- secretRef:
name: jackson

View File

@ -0,0 +1,44 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mocksaml
spec:
selector:
matchLabels:
tier: mocksaml
replicas: 1
template:
metadata:
labels:
app: mocksaml
tier: mocksaml
spec:
containers:
- name: mocksaml
image: boxyhq/mock-saml:tagwillbereplaced
imagePullPolicy: IfNotPresent
startupProbe:
httpGet:
port: 4000
path: /api/health
periodSeconds: 10
failureThreshold: 5
readinessProbe:
httpGet:
port: 4000
path: /api/health
periodSeconds: 30
failureThreshold: 5
successThreshold: 2
ports:
- containerPort: 4000
name: http
protocol: TCP
envFrom:
- secretRef:
name: mocksaml
resources:
requests:
cpu: 100m
limits:
cpu: 500m

View File

@ -11,13 +11,11 @@ metadata:
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-ssl-cert: 'arn:aws:acm:us-west-1:511214097407:certificate/3cd477cb-f54b-4701-89f1-9416216cb1c9'
service.beta.kubernetes.io/aws-load-balancer-ssl-ports: '443'
service.beta.kubernetes.io/aws-load-balancer-ssl-negotiation-policy: ELBSecurityPolicy-TLS13-1-2-2021-06
spec:
externalTrafficPolicy: Local
type: LoadBalancer
ports:
- name: http
port: 80
targetPort: 5225
- name: https
port: 443
targetPort: 5225

View File

@ -1,3 +1,5 @@
---
bases:
- ../../../base/services/internal
- ./jackson-service.yaml
- ./mocksaml-service.yaml

View File

@ -0,0 +1,24 @@
apiVersion: v1
kind: Service
metadata:
name: mocksaml
labels:
app: mocksaml
tier: mocksaml
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: nlb
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-ssl-cert: 'arn:aws:acm:us-west-1:511214097407:certificate/10249968-7517-424c-a7c7-cd66b4310752'
service.beta.kubernetes.io/aws-load-balancer-ssl-ports: '443'
service.beta.kubernetes.io/aws-load-balancer-ssl-negotiation-policy: ELBSecurityPolicy-TLS13-1-2-2021-06
spec:
externalTrafficPolicy: Local
type: LoadBalancer
ports:
- name: https
port: 443
targetPort: 4000
selector:
app: mocksaml
tier: mocksaml

View File

@ -6,6 +6,7 @@ bases:
resources:
- ./secrets.yaml
- ./migratepg-job.yaml
commonLabels:
jacksondev: '1'

View File

@ -0,0 +1,18 @@
apiVersion: batch/v1
kind: Job
metadata:
name: jackson-migrate-pg
spec:
template:
spec:
restartPolicy: 'OnFailure'
containers:
- name: db
image: boxyhq/jackson-local
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- migrate.sh
envFrom:
- secretRef:
name: jackson

View File

@ -34,3 +34,4 @@ stringData:
RETRACED_EXTERNAL_URL: 'http://localhost:3000/auditlog'
RETRACED_ADMIN_ROOT_TOKEN: 'dev'
BOXYHQ_LICENSE_KEY: ''
DB_MANUAL_MIGRATION: 'true'

2
kustomize/overlays/prod-eu/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
**/*-secrets.yaml
**/secrets.yaml

View File

@ -0,0 +1,12 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: jackson
spec:
replicas: 1
template:
spec:
containers:
- name: jackson
image: boxyhq/jackson:tagwillbereplaced
imagePullPolicy: IfNotPresent

View File

@ -0,0 +1,18 @@
---
bases:
- ../../base
resources:
- ./secrets.yaml
- ./mocksaml-secrets.yaml
- ./mocksaml-deployment.yaml
- ./migratepg-job.yaml
patches:
- ./jackson-deployment.yaml
images:
- name: boxyhq/jackson
newTag: 1.14.0
- name: boxyhq/mock-saml
newTag: 1.1.7

View File

@ -0,0 +1,18 @@
apiVersion: batch/v1
kind: Job
metadata:
name: jackson-migrate-pg
spec:
template:
spec:
restartPolicy: 'OnFailure'
containers:
- name: db
image: boxyhq/jackson:tagwillbereplaced
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- migrate.sh
envFrom:
- secretRef:
name: jackson

View File

@ -0,0 +1,44 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mocksaml
spec:
selector:
matchLabels:
tier: mocksaml
replicas: 1
template:
metadata:
labels:
app: mocksaml
tier: mocksaml
spec:
containers:
- name: mocksaml
image: boxyhq/mock-saml:tagwillbereplaced
imagePullPolicy: IfNotPresent
startupProbe:
httpGet:
port: 4000
path: /api/health
periodSeconds: 10
failureThreshold: 5
readinessProbe:
httpGet:
port: 4000
path: /api/health
periodSeconds: 30
failureThreshold: 5
successThreshold: 2
ports:
- containerPort: 4000
name: http
protocol: TCP
envFrom:
- secretRef:
name: mocksaml
resources:
requests:
cpu: 100m
limits:
cpu: 500m

View File

@ -0,0 +1,24 @@
apiVersion: v1
kind: Service
metadata:
name: jackson
labels:
app: jackson
tier: jackson
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: nlb
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-ssl-cert: 'arn:aws:acm:eu-central-1:511214097407:certificate/3fc4272a-d97c-4bf0-8f8a-3425fda27e31'
service.beta.kubernetes.io/aws-load-balancer-ssl-ports: '443'
service.beta.kubernetes.io/aws-load-balancer-ssl-negotiation-policy: ELBSecurityPolicy-TLS13-1-2-2021-06
spec:
externalTrafficPolicy: Local
type: LoadBalancer
ports:
- name: https
port: 443
targetPort: 5225
selector:
app: jackson
tier: jackson

View File

@ -0,0 +1,5 @@
---
bases:
- ../../../base/services/internal
- ./jackson-service.yaml
- ./mocksaml-service.yaml

View File

@ -0,0 +1,24 @@
apiVersion: v1
kind: Service
metadata:
name: mocksaml
labels:
app: mocksaml
tier: mocksaml
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: nlb
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-ssl-cert: 'arn:aws:acm:eu-central-1:511214097407:certificate/7eab394e-9a01-46c4-b941-82288a4479e9'
service.beta.kubernetes.io/aws-load-balancer-ssl-ports: '443'
service.beta.kubernetes.io/aws-load-balancer-ssl-negotiation-policy: ELBSecurityPolicy-TLS13-1-2-2021-06
spec:
externalTrafficPolicy: Local
type: LoadBalancer
ports:
- name: https
port: 443
targetPort: 4000
selector:
app: mocksaml
tier: mocksaml

View File

@ -43,6 +43,7 @@ const db: DatabaseOption = {
readCapacityUnits: process.env.DB_DYNAMODB_RCUS ? Number(process.env.DB_DYNAMODB_RCUS) : undefined,
writeCapacityUnits: process.env.DB_DYNAMODB_RCUS ? Number(process.env.DB_DYNAMODB_WCUS) : undefined,
},
manualMigration: process.env.DB_MANUAL_MIGRATION === 'true',
};
const jacksonOptions: JacksonOption = {

View File

@ -61,14 +61,13 @@ export const strategyChecker = (req: NextApiRequest): { isSAML: boolean; isOIDC:
// The oidcMetadata JSON will be parsed here
export const oidcMetadataParse = (
body:
| (
| OIDCSSOConnectionWithDiscoveryUrl
| (Omit<OIDCSSOConnectionWithMetadata, 'oidcMetadata'> & { oidcMetadata: string })
) & {
clientID: string;
clientSecret: string;
}
body: (
| OIDCSSOConnectionWithDiscoveryUrl
| (Omit<OIDCSSOConnectionWithMetadata, 'oidcMetadata'> & { oidcMetadata: string })
) & {
clientID: string;
clientSecret: string;
}
) => {
if (!body.oidcDiscoveryUrl && typeof body.oidcMetadata === 'string') {
try {

21
migrate.sh Executable file
View File

@ -0,0 +1,21 @@
#!/bin/sh
echo "Initiating Migration..."
export NODE_PATH=$(npm root -g)
cd ./npm
if [ "$DB_ENGINE" = "mongo" ]
then
migrate-mongo up
else
ts-node --transpile-only --project tsconfig.json $NODE_PATH/typeorm/cli.js migration:run -d ./typeorm.ts
fi
if [ $? -eq 1 ]
then
echo "Migration Failed..."
exit 1
fi
echo "Migration Finished..."
cd ..

View File

@ -1,2 +1,3 @@
node_modules
dist
dist
npm/dist

View File

@ -0,0 +1,22 @@
const config = {
mongodb: {
url: process.env.DB_URL || 'mongodb://localhost:27017/jackson',
options: {
useNewUrlParser: true, // removes a deprecation warning when connecting
useUnifiedTopology: true, // removes a deprecating warning when connecting
// connectTimeoutMS: 3600000, // increase connection timeout to 1 hour
// socketTimeoutMS: 3600000, // increase socket timeout to 1 hour
},
},
migrationsDir: 'migration/mongo',
changelogCollectionName: 'changelog',
migrationFileExtension: '.js',
// Enable the algorithm to create a checksum of the file contents and use that in the comparison to determine
// if the file should be run. Requires that scripts are coded to be run multiple times.
useFileHash: false,
// Don't change this, unless you know what you're doing
moduleSystem: 'commonjs',
};
module.exports = config;

View File

@ -1,23 +0,0 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class Initial1640877418166 implements MigrationInterface {
name = 'Initial1640877418166'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE \`jackson_store\` (\`key\` varchar(1500) NOT NULL, \`value\` text NOT NULL, \`iv\` varchar(64) NULL, \`tag\` varchar(64) NULL, PRIMARY KEY (\`key\`)) ENGINE=InnoDB`);
await queryRunner.query(`CREATE TABLE \`jackson_index\` (\`id\` int NOT NULL AUTO_INCREMENT, \`key\` varchar(1500) NOT NULL, \`storeKey\` varchar(1500) NOT NULL, INDEX \`_jackson_index_key\` (\`key\`), INDEX \`_jackson_index_key_store\` (\`key\`, \`storeKey\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
await queryRunner.query(`CREATE TABLE \`jackson_ttl\` (\`key\` varchar(1500) NOT NULL, \`expiresAt\` bigint NOT NULL, INDEX \`_jackson_ttl_expires_at\` (\`expiresAt\`), PRIMARY KEY (\`key\`)) ENGINE=InnoDB`);
await queryRunner.query(`ALTER TABLE \`jackson_index\` ADD CONSTRAINT \`FK_937b040fb2592b4671cbde09e83\` FOREIGN KEY (\`storeKey\`) REFERENCES \`jackson_store\`(\`key\`) ON DELETE CASCADE ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`jackson_index\` DROP FOREIGN KEY \`FK_937b040fb2592b4671cbde09e83\``);
await queryRunner.query(`DROP INDEX \`_jackson_ttl_expires_at\` ON \`jackson_ttl\``);
await queryRunner.query(`DROP TABLE \`jackson_ttl\``);
await queryRunner.query(`DROP INDEX \`_jackson_index_key_store\` ON \`jackson_index\``);
await queryRunner.query(`DROP INDEX \`_jackson_index_key\` ON \`jackson_index\``);
await queryRunner.query(`DROP TABLE \`jackson_index\``);
await queryRunner.query(`DROP TABLE \`jackson_store\``);
}
}

View File

@ -1,16 +0,0 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class createdAt1644332636666 implements MigrationInterface {
name = 'createdAt1644332636666'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`jackson_store\` ADD \`createdAt\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP()`);
await queryRunner.query(`ALTER TABLE \`jackson_store\` ADD \`modifiedAt\` timestamp NULL`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`jackson_store\` DROP COLUMN \`modifiedAt\``);
await queryRunner.query(`ALTER TABLE \`jackson_store\` DROP COLUMN \`createdAt\``);
}
}

View File

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MdNamespace1692767993709 implements MigrationInterface {
name = 'MdNamespace1692767993709'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`jackson_store\` ADD \`namespace\` varchar(64) NULL`);
await queryRunner.query(`CREATE INDEX \`_jackson_store_namespace\` ON \`jackson_store\` (\`namespace\`)`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX \`_jackson_store_namespace\` ON \`jackson_store\``);
await queryRunner.query(`ALTER TABLE \`jackson_store\` DROP COLUMN \`namespace\``);
}
}

View File

@ -0,0 +1,23 @@
module.exports = {
async up(db, client) {
const collection = db.collection('jacksonStore');
const response = await collection.distinct('_id', {});
const searchTerm = ':';
for (const k in response) {
const key = response[k].toString();
const tokens2 = key.split(searchTerm).slice(0, 2);
const value = tokens2.join(searchTerm);
await db.collection('jacksonStore').updateOne({ _id: key }, {$set: { namespace: value }});
}
},
async down(db, client) {
const collection = db.collection('jacksonStore');
const response = await collection.distinct('_id', {});
for (const k in response) {
const key = response[k].toString();
await db.collection('jacksonStore').updateOne({ _id: key }, {$set: { namespace: '' }});
}
}
};

View File

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MssNamespace1692767993709 implements MigrationInterface {
name = 'MssNamespace1692767993709'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "jackson_store" ADD "namespace" varchar(64)`);
await queryRunner.query(`CREATE INDEX "_jackson_store_namespace" ON "jackson_store" ("namespace") `);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "_jackson_store_namespace" ON "jackson_store"`);
await queryRunner.query(`ALTER TABLE "jackson_store" DROP COLUMN "namespace"`);
}
}

View File

@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from "typeorm"
export class namespace1692817789888 implements MigrationInterface {
name = 'namespace1692817789888'
public async up(queryRunner: QueryRunner): Promise<void> {
const response = await queryRunner.query("select jackson.[key] from jackson_store jackson")
const searchTerm = ':';
for (const k in response) {
const key = response[k].key;
const tokens2 = key.split(searchTerm).slice(0, 2);
const value = tokens2.join(searchTerm);
queryRunner.query(`update jackson_store set namespace = '${value}' where jackson_store.[key] = '${key}'`)
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
const response = await queryRunner.query("select jackson.[key] from jackson_store jackson")
for (const k in response) {
const key = response[k].key;
queryRunner.query(`update jackson_store set namespace = NULL where jackson_store.[key] = '${key}'`)
}
}
}

View File

@ -1,23 +0,0 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class Initial1640877358925 implements MigrationInterface {
name = 'Initial1640877358925'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE \`jackson_store\` (\`key\` varchar(1500) NOT NULL, \`value\` text NOT NULL, \`iv\` varchar(64) NULL, \`tag\` varchar(64) NULL, PRIMARY KEY (\`key\`)) ENGINE=InnoDB`);
await queryRunner.query(`CREATE TABLE \`jackson_index\` (\`id\` int NOT NULL AUTO_INCREMENT, \`key\` varchar(1500) NOT NULL, \`storeKey\` varchar(1500) NOT NULL, INDEX \`_jackson_index_key\` (\`key\`), INDEX \`_jackson_index_key_store\` (\`key\`, \`storeKey\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
await queryRunner.query(`CREATE TABLE \`jackson_ttl\` (\`key\` varchar(1500) NOT NULL, \`expiresAt\` bigint NOT NULL, INDEX \`_jackson_ttl_expires_at\` (\`expiresAt\`), PRIMARY KEY (\`key\`)) ENGINE=InnoDB`);
await queryRunner.query(`ALTER TABLE \`jackson_index\` ADD CONSTRAINT \`FK_937b040fb2592b4671cbde09e83\` FOREIGN KEY (\`storeKey\`) REFERENCES \`jackson_store\`(\`key\`) ON DELETE CASCADE ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`jackson_index\` DROP FOREIGN KEY \`FK_937b040fb2592b4671cbde09e83\``);
await queryRunner.query(`DROP INDEX \`_jackson_ttl_expires_at\` ON \`jackson_ttl\``);
await queryRunner.query(`DROP TABLE \`jackson_ttl\``);
await queryRunner.query(`DROP INDEX \`_jackson_index_key_store\` ON \`jackson_index\``);
await queryRunner.query(`DROP INDEX \`_jackson_index_key\` ON \`jackson_index\``);
await queryRunner.query(`DROP TABLE \`jackson_index\``);
await queryRunner.query(`DROP TABLE \`jackson_store\``);
}
}

View File

@ -1,16 +0,0 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class createdAt1644332641078 implements MigrationInterface {
name = 'createdAt1644332641078'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`jackson_store\` ADD \`createdAt\` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP`);
await queryRunner.query(`ALTER TABLE \`jackson_store\` ADD \`modifiedAt\` timestamp NULL`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`jackson_store\` DROP COLUMN \`modifiedAt\``);
await queryRunner.query(`ALTER TABLE \`jackson_store\` DROP COLUMN \`createdAt\``);
}
}

View File

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class msNamespace1692767993709 implements MigrationInterface {
name = 'msNamespace1692767993709'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`jackson_store\` ADD \`namespace\` varchar(64) NULL`);
await queryRunner.query(`CREATE INDEX \`_jackson_store_namespace\` ON \`jackson_store\` (\`namespace\`)`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX \`_jackson_store_namespace\` ON \`jackson_store\``);
await queryRunner.query(`ALTER TABLE \`jackson_store\` DROP COLUMN \`namespace\``);
}
}

View File

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MsNamespace1692767993709 implements MigrationInterface {
name = 'MsNamespace1692767993709'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`jackson_store\` ADD \`namespace\` varchar(64) NULL`);
await queryRunner.query(`CREATE INDEX \`_jackson_store_namespace\` ON \`jackson_store\` (\`namespace\`)`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX \`_jackson_store_namespace\` ON \`jackson_store\``);
await queryRunner.query(`ALTER TABLE \`jackson_store\` DROP COLUMN \`namespace\``);
}
}

View File

@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class PgNamespace1692767993709 implements MigrationInterface {
name = 'PgNamespace1692767993709'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "jackson_store" ADD "namespace" character varying(64)`);
await queryRunner.query(`CREATE INDEX "_jackson_store_namespace" ON "jackson_store" ("namespace") `);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "public"."_jackson_store_namespace"`);
await queryRunner.query(`ALTER TABLE "jackson_store" DROP COLUMN "namespace"`);
}
}

View File

@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from "typeorm"
export class namespace1692817789888 implements MigrationInterface {
name = 'namespace1692817789888'
public async up(queryRunner: QueryRunner): Promise<void> {
const response = await queryRunner.query("select jackson.key from jackson_store jackson")
const searchTerm = ':';
for (const k in response) {
const key = response[k].key;
const tokens2 = key.split(searchTerm).slice(0, 2);
const value = tokens2.join(searchTerm);
queryRunner.query(`update jackson_store set namespace = '${value}' where jackson_store.key = '${key}'`)
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
const response = await queryRunner.query("select jackson.key from jackson_store jackson")
for (const k in response) {
const key = response[k].key;
queryRunner.query(`update jackson_store set namespace = NULL where jackson_store.key = '${key}'`)
}
}
}

1570
npm/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -20,13 +20,16 @@
"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/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_SSL=true 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",
"prepublishOnly": "npm run build",
"test": "cross-env BOXYHQ_NO_ANALYTICS=1 tap --timeout=0 --allow-incomplete-coverage --allow-empty-coverage test/**/*.test.ts",
@ -36,23 +39,23 @@
"coverage-map": "map.js"
},
"dependencies": {
"@aws-sdk/client-dynamodb": "3.425.0",
"@aws-sdk/credential-providers": "3.425.0",
"@aws-sdk/util-dynamodb": "3.425.0",
"@aws-sdk/client-dynamodb": "3.429.0",
"@aws-sdk/credential-providers": "3.429.0",
"@aws-sdk/util-dynamodb": "3.429.0",
"@boxyhq/error-code-mnemonic": "0.1.1",
"@boxyhq/metrics": "0.2.5",
"@boxyhq/saml20": "1.2.4",
"@googleapis/admin": "12.4.0",
"@googleapis/admin": "13.0.0",
"axios": "1.5.1",
"encoding": "0.1.13",
"jose": "4.15.2",
"jose": "4.15.4",
"lodash": "4.17.21",
"mixpanel": "0.18.0",
"mongodb": "6.1.0",
"mssql": "10.0.1",
"mysql2": "3.6.1",
"mysql2": "3.6.2",
"node-forge": "1.3.1",
"openid-client": "5.6.0",
"openid-client": "5.6.1",
"pg": "8.11.3",
"redis": "4.6.10",
"reflect-metadata": "0.1.13",
@ -62,15 +65,16 @@
"xmlbuilder": "15.1.1"
},
"devDependencies": {
"@faker-js/faker": "8.1.0",
"@faker-js/faker": "8.2.0",
"@types/lodash": "4.14.199",
"@types/node": "20.8.2",
"@types/sinon": "10.0.18",
"@types/node": "20.8.6",
"@types/sinon": "10.0.19",
"@types/tap": "15.0.9",
"cross-env": "7.0.3",
"nock": "13.3.3",
"migrate-mongo": "11.0.0",
"nock": "13.3.4",
"sinon": "16.1.0",
"tap": "18.4.3",
"tap": "18.5.2",
"ts-node": "10.9.1",
"tsconfig-paths": "4.2.0",
"typescript": "5.2.2"

View File

@ -31,9 +31,12 @@ export class AnalyticsController {
await this.send();
}
setInterval(async () => {
await this.send();
}, 60 * 60 * 24 * 1000);
setInterval(
async () => {
await this.send();
},
60 * 60 * 24 * 1000
);
}
async send() {
@ -45,9 +48,12 @@ export class AnalyticsController {
},
(err: Error | undefined) => {
if (err) {
setTimeout(() => {
this.send();
}, 1000 * 60 * 60);
setTimeout(
() => {
this.send();
},
1000 * 60 * 60
);
return;
}

View File

@ -486,7 +486,7 @@ export class OAuthController implements IOAuthController {
public async samlResponse(
body: SAMLResponsePayload
): Promise<{ redirect_url?: string; app_select_form?: string; responseForm?: string }> {
): Promise<{ redirect_url?: string; app_select_form?: string; response_form?: string }> {
let connection: SAMLSSORecord | undefined;
let rawResponse: string | undefined;
let sessionId: string | undefined;
@ -535,7 +535,7 @@ export class OAuthController implements IOAuthController {
}
isSAMLFederated = session && 'samlFederated' in session;
const isSPFflow = !isIdPFlow && !isSAMLFederated;
const isSPFlow = !isIdPFlow && !isSAMLFederated;
// IdP initiated SSO flow
if (isIdPFlow) {
@ -563,7 +563,7 @@ export class OAuthController implements IOAuthController {
// SP initiated SSO flow
// Resolve if there are multiple matches for SP login
if (isSPFflow) {
if (isSPFlow || isSAMLFederated) {
connection = connections.filter((c) => {
return (
c.clientID === session.requested.client_id ||
@ -572,10 +572,6 @@ export class OAuthController implements IOAuthController {
})[0];
}
if (!connection) {
connection = connections[0];
}
if (!connection) {
throw new JacksonError('SAML connection not found.', 403);
}
@ -630,7 +626,7 @@ export class OAuthController implements IOAuthController {
await this.sessionStore.delete(sessionId);
return { responseForm };
return { response_form: responseForm };
}
const code = await this._buildAuthorizationCode(connection, profile, session, isIdPFlow);

View File

@ -1,7 +1,4 @@
export const redirect = (
redirectUrl: string,
redirectUrls: string[]
): boolean => {
export const redirect = (redirectUrl: string, redirectUrls: string[]): boolean => {
const url: URL = new URL(redirectUrl);
for (const idx in redirectUrls) {
@ -9,11 +6,7 @@ export const redirect = (
// TODO: Check pathname, for now pathname is ignored
if (
rUrl.protocol === url.protocol &&
rUrl.hostname === url.hostname &&
rUrl.port === url.port
) {
if (rUrl.protocol === url.protocol && rUrl.hostname === url.hostname && rUrl.port === url.port) {
return true;
}
}

View File

@ -5,7 +5,5 @@ export const transformBase64 = (input: string): string => {
};
export const encode = (code_challenge: string): string => {
return transformBase64(
crypto.createHash('sha256').update(code_challenge).digest('base64')
);
return transformBase64(crypto.createHash('sha256').update(code_challenge).digest('base64'));
};

View File

@ -11,6 +11,7 @@ import { JacksonError } from './error';
import { IndexNames } from './utils';
import { relayStatePrefix } from './utils';
import { createSAMLResponse } from '../saml/lib';
import * as redirect from './oauth/redirect';
const deflateRawAsync = promisify(deflateRaw);
@ -134,21 +135,34 @@ export class SAMLHandler {
return { connection: connections[0] };
}
async createSAMLRequest(params: {
connection: SAMLSSORecord;
requestParams: Record<string, any>;
}): Promise<{ redirectUrl: string }> {
async createSAMLRequest(params: { connection: SAMLSSORecord; requestParams: Record<string, any> }) {
const { connection, requestParams } = params;
// We have a connection now, so we can create the SAML request
const certificate = await getDefaultCertificate();
const { sso } = connection.idpMetadata;
let ssoUrl;
let post = false;
if ('redirectUrl' in sso) {
ssoUrl = sso.redirectUrl;
} else if ('postUrl' in sso) {
ssoUrl = sso.postUrl;
post = true;
}
const samlRequest = saml.request({
ssoUrl: connection.idpMetadata.sso.redirectUrl,
ssoUrl,
entityID: `${this.opts.samlAudience}`,
callbackUrl: `${this.opts.externalUrl}/api/oauth/saml`,
callbackUrl: this.opts.externalUrl + this.opts.samlPath,
signingKey: certificate.privateKey,
publicKey: certificate.publicKey,
forceAuthn: !!connection.forceAuthn,
identifierFormat: connection.identifierFormat
? connection.identifierFormat
: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
});
// Create a new session to store SP request information
@ -156,23 +170,40 @@ export class SAMLHandler {
await this.session.put(sessionId, {
id: samlRequest.id,
request: {
requested: {
...requestParams,
client_id: connection.clientID,
},
samlFederated: true,
});
// Create URL to redirect to the Identity Provider
const url = new URL(`${connection.idpMetadata.sso.redirectUrl}`);
const relayState = `${relayStatePrefix}${sessionId}`;
url.searchParams.set('RelayState', `${relayStatePrefix}${sessionId}`);
url.searchParams.set(
'SAMLRequest',
Buffer.from(await deflateRawAsync(samlRequest.request)).toString('base64')
);
let redirectUrl;
let authorizeForm;
// Decide whether to use HTTP Redirect or HTTP POST binding
if (!post) {
redirectUrl = redirect.success(ssoUrl, {
RelayState: relayState,
SAMLRequest: Buffer.from(await deflateRawAsync(samlRequest.request)).toString('base64'),
});
} else {
authorizeForm = saml.createPostForm(ssoUrl, [
{
name: 'RelayState',
value: relayState,
},
{
name: 'SAMLRequest',
value: Buffer.from(samlRequest.request).toString('base64'),
},
]);
}
return {
redirectUrl: url.toString(),
redirect_url: redirectUrl,
authorize_form: authorizeForm,
};
}
@ -183,18 +214,18 @@ export class SAMLHandler {
try {
const responseSigned = await createSAMLResponse({
audience: session.request.entityId,
acsUrl: session.request.acsUrl,
requestId: session.request.id,
audience: session.requested.entityId,
acsUrl: session.requested.acsUrl,
requestId: session.requested.id,
issuer: `${this.opts.samlAudience}`,
profile,
...certificate,
});
const responseForm = saml.createPostForm(session.request.acsUrl, [
const responseForm = saml.createPostForm(session.requested.acsUrl, [
{
name: 'RelayState',
value: session.request.relayState,
value: session.requested.relayState,
},
{
name: 'SAMLResponse',

View File

@ -11,6 +11,7 @@ export default function defaultDb(opts: JacksonOption) {
opts.db.dynamodb.region = opts.db.dynamodb.region || 'us-east-1';
opts.db.dynamodb.readCapacityUnits = opts.db.dynamodb.readCapacityUnits || 5;
opts.db.dynamodb.writeCapacityUnits = opts.db.dynamodb.writeCapacityUnits || 5;
opts.db.manualMigration = opts.db.manualMigration || false;
return opts;
}

View File

@ -6,6 +6,7 @@ type _Document = {
value: Encrypted;
expiresAt?: Date;
modifiedAt: string;
namespace: string;
indexes: string[];
};
@ -34,10 +35,38 @@ class Mongo implements DatabaseDriver {
await this.collection.createIndex({ indexes: 1 });
await this.collection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 1 });
await this.collection.createIndex({ namespace: 1 });
// eslint-disable-next-line no-constant-condition
while (true) {
try {
if (!this.options.manualMigration) {
await this.indexNamespace();
}
break;
} catch (err) {
console.error(
`error in index namespace execution for db engine: ${this.options.engine}, err: ${err}`
);
await dbutils.sleep(1000);
continue;
}
}
return this;
}
async indexNamespace() {
const docs = await this.collection.find({ namespace: { $exists: false } }).toArray();
const searchTerm = ':';
for (const doc of docs || []) {
const tokens2 = doc._id.toString().split(searchTerm).slice(0, 2);
const namespace = tokens2.join(searchTerm);
await this.collection.updateOne({ _id: doc._id }, { $set: { namespace } });
}
}
async get(namespace: string, key: string): Promise<any> {
const res = await this.collection.findOne({
_id: dbutils.key(namespace, key) as any,
@ -51,9 +80,8 @@ class Mongo implements DatabaseDriver {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getAll(namespace: string, pageOffset?: number, pageLimit?: number, _?: string): Promise<Records> {
const _namespaceMatch = new RegExp(`^${namespace}:.*`);
const docs = await this.collection
.find({ _id: _namespaceMatch }, { sort: { createdAt: -1 }, skip: pageOffset, limit: pageLimit })
.find({ namespace: namespace }, { sort: { createdAt: -1 }, skip: pageOffset, limit: pageLimit })
.toArray();
if (docs) {
@ -102,7 +130,7 @@ class Mongo implements DatabaseDriver {
if (ttl) {
doc.expiresAt = new Date(Date.now() + ttl * 1000);
}
doc.namespace = namespace;
// no ttl support for secondary indexes
for (const idx of indexes || []) {
const idxKey = dbutils.keyForIndex(namespace, idx);

View File

@ -1,7 +1,7 @@
import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
@Index('_jackson_index_key_store', ['key', 'storeKey'])
@Entity()
@Entity({ name: 'jackson_index' })
export class JacksonIndex {
@PrimaryGeneratedColumn()
id!: number;

View File

@ -1,6 +1,6 @@
import { Entity, Column } from 'typeorm';
import { Entity, Column, Index } from 'typeorm';
@Entity()
@Entity({ name: 'jackson_store' })
export class JacksonStore {
@Column({
primary: true,
@ -40,4 +40,12 @@ export class JacksonStore {
nullable: true,
})
modifiedAt?: string;
@Index('_jackson_store_namespace')
@Column({
type: 'varchar',
length: 64,
nullable: true,
})
namespace?: string;
}

View File

@ -1,6 +1,6 @@
import { Entity, Column, Index } from 'typeorm';
@Entity()
@Entity({ name: 'jackson_ttl' })
export class JacksonTTL {
@Column({
primary: true,

View File

@ -3,7 +3,7 @@ import { JacksonStore } from './JacksonStore';
import { Entity, PrimaryGeneratedColumn, Column, Index, ManyToOne } from 'typeorm';
@Index('_jackson_index_key_store', ['key', 'storeKey'])
@Entity()
@Entity({ name: 'jackson_index' })
export class JacksonIndex {
@PrimaryGeneratedColumn()
id!: number;

View File

@ -1,6 +1,6 @@
import { Entity, Column } from 'typeorm';
import { Entity, Column, Index } from 'typeorm';
@Entity()
@Entity({ name: 'jackson_store' })
export class JacksonStore {
@Column({
primary: true,
@ -40,4 +40,12 @@ export class JacksonStore {
nullable: true,
})
modifiedAt?: string;
@Index('_jackson_store_namespace')
@Column({
type: 'varchar',
length: 64,
nullable: true,
})
namespace?: string;
}

View File

@ -1,6 +1,6 @@
import { Entity, Column, Index } from 'typeorm';
@Entity()
@Entity({ name: 'jackson_ttl' })
export class JacksonTTL {
@Column({
primary: true,

View File

@ -3,7 +3,7 @@ import { JacksonStore } from './JacksonStore';
import { Entity, PrimaryGeneratedColumn, Column, Index, ManyToOne } from 'typeorm';
@Index('_jackson_index_key_store', ['key', 'storeKey'])
@Entity()
@Entity({ name: 'jackson_index' })
export class JacksonIndex {
@PrimaryGeneratedColumn()
id!: number;

View File

@ -1,6 +1,6 @@
import { Entity, Column } from 'typeorm';
import { Entity, Column, Index } from 'typeorm';
@Entity()
@Entity({ name: 'jackson_store' })
export class JacksonStore {
@Column({
primary: true,
@ -40,4 +40,12 @@ export class JacksonStore {
nullable: true,
})
modifiedAt?: string;
@Index('_jackson_store_namespace')
@Column({
type: 'varchar',
length: 64,
nullable: true,
})
namespace?: string;
}

View File

@ -1,6 +1,6 @@
import { Entity, Column, Index } from 'typeorm';
@Entity()
@Entity({ name: 'jackson_ttl' })
export class JacksonTTL {
@Column({
primary: true,

View File

@ -3,7 +3,7 @@ import { JacksonStore } from './JacksonStore';
import { Entity, PrimaryGeneratedColumn, Column, Index, ManyToOne } from 'typeorm';
@Index('_jackson_index_key_store', ['key', 'storeKey'])
@Entity()
@Entity({ name: 'jackson_index' })
export class JacksonIndex {
@PrimaryGeneratedColumn()
id!: number;

View File

@ -1,6 +1,6 @@
import { Entity, Column } from 'typeorm';
import { Entity, Column, Index } from 'typeorm';
@Entity()
@Entity({ name: 'jackson_store' })
export class JacksonStore {
@Column({
primary: true,
@ -40,4 +40,12 @@ export class JacksonStore {
nullable: true,
})
modifiedAt?: string;
@Index('_jackson_store_namespace')
@Column({
type: 'varchar',
length: 64,
nullable: true,
})
namespace?: string;
}

View File

@ -1,6 +1,6 @@
import { Entity, Column, Index } from 'typeorm';
@Entity()
@Entity({ name: 'jackson_ttl' })
export class JacksonTTL {
@Column({
primary: true,

View File

@ -3,7 +3,7 @@
require('reflect-metadata');
import { DatabaseDriver, DatabaseOption, Index, Encrypted, Records } from '../../typings';
import { DataSource, DataSourceOptions, Like, In } from 'typeorm';
import { DataSource, DataSourceOptions, In, IsNull } from 'typeorm';
import * as dbutils from '../utils';
import * as mssql from './mssql';
@ -26,13 +26,17 @@ class Sql implements DatabaseDriver {
async init({ JacksonStore, JacksonIndex, JacksonTTL }): Promise<Sql> {
const sqlType = this.options.engine === 'planetscale' ? 'mysql' : this.options.type!;
// Synchronize by default for non-planetscale engines only if migrations are not set to run
let synchronize = !this.options.manualMigration;
if (this.options.engine === 'planetscale') {
synchronize = false;
}
while (true) {
try {
const baseOpts = {
type: sqlType,
synchronize: this.options.engine !== 'planetscale',
migrationsTableName: '_jackson_migrations',
synchronize,
logging: ['error'],
entities: [JacksonStore, JacksonIndex, JacksonTTL],
};
@ -73,6 +77,21 @@ class Sql implements DatabaseDriver {
this.indexRepository = this.dataSource.getRepository(JacksonIndex);
this.ttlRepository = this.dataSource.getRepository(JacksonTTL);
while (true) {
try {
if (synchronize) {
await this.indexNamespace();
}
break;
} catch (err) {
console.error(
`error in index namespace execution for engine: ${this.options.engine}, type: ${sqlType} err: ${err}`
);
await dbutils.sleep(1000);
continue;
}
}
if (this.options.ttl && this.options.cleanupLimit) {
this.ttlCleanup = async () => {
const now = Date.now();
@ -111,6 +130,23 @@ class Sql implements DatabaseDriver {
return this;
}
async indexNamespace() {
const res = await this.storeRepository.find({
where: {
namespace: IsNull(),
},
select: ['key'],
});
const searchTerm = ':';
for (const r of res) {
const key = r.key;
const tokens2 = key.split(searchTerm).slice(0, 2);
const value = tokens2.join(searchTerm);
await this.storeRepository.update({ key }, { namespace: value });
}
}
async get(namespace: string, key: string): Promise<any> {
const res = await this.storeRepository.findOneBy({
key: dbutils.key(namespace, key),
@ -131,7 +167,7 @@ class Sql implements DatabaseDriver {
async getAll(namespace: string, pageOffset?: number, pageLimit?: number, _?: string): Promise<Records> {
const skipOffsetAndLimitValue = !dbutils.isNumeric(pageOffset) && !dbutils.isNumeric(pageLimit);
const res = await this.storeRepository.find({
where: { key: Like(`%${namespace}%`) },
where: { namespace: namespace },
select: ['value', 'iv', 'tag'],
order: {
['createdAt']: 'DESC',
@ -195,6 +231,7 @@ class Sql implements DatabaseDriver {
store.iv = val.iv;
store.tag = val.tag;
store.modifiedAt = new Date().toISOString();
store.namespace = namespace;
await transactionalEntityManager.save(store);
if (ttl) {

View File

@ -7,7 +7,10 @@ import type {
} from '../typings';
export class RequestHandler {
constructor(private directoryUsers: IDirectoryUsers, private directoryGroups: IDirectoryGroups) {}
constructor(
private directoryUsers: IDirectoryUsers,
private directoryGroups: IDirectoryGroups
) {}
async handle(request: DirectorySyncRequest, callback?: EventCallback): Promise<DirectorySyncResponse> {
const resourceType = request.resourceType.toLowerCase();

View File

@ -157,11 +157,10 @@ export type UserPatchOperation = {
export type GroupPatchOperation = {
op: 'add' | 'remove' | 'replace';
path?: 'members' | 'displayName';
value:
| {
value: string;
display?: string;
}[];
value: {
value: string;
display?: string;
}[];
};
export type GroupMembership = {

View File

@ -5,7 +5,7 @@ import { JacksonError } from '../../controller/error';
import { SAMLHandler } from '../../controller/saml-handler';
import type { JacksonOption, SAMLSSORecord, SAMLTracerInstance } from '../../typings';
import { extractSAMLRequestAttributes } from '../../saml/lib';
import { getErrorMessage } from '../../controller/utils';
import { getErrorMessage, isConnectionActive } from '../../controller/utils';
import { throwIfInvalidLicense } from '../common/checkLicense';
export class SSO {
@ -45,6 +45,7 @@ export class SSO {
let connection: SAMLSSORecord | undefined;
let id, acsUrl, entityId, publicKey, providerName, decodedRequest, app;
try {
const parsedSAMLRequest = await extractSAMLRequestAttributes(request);
@ -81,7 +82,8 @@ export class SSO {
// If there is a redirect URL, then we need to redirect to that URL
if ('redirectUrl' in response) {
return {
redirectUrl: response.redirectUrl,
redirect_url: response.redirectUrl,
authorize_form: null,
};
}
@ -94,6 +96,10 @@ export class SSO {
throw new JacksonError('No SAML connection found.', 404);
}
if (!isConnectionActive(connection)) {
throw new JacksonError('SSO connection is deactivated. Please contact your administrator.', 403);
}
return await this.samlHandler.createSAMLRequest({
connection,
requestParams: {
@ -103,6 +109,8 @@ export class SSO {
publicKey,
providerName,
relayState,
tenant: app.tenant,
product: app.product,
},
});
} catch (err: unknown) {

View File

@ -35,8 +35,8 @@ const mapping = [
...arrayMapping,
];
type attributes = typeof mapping[number]['attribute'];
type schemas = typeof mapping[number]['schema'];
type attributes = (typeof mapping)[number]['attribute'];
type schemas = (typeof mapping)[number]['schema'];
const map = (claims: Record<attributes | schemas, unknown>) => {
arrayMapping.forEach((m) => {

View File

@ -250,7 +250,7 @@ export const createSAMLResponse = async ({
const xml = xmlbuilder.create(nodes, { encoding: 'UTF-8' }).end();
return await saml.sign(
return saml.sign(
xml,
privateKey,
publicKey,

View File

@ -169,7 +169,7 @@ export interface IOAuthController {
authorize(body: OAuthReq): Promise<{ redirect_url?: string; authorize_form?: string }>;
samlResponse(
body: SAMLResponsePayload
): Promise<{ redirect_url?: string; app_select_form?: string; responseForm?: string }>;
): Promise<{ redirect_url?: string; app_select_form?: string; response_form?: string }>;
oidcAuthzResponse(body: OIDCAuthzResponsePayload): Promise<{ redirect_url?: string }>;
token(body: OAuthTokenReq): Promise<OAuthTokenRes>;
userInfo(token: string): Promise<Profile>;
@ -378,6 +378,7 @@ export interface DatabaseOption {
readCapacityUnits?: number;
writeCapacityUnits?: number;
};
manualMigration?: boolean;
}
export interface JacksonOption {

View File

@ -79,15 +79,15 @@ tap.test('Federated SAML flow', async (t) => {
});
// Extract relay state created by Jackson
jacksonRelayState = new URL(response.redirectUrl).searchParams.get('RelayState');
jacksonRelayState = new URL(response.redirect_url).searchParams.get('RelayState');
t.ok(
response.redirectUrl?.startsWith(`${connection.idpMetadata.sso.redirectUrl}`),
response.redirect_url?.startsWith(`${connection.idpMetadata.sso.redirectUrl}`),
'Should have a SSO URL that starts with IdP SSO URL'
);
t.ok(response.redirectUrl, 'Should have a redirect URL');
t.ok(response.redirectUrl?.includes('SAMLRequest'), 'Should have a SAMLRequest in the redirect URL');
t.ok(response.redirectUrl?.includes('RelayState'), 'Should have a RelayState in the redirect URL');
t.ok(response.redirect_url, 'Should have a redirect URL');
t.ok(response.redirect_url?.includes('SAMLRequest'), 'Should have a SAMLRequest in the redirect URL');
t.ok(response.redirect_url?.includes('RelayState'), 'Should have a RelayState in the redirect URL');
});
t.test('Should be able to accept SAML Response from IdP and generate SAML Response for SP', async (t) => {
@ -110,15 +110,15 @@ tap.test('Federated SAML flow', async (t) => {
});
t.ok(response);
t.ok('responseForm' in response);
t.ok('response_form' in response);
t.ok(
response.responseForm?.includes('SAMLResponse'),
response.response_form?.includes('SAMLResponse'),
'Should have a SAMLResponse in the response form'
);
t.ok(response.responseForm?.includes('RelayState'), 'Should have a RelayState in the response form');
t.ok(response.response_form?.includes('RelayState'), 'Should have a RelayState in the response form');
const relayState = response.responseForm
? response.responseForm.match(/<input type="hidden" name="RelayState" value="(.*)"\/>/)?.[1]
const relayState = response.response_form
? response.response_form.match(/<input type="hidden" name="RelayState" value="(.*)"\/>/)?.[1]
: null;
t.match(relayState, relayStateFromSP, 'Should have the same relay state as the one sent by SP');

View File

@ -33,7 +33,8 @@ if (process.env.DB_SSL === 'true') {
};
}
const url = process.env.DB_URL || 'postgresql://postgres:postgres@localhost:5432/postgres';
const url =
process.env.DB_URL || process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/postgres';
let AppDataSource: DataSource;
@ -43,7 +44,10 @@ const baseOpts = {
migrationsTableName: '_jackson_migrations',
logging: 'all',
entities: [`src/db/${entitiesDir}/entity/**/*.ts`],
migrations: [`migration/${migrationsDir}/**/*.ts`],
migrations:
type === 'mssql'
? [`migration/${migrationsDir}/**/*.ts`]
: [`migration/${migrationsDir}/**/*.ts`, `migration/sql/**/*.ts`],
};
if (type === 'mssql') {
@ -59,7 +63,10 @@ if (type === 'mssql') {
});
} else {
AppDataSource = new DataSource(<DataSourceOptions>{
url: process.env.DB_URL || 'postgresql://postgres:postgres@localhost:5432/postgres',
url:
process.env.DB_URL ||
process.env.DATABASE_URL ||
'postgresql://postgres:postgres@localhost:5432/postgres',
ssl,
...baseOpts,
});

7036
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "jackson",
"version": "1.13.0",
"version": "1.14.0",
"private": true,
"description": "SAML 2.0 service",
"keywords": [
@ -15,7 +15,6 @@
"dev": "cross-env JACKSON_API_KEYS=secret IDP_ENABLED=true next dev -p 5225",
"dev-dbs": "docker-compose -f ./_dev/docker-compose.yml up -d",
"dev-dbs-destroy": "docker-compose -f ./_dev/docker-compose.yml down --volumes --remove-orphans",
"lint": "next lint && eslint -c .eslintrc.js --ext .ts ./",
"mongo": "cross-env JACKSON_API_KEYS=secret DB_ENGINE=mongo DB_URL=mongodb://localhost:27017/jackson npm run dev",
"pre-loaded": "cross-env JACKSON_API_KEYS=secret DB_ENGINE=mem PRE_LOADED_CONNECTION='./_dev/saml_config' npm run dev",
"pre-loaded-db": "cross-env JACKSON_API_KEYS=secret PRE_LOADED_CONNECTION='./_dev/saml_config' npm run dev",
@ -32,8 +31,10 @@
"mariadb:skaffold": "skaffold dev -f skaffold-mariadb.yaml --status-check=false --force=true",
"mssql:skaffold": "skaffold dev -f skaffold-mssql.yaml --status-check=false --force=true",
"dynamodb:skaffold": "skaffold dev -f skaffold-dynamodb.yaml --status-check=false --force=true",
"demo:skaffold": "echo 'This is only meant for BoxyHQ internal use. Please use {dbname}:skaffold instead' && skaffold run -f skaffold-demo.yaml --status-check=false --force=true",
"demo-services:skaffold": "echo 'This is only meant for BoxyHQ internal use. Please use {dbname}:skaffold instead' && skaffold run -f skaffold-demo-services.yaml --status-check=false --force=true",
"demo:skaffold": "echo 'This is only meant for BoxyHQ internal use. Please use {dbname}:skaffold instead' && skaffold run -f skaffold-demo.yaml --status-check=false",
"demo-services:skaffold": "echo 'This is only meant for BoxyHQ internal use. Please use {dbname}:skaffold instead' && skaffold run -f skaffold-demo-services.yaml --status-check=false",
"prod-eu:skaffold": "echo 'This is only meant for BoxyHQ internal use. Please use {dbname}:skaffold instead' && skaffold run -f skaffold-prod-eu.yaml --status-check=false",
"prod-eu-services:skaffold": "echo 'This is only meant for BoxyHQ internal use. Please use {dbname}:skaffold instead' && skaffold run -f skaffold-prod-eu-services.yaml --status-check=false",
"start": "cross-env PORT=5225 NODE_OPTIONS=--dns-result-order=ipv4first node .next/standalone/server.js",
"swagger-jsdoc": "swagger-jsdoc -d swagger/swaggerDefinition.js npm/src/**/*.ts npm/src/**/**/*.ts -o swagger/swagger.json",
"redis": "cross-env JACKSON_API_KEYS=secret DB_ENGINE=redis DB_TYPE=redis DB_URL=redis://localhost:6379/redis npm run dev",
@ -50,57 +51,61 @@
"prebuild": "ts-node --logError prebuild.ts",
"build": "next build",
"postbuild": "ts-node --logError postbuild.ts",
"release": "git checkout release && git merge origin/main && release-it"
"release": "git checkout release && git merge origin/main && release-it",
"check-types": "tsc --pretty --noEmit",
"check-format": "prettier --check .",
"check-lint": "next lint && eslint -c .eslintrc.js --ext ts --ext tsx --ext js ./",
"format": "prettier --write ."
},
"dependencies": {
"@boxyhq/metrics": "0.2.5",
"@boxyhq/react-ui": "3.3.12",
"@boxyhq/react-ui": "3.3.14",
"@boxyhq/saml-jackson": "file:npm",
"@heroicons/react": "2.0.18",
"@retracedhq/logs-viewer": "2.5.1",
"@retracedhq/retraced": "0.7.0",
"@tailwindcss/typography": "0.5.10",
"axios": "1.5.1",
"blockly": "10.2.1",
"blockly": "10.2.2",
"classnames": "2.3.2",
"cors": "2.8.5",
"daisyui": "3.9.2",
"daisyui": "3.9.3",
"i18next": "22.5.1",
"medium-zoom": "1.0.8",
"micromatch": "4.0.5",
"next": "13.4.19",
"next": "13.5.5",
"next-auth": "4.23.2",
"next-i18next": "13.3.0",
"next-mdx-remote": "4.4.1",
"nodemailer": "6.9.5",
"nodemailer": "6.9.6",
"raw-body": "2.5.2",
"react": "18.2.0",
"react-daisyui": "4.1.2",
"react-dom": "18.2.0",
"react-i18next": "12.3.1",
"react-syntax-highlighter": "15.5.0",
"remark-gfm": "4.0.0",
"remark-gfm": "3.0.1",
"sharp": "0.32.6",
"swr": "2.2.4"
},
"devDependencies": {
"@apidevtools/swagger-cli": "4.0.4",
"@playwright/test": "1.38.1",
"@types/cors": "2.8.14",
"@playwright/test": "1.39.0",
"@types/cors": "2.8.15",
"@types/micromatch": "4.0.3",
"@types/node": "20.8.2",
"@types/react": "18.2.25",
"@typescript-eslint/eslint-plugin": "6.7.4",
"@typescript-eslint/parser": "6.7.4",
"@types/node": "20.8.6",
"@types/react": "18.2.28",
"@typescript-eslint/eslint-plugin": "6.8.0",
"@typescript-eslint/parser": "6.8.0",
"autoprefixer": "10.4.16",
"cross-env": "7.0.3",
"env-cmd": "10.1.0",
"eslint": "8.50.0",
"eslint-config-next": "13.5.4",
"eslint": "8.51.0",
"eslint-config-next": "13.5.5",
"eslint-config-prettier": "9.0.0",
"postcss": "8.4.31",
"prettier": "3.0.3",
"prettier-plugin-tailwindcss": "0.5.5",
"prettier-plugin-tailwindcss": "0.5.6",
"release-it": "16.2.1",
"swagger-jsdoc": "6.2.8",
"tailwindcss": "3.3.3",

View File

@ -10,7 +10,7 @@ import micromatch from 'micromatch';
import nextI18NextConfig from '../next-i18next.config.js';
import { AccountLayout, SetupLinkLayout } from '@components/layouts';
import '@boxyhq/react-ui/dist/style.css';
import '../styles/globals.css';
const unauthenticatedRoutes = [

View File

@ -20,7 +20,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
};
// Handle SAML Response generated by IdP
const { redirect_url, app_select_form, responseForm } = await oauthController.samlResponse({
const { redirect_url, app_select_form, response_form } = await oauthController.samlResponse({
SAMLResponse,
RelayState,
idp_hint,
@ -36,9 +36,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.send(app_select_form);
}
if (responseForm) {
if (response_form) {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.send(responseForm);
return res.send(response_form);
}
} catch (error: any) {
const { message, statusCode = 500 } = error;

View File

@ -0,0 +1,6 @@
apiVersion: skaffold/v4beta6
kind: Config
manifests:
kustomize:
paths:
- ./kustomize/overlays/prod-eu/services

6
skaffold-prod-eu.yaml Normal file
View File

@ -0,0 +1,6 @@
apiVersion: skaffold/v4beta6
kind: Config
manifests:
kustomize:
paths:
- ./kustomize/overlays/prod-eu

View File

@ -10,8 +10,18 @@ html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans,
Droid Sans, Helvetica Neue, sans-serif;
font-family:
-apple-system,
BlinkMacSystemFont,
Segoe UI,
Roboto,
Oxygen,
Ubuntu,
Cantarell,
Fira Sans,
Droid Sans,
Helvetica Neue,
sans-serif;
}
a {

View File

@ -16,10 +16,7 @@
},
"host": "localhost:5225",
"basePath": "/",
"schemes": [
"http",
"https"
],
"schemes": ["http", "https"],
"securityDefinitions": {
"apiKey": {
"type": "apiKey",
@ -38,16 +35,9 @@
"post": {
"summary": "Create SSO connection",
"operationId": "create-sso-connection",
"tags": [
"Single Sign On"
],
"produces": [
"application/json"
],
"consumes": [
"application/x-www-form-urlencoded",
"application/json"
],
"tags": ["Single Sign On"],
"produces": ["application/json"],
"consumes": ["application/x-www-form-urlencoded", "application/json"],
"parameters": [
{
"$ref": "#/parameters/nameParamPost"
@ -107,13 +97,8 @@
"patch": {
"summary": "Update SSO Connection",
"operationId": "update-sso-connection",
"tags": [
"Single Sign On"
],
"consumes": [
"application/json",
"application/x-www-form-urlencoded"
],
"tags": ["Single Sign On"],
"consumes": ["application/json", "application/x-www-form-urlencoded"],
"parameters": [
{
"$ref": "#/parameters/clientIDParamPatch"
@ -196,9 +181,7 @@
}
],
"operationId": "get-connections",
"tags": [
"Single Sign On"
],
"tags": ["Single Sign On"],
"responses": {
"200": {
"$ref": "#/responses/200Get"
@ -231,9 +214,7 @@
],
"summary": "Delete SSO Connections",
"operationId": "delete-sso-connection",
"tags": [
"Single Sign On"
],
"tags": ["Single Sign On"],
"responses": {
"200": {
"description": "Success"
@ -256,9 +237,7 @@
}
],
"operationId": "get-connections-by-product",
"tags": [
"Single Sign On"
],
"tags": ["Single Sign On"],
"responses": {
"200": {
"$ref": "#/responses/200Get"
@ -276,12 +255,8 @@
"post": {
"summary": "Code exchange",
"operationId": "oauth-code-exchange",
"tags": [
"OAuth"
],
"consumes": [
"application/x-www-form-urlencoded"
],
"tags": ["OAuth"],
"consumes": ["application/x-www-form-urlencoded"],
"parameters": [
{
"name": "grant_type",
@ -356,9 +331,7 @@
"get": {
"summary": "Get profile",
"operationId": "oauth-get-profile",
"tags": [
"OAuth"
],
"tags": ["OAuth"],
"responses": {
"200": {
"description": "Success",
@ -413,16 +386,9 @@
"post": {
"summary": "Create a Setup Link",
"operationId": "create-sso-setup-link",
"tags": [
"Setup Links | Single Sign On"
],
"produces": [
"application/json"
],
"consumes": [
"application/x-www-form-urlencoded",
"application/json"
],
"tags": ["Setup Links | Single Sign On"],
"produces": ["application/json"],
"consumes": ["application/x-www-form-urlencoded", "application/json"],
"parameters": [
{
"$ref": "#/parameters/tenantParamPost"
@ -460,9 +426,7 @@
}
],
"operationId": "delete-sso-setup-link",
"tags": [
"Setup Links | Single Sign On"
],
"tags": ["Setup Links | Single Sign On"],
"responses": {
"200": {
"description": "Success",
@ -489,9 +453,7 @@
}
],
"operationId": "get-sso-setup-link",
"tags": [
"Setup Links | Single Sign On"
],
"tags": ["Setup Links | Single Sign On"],
"responses": {
"200": {
"description": "Success",
@ -506,16 +468,9 @@
"post": {
"summary": "Create a Setup Link",
"operationId": "create-dsync-setup-link",
"tags": [
"Setup Links | Directory Sync"
],
"produces": [
"application/json"
],
"consumes": [
"application/x-www-form-urlencoded",
"application/json"
],
"tags": ["Setup Links | Directory Sync"],
"produces": ["application/json"],
"consumes": ["application/x-www-form-urlencoded", "application/json"],
"parameters": [
{
"$ref": "#/parameters/tenantParamPost"
@ -547,9 +502,7 @@
}
],
"operationId": "delete-dsync-setup-link",
"tags": [
"Setup Links | Directory Sync"
],
"tags": ["Setup Links | Directory Sync"],
"responses": {
"200": {
"description": "Success",
@ -576,9 +529,7 @@
}
],
"operationId": "get-dsync-setup-link",
"tags": [
"Setup Links | Directory Sync"
],
"tags": ["Setup Links | Directory Sync"],
"responses": {
"200": {
"description": "Success",
@ -598,9 +549,7 @@
}
],
"operationId": "get-sso-setup-link-by-product",
"tags": [
"Setup Links | Single Sign On"
],
"tags": ["Setup Links | Single Sign On"],
"responses": {
"200": {
"description": "Success",
@ -623,9 +572,7 @@
}
],
"operationId": "get-dsync-setup-link-by-product",
"tags": [
"Setup Links | Directory Sync"
],
"tags": ["Setup Links | Directory Sync"],
"responses": {
"200": {
"description": "Success",
@ -651,12 +598,8 @@
"type": "string"
}
],
"tags": [
"SAML Traces"
],
"produces": [
"application/json"
],
"tags": ["SAML Traces"],
"produces": ["application/json"],
"responses": {
"200": {
"description": "Success",
@ -675,12 +618,8 @@
"$ref": "#/parameters/product"
}
],
"tags": [
"SAML Traces"
],
"produces": [
"application/json"
],
"tags": ["SAML Traces"],
"produces": ["application/json"],
"responses": {
"200": {
"description": "Success",
@ -741,16 +680,9 @@
"type": "string"
}
],
"tags": [
"Directory Sync"
],
"produces": [
"application/json"
],
"consumes": [
"application/x-www-form-urlencoded",
"application/json"
],
"tags": ["Directory Sync"],
"produces": ["application/json"],
"consumes": ["application/x-www-form-urlencoded", "application/json"],
"responses": {
"200": {
"description": "Success",
@ -773,16 +705,9 @@
"$ref": "#/parameters/product"
}
],
"tags": [
"Directory Sync"
],
"produces": [
"application/json"
],
"consumes": [
"application/x-www-form-urlencoded",
"application/json"
],
"tags": ["Directory Sync"],
"produces": ["application/json"],
"consumes": ["application/x-www-form-urlencoded", "application/json"],
"responses": {
"200": {
"description": "Success",
@ -808,12 +733,8 @@
"type": "string"
}
],
"tags": [
"Directory Sync"
],
"produces": [
"application/json"
],
"tags": ["Directory Sync"],
"produces": ["application/json"],
"responses": {
"200": {
"description": "Success",
@ -834,12 +755,8 @@
"type": "string"
}
],
"tags": [
"Directory Sync"
],
"produces": [
"application/json"
],
"tags": ["Directory Sync"],
"produces": ["application/json"],
"responses": {
"200": {
"description": "Success"
@ -855,12 +772,8 @@
"$ref": "#/parameters/product"
}
],
"tags": [
"Directory Sync"
],
"produces": [
"application/json"
],
"tags": ["Directory Sync"],
"produces": ["application/json"],
"responses": {
"200": {
"description": "Success",
@ -892,12 +805,8 @@
"type": "string"
}
],
"tags": [
"Directory Sync"
],
"produces": [
"application/json"
],
"tags": ["Directory Sync"],
"produces": ["application/json"],
"responses": {
"200": {
"description": "Success",
@ -922,12 +831,8 @@
"$ref": "#/parameters/directoryId"
}
],
"tags": [
"Directory Sync"
],
"produces": [
"application/json"
],
"tags": ["Directory Sync"],
"produces": ["application/json"],
"responses": {
"200": {
"description": "Success",
@ -959,12 +864,8 @@
"type": "string"
}
],
"tags": [
"Directory Sync"
],
"produces": [
"application/json"
],
"tags": ["Directory Sync"],
"produces": ["application/json"],
"responses": {
"200": {
"description": "Success",
@ -989,12 +890,8 @@
"$ref": "#/parameters/directoryId"
}
],
"tags": [
"Directory Sync"
],
"produces": [
"application/json"
],
"tags": ["Directory Sync"],
"produces": ["application/json"],
"responses": {
"200": {
"description": "Success",
@ -1069,16 +966,9 @@
"type": "string"
}
],
"tags": [
"SAML Federation"
],
"produces": [
"application/json"
],
"consumes": [
"application/x-www-form-urlencoded",
"application/json"
],
"tags": ["SAML Federation"],
"produces": ["application/json"],
"consumes": ["application/x-www-form-urlencoded", "application/json"],
"responses": {
"200": {
"description": "Success",
@ -1116,12 +1006,8 @@
"type": "string"
}
],
"tags": [
"SAML Federation"
],
"produces": [
"application/json"
],
"tags": ["SAML Federation"],
"produces": ["application/json"],
"responses": {
"200": {
"description": "Success",
@ -1198,12 +1084,8 @@
"type": "string"
}
],
"tags": [
"SAML Federation"
],
"produces": [
"application/json"
],
"tags": ["SAML Federation"],
"produces": ["application/json"],
"responses": {
"200": {
"description": "Success",
@ -1238,12 +1120,8 @@
"type": "string"
}
],
"tags": [
"SAML Federation"
],
"produces": [
"application/json"
],
"tags": ["SAML Federation"],
"produces": ["application/json"],
"responses": {
"200": {
"description": "Success",
@ -1266,12 +1144,8 @@
"type": "string"
}
],
"tags": [
"SAML Federation"
],
"produces": [
"application/json"
],
"tags": ["SAML Federation"],
"produces": ["application/json"],
"responses": {
"200": {
"description": "Success",
@ -1301,9 +1175,7 @@
"provider": "okta.com"
},
"defaultRedirectUrl": "https://hoppscotch.io/",
"redirectUrl": [
"https://hoppscotch.io/"
],
"redirectUrl": ["https://hoppscotch.io/"],
"tenant": "hoppscotch.io",
"product": "API Engine",
"name": "Hoppscotch-SP",
@ -1899,4 +1771,4 @@
}
},
"tags": []
}
}