Merge pull request #7349 from Budibase/feature/configurable-sso-auth-scopes
Feature/configurable sso auth scopes
This commit is contained in:
commit
3d60418d5a
|
@ -0,0 +1,178 @@
|
|||
import filterTests from "../../support/filterTests"
|
||||
// const interact = require("../support/interact")
|
||||
|
||||
filterTests(["smoke", "all"], () => {
|
||||
context("Auth Configuration", () => {
|
||||
before(() => {
|
||||
cy.login()
|
||||
})
|
||||
|
||||
after(() => {
|
||||
cy.get(".spectrum-SideNav li").contains("Auth").click()
|
||||
cy.location().should(loc => {
|
||||
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
|
||||
})
|
||||
|
||||
cy.get("[data-cy=new-scope-input]").clear()
|
||||
|
||||
cy.get("div.content").scrollTo("bottom")
|
||||
cy.get("[data-cy=oidc-active]").click()
|
||||
|
||||
cy.get("[data-cy=oidc-active]").should('not.be.checked')
|
||||
|
||||
cy.intercept("POST", "/api/global/configs").as("updateAuth")
|
||||
cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true})
|
||||
cy.wait("@updateAuth")
|
||||
cy.get("@updateAuth").its("response.statusCode").should("eq", 200)
|
||||
|
||||
cy.get(".spectrum-Toast-content")
|
||||
.contains("Settings saved")
|
||||
.should("be.visible")
|
||||
})
|
||||
|
||||
it("Should allow updating of the OIDC config", () => {
|
||||
cy.get(".spectrum-SideNav li").contains("Auth").click()
|
||||
cy.location().should(loc => {
|
||||
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
|
||||
})
|
||||
cy.get("div.content").scrollTo("bottom")
|
||||
cy.get(".spectrum-Toast .spectrum-ClearButton").click()
|
||||
|
||||
cy.get("input[data-cy=configUrl]").type("http://budi-auth.com/v2")
|
||||
cy.get("input[data-cy=clientID]").type("34ac6a13-f24a-4b52-c70d-fa544ffd11b2")
|
||||
cy.get("input[data-cy=clientSecret]").type("12A8Q~4nS_DWhOOJ2vWIRsNyDVsdtXPD.Zxa9df_")
|
||||
|
||||
cy.get("button[data-cy=oidc-save]").should("not.be.disabled");
|
||||
|
||||
cy.intercept("POST", "/api/global/configs").as("updateAuth")
|
||||
cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true})
|
||||
cy.wait("@updateAuth")
|
||||
cy.get("@updateAuth").its("response.statusCode").should("eq", 200)
|
||||
|
||||
cy.get(".spectrum-Toast-content")
|
||||
.contains("Settings saved")
|
||||
.should("be.visible")
|
||||
})
|
||||
|
||||
it("Should display default scopes in advanced config.", () => {
|
||||
cy.get(".spectrum-SideNav li").contains("Auth").click()
|
||||
cy.location().should(loc => {
|
||||
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
|
||||
})
|
||||
cy.get("div.content").scrollTo("bottom")
|
||||
|
||||
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
|
||||
|
||||
cy.get(".spectrum-Tags-item").contains("openid")
|
||||
cy.get(".spectrum-Tags-item").contains("openid").find(".spectrum-ClearButton").should("not.exist")
|
||||
|
||||
cy.get(".spectrum-Tags-item").contains("offline_access")
|
||||
cy.get(".spectrum-Tags-item").contains("email")
|
||||
cy.get(".spectrum-Tags-item").contains("profile")
|
||||
})
|
||||
|
||||
it("Add a new scopes", () => {
|
||||
cy.get(".spectrum-SideNav li").contains("Auth").click()
|
||||
cy.location().should(loc => {
|
||||
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
|
||||
})
|
||||
cy.get("div.content").scrollTo("bottom")
|
||||
|
||||
cy.get("[data-cy=new-scope-input]").type("Sample{enter}")
|
||||
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 5)
|
||||
cy.get(".spectrum-Tags-item").contains("Sample")
|
||||
|
||||
cy.get(".auth-form input.spectrum-Textfield-input").type("Another ")
|
||||
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 6)
|
||||
cy.get(".spectrum-Tags-item").contains("Another")
|
||||
|
||||
cy.get("button[data-cy=oidc-save]").should("not.be.disabled");
|
||||
|
||||
cy.intercept("POST", "/api/global/configs").as("updateAuth")
|
||||
cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true})
|
||||
cy.wait("@updateAuth")
|
||||
cy.get("@updateAuth").its("response.statusCode").should("eq", 200)
|
||||
|
||||
cy.reload()
|
||||
|
||||
cy.get("div.content").scrollTo("bottom")
|
||||
|
||||
cy.get(".spectrum-Tags-item").contains("openid")
|
||||
cy.get(".spectrum-Tags-item").contains("offline_access")
|
||||
cy.get(".spectrum-Tags-item").contains("email")
|
||||
cy.get(".spectrum-Tags-item").contains("profile")
|
||||
cy.get(".spectrum-Tags-item").contains("Sample")
|
||||
cy.get(".spectrum-Tags-item").contains("Another")
|
||||
})
|
||||
|
||||
it("Should allow the removal of auth scopes", () => {
|
||||
cy.get(".spectrum-SideNav li").contains("Auth").click()
|
||||
cy.location().should(loc => {
|
||||
expect(loc.pathname).to.eq("/builder/portal/manage/auth")
|
||||
})
|
||||
cy.get("div.content").scrollTo("bottom")
|
||||
|
||||
cy.get(".spectrum-Tags-item").contains("offline_access").parent().find(".spectrum-ClearButton").click()
|
||||
cy.get(".spectrum-Tags-item").contains("profile").parent().find(".spectrum-ClearButton").click()
|
||||
|
||||
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
|
||||
|
||||
cy.get(".spectrum-Tags-item").contains("offline_access").should("not.exist")
|
||||
cy.get(".spectrum-Tags-item").contains("profile").should("not.exist")
|
||||
|
||||
cy.get("button[data-cy=oidc-save]").should("not.be.disabled");
|
||||
|
||||
cy.intercept("POST", "/api/global/configs").as("updateAuth")
|
||||
cy.get("button[data-cy=oidc-save]").contains("Save").click({force: true})
|
||||
cy.wait("@updateAuth")
|
||||
cy.get("@updateAuth").its("response.statusCode").should("eq", 200)
|
||||
|
||||
cy.get(".spectrum-Toast-content")
|
||||
.contains("Settings saved")
|
||||
.should("be.visible")
|
||||
|
||||
cy.reload()
|
||||
|
||||
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
|
||||
|
||||
cy.get(".spectrum-Tags-item").contains("offline_access").should("not.exist")
|
||||
cy.get(".spectrum-Tags-item").contains("profile").should("not.exist")
|
||||
})
|
||||
|
||||
it("Should allow auth scopes to be reset to the core defaults.", () => {
|
||||
cy.get(".spectrum-SideNav li").contains("Auth").click()
|
||||
|
||||
cy.get("div.content").scrollTo("bottom")
|
||||
|
||||
cy.get("[data-cy=restore-oidc-default-scopes]").click({force: true})
|
||||
|
||||
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
|
||||
|
||||
cy.get(".spectrum-Tags-item").contains("openid")
|
||||
cy.get(".spectrum-Tags-item").contains("offline_access")
|
||||
cy.get(".spectrum-Tags-item").contains("email")
|
||||
cy.get(".spectrum-Tags-item").contains("profile")
|
||||
})
|
||||
|
||||
it("Should not allow invalid characters in the auth scopes", () => {
|
||||
cy.get("[data-cy=new-scope-input]").type("thisIsInvalid\\{enter}")
|
||||
cy.get(".spectrum-Form-itemField .error").contains("Auth scopes cannot contain spaces, double quotes or backslashes")
|
||||
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
|
||||
|
||||
cy.get("[data-cy=new-scope-input]").clear()
|
||||
|
||||
cy.get("[data-cy=new-scope-input]").type("alsoInvalid\"{enter}")
|
||||
cy.get(".spectrum-Form-itemField .error").contains("Auth scopes cannot contain spaces, double quotes or backslashes")
|
||||
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
|
||||
|
||||
cy.get("[data-cy=new-scope-input]").clear()
|
||||
})
|
||||
|
||||
it("Should not allow duplicate auth scopes", () => {
|
||||
cy.get("[data-cy=new-scope-input]").type("offline_access{enter}")
|
||||
cy.get(".spectrum-Form-itemField .error").contains("Auth scope already exists")
|
||||
cy.get(".spectrum-Tags").find(".spectrum-Tags-item").its("length").should("eq", 4)
|
||||
})
|
||||
|
||||
})
|
||||
})
|
|
@ -18,6 +18,8 @@
|
|||
Body,
|
||||
Select,
|
||||
Toggle,
|
||||
Tag,
|
||||
Tags,
|
||||
} from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import { API } from "api"
|
||||
|
@ -29,6 +31,8 @@
|
|||
OIDC: "oidc",
|
||||
}
|
||||
|
||||
const HasSpacesRegex = /[\\"\s]/
|
||||
|
||||
// Some older google configs contain a manually specified value - retain the functionality to edit the field
|
||||
// When there is no value or we are in the cloud - prohibit editing the field, must use platform url to change
|
||||
$: googleCallbackUrl = undefined
|
||||
|
@ -145,7 +149,6 @@
|
|||
|
||||
async function save(docs) {
|
||||
let calls = []
|
||||
|
||||
// Only if the user has provided an image, upload it
|
||||
if (image) {
|
||||
let data = new FormData()
|
||||
|
@ -157,7 +160,6 @@
|
|||
})
|
||||
)
|
||||
}
|
||||
|
||||
docs.forEach(element => {
|
||||
// Delete unsupported fields
|
||||
delete element.createdAt
|
||||
|
@ -199,7 +201,6 @@
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (calls.length) {
|
||||
Promise.all(calls)
|
||||
.then(data => {
|
||||
|
@ -215,6 +216,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
let defaultScopes = ["profile", "email", "offline_access"]
|
||||
|
||||
const refreshScopes = idx => {
|
||||
providers.oidc.config.configs[idx]["scopes"] =
|
||||
providers.oidc.config.configs[idx]["scopes"]
|
||||
}
|
||||
|
||||
let scopesFields = [
|
||||
{
|
||||
editing: true,
|
||||
inputText: null,
|
||||
error: null,
|
||||
},
|
||||
]
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await organisation.init()
|
||||
|
@ -276,7 +292,7 @@
|
|||
if (!oidcDoc?._id) {
|
||||
providers.oidc = {
|
||||
type: ConfigTypes.OIDC,
|
||||
config: { configs: [{ activated: true }] },
|
||||
config: { configs: [{ activated: true, scopes: defaultScopes }] },
|
||||
}
|
||||
} else {
|
||||
originalOidcDoc = cloneDeep(oidcDoc)
|
||||
|
@ -345,6 +361,7 @@
|
|||
size="s"
|
||||
cta
|
||||
on:click={() => save([providers.oidc])}
|
||||
dataCy={"oidc-save"}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
|
@ -362,6 +379,7 @@
|
|||
bind:value={providers.oidc.config.configs[0][field.name]}
|
||||
readonly={field.readonly}
|
||||
placeholder={field.placeholder}
|
||||
dataCy={field.name}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
@ -392,15 +410,132 @@
|
|||
<div class="form-row">
|
||||
<Label size="L">Activated</Label>
|
||||
<Toggle
|
||||
dataCy={"oidc-active"}
|
||||
text=""
|
||||
bind:value={providers.oidc.config.configs[0].activated}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
<span class="advanced-config">
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading size="XS">
|
||||
<div class="auth-scopes">
|
||||
<div>Advanced</div>
|
||||
<Button
|
||||
secondary
|
||||
newStyles
|
||||
size="S"
|
||||
on:click={() => {
|
||||
providers.oidc.config.configs[0]["scopes"] = [...defaultScopes]
|
||||
}}
|
||||
dataCy={"restore-oidc-default-scopes"}
|
||||
>
|
||||
Restore Defaults
|
||||
</Button>
|
||||
</div>
|
||||
</Heading>
|
||||
<Body size="S">
|
||||
Changes to your authentication scopes will only take effect when you
|
||||
next log in. Please refer to your vendor documentation before
|
||||
modification.
|
||||
</Body>
|
||||
|
||||
<div class="auth-form">
|
||||
<span class="add-new">
|
||||
<Label size="L">{"Auth Scopes"}</Label>
|
||||
<Input
|
||||
dataCy={"new-scope-input"}
|
||||
error={scopesFields[0].error}
|
||||
placeholder={"New Scope"}
|
||||
bind:value={scopesFields[0].inputText}
|
||||
on:keyup={e => {
|
||||
if (!scopesFields[0].inputText) {
|
||||
scopesFields[0].error = null
|
||||
}
|
||||
if (
|
||||
e.key === "Enter" ||
|
||||
e.keyCode === 13 ||
|
||||
e.code == "Space" ||
|
||||
e.keyCode == 32
|
||||
) {
|
||||
let scopes = providers.oidc.config.configs[0]["scopes"]
|
||||
? providers.oidc.config.configs[0]["scopes"]
|
||||
: [...defaultScopes]
|
||||
|
||||
let update = scopesFields[0].inputText.trim()
|
||||
|
||||
if (HasSpacesRegex.test(update)) {
|
||||
scopesFields[0].error =
|
||||
"Auth scopes cannot contain spaces, double quotes or backslashes"
|
||||
return
|
||||
} else if (scopes.indexOf(update) > -1) {
|
||||
scopesFields[0].error = "Auth scope already exists"
|
||||
return
|
||||
} else if (!update.length) {
|
||||
scopesFields[0].inputText = null
|
||||
scopesFields[0].error = null
|
||||
return
|
||||
} else {
|
||||
scopesFields[0].error = null
|
||||
scopes.push(update)
|
||||
providers.oidc.config.configs[0]["scopes"] = scopes
|
||||
scopesFields[0].inputText = null
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<div class="tag-wrap">
|
||||
<span />
|
||||
<Tags>
|
||||
<Tag closable={false}>openid</Tag>
|
||||
{#each providers.oidc.config.configs[0]["scopes"] || [...defaultScopes] as tag, idx}
|
||||
<Tag
|
||||
closable={scopesFields[0].editing}
|
||||
on:click={() => {
|
||||
let idxScopes = providers.oidc.config.configs[0]["scopes"]
|
||||
if (idxScopes.length == 1) {
|
||||
idxScopes.pop()
|
||||
} else {
|
||||
idxScopes.splice(idx, 1)
|
||||
refreshScopes(0)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</Tag>
|
||||
{/each}
|
||||
</Tags>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</span>
|
||||
{/if}
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.auth-scopes {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.advanced-config :global(.spectrum-Tags-item) {
|
||||
margin-left: 0px;
|
||||
margin-top: var(--spacing-m);
|
||||
margin-right: var(--spacing-m);
|
||||
}
|
||||
|
||||
.auth-form > * {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-l);
|
||||
grid-template-columns: 100px 1fr;
|
||||
}
|
||||
|
||||
.advanced-config .auth-form .tag-wrap {
|
||||
padding: 0px 5px 5px 0px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 100px 1fr;
|
||||
|
|
|
@ -227,9 +227,22 @@ export const oidcPreAuth = async (ctx: any, next: any) => {
|
|||
|
||||
setCookie(ctx, configId, Cookies.OIDC_CONFIG)
|
||||
|
||||
const db = getGlobalDB()
|
||||
const config = await core.db.getScopedConfig(db, {
|
||||
type: Configs.OIDC,
|
||||
group: ctx.query.group,
|
||||
})
|
||||
|
||||
const chosenConfig = config.configs.filter((c: any) => c.uuid === configId)[0]
|
||||
|
||||
let authScopes =
|
||||
chosenConfig.scopes?.length > 0
|
||||
? chosenConfig.scopes
|
||||
: ["profile", "email", "offline_access"]
|
||||
|
||||
return passport.authenticate(strategy, {
|
||||
// required 'openid' scope is added by oidc strategy factory
|
||||
scope: ["profile", "email", "offline_access"], //auth0 offline_access scope required for the refresh token behaviour.
|
||||
scope: authScopes,
|
||||
})(ctx, next)
|
||||
}
|
||||
|
||||
|
|
|
@ -53,6 +53,7 @@ function oidcValidation() {
|
|||
name: Joi.string().allow("", null),
|
||||
uuid: Joi.string().required(),
|
||||
activated: Joi.boolean().required(),
|
||||
scopes: Joi.array().optional()
|
||||
})
|
||||
).required(true)
|
||||
}).unknown(true)
|
||||
|
|
Loading…
Reference in New Issue