Merge pull request #1538 from Budibase/fix/lockdown-admin
Locking down administration endpoints and routes
This commit is contained in:
commit
0b728f07fa
|
@ -43,6 +43,7 @@ module.exports = (noAuthPatterns = [], opts) => {
|
||||||
// this is an internal request, no user made it
|
// this is an internal request, no user made it
|
||||||
if (apiKey && apiKey === env.INTERNAL_API_KEY) {
|
if (apiKey && apiKey === env.INTERNAL_API_KEY) {
|
||||||
ctx.isAuthenticated = true
|
ctx.isAuthenticated = true
|
||||||
|
ctx.internal = true
|
||||||
} else if (authCookie) {
|
} else if (authCookie) {
|
||||||
try {
|
try {
|
||||||
const db = database.getDB(StaticDatabases.GLOBAL.name)
|
const db = database.getDB(StaticDatabases.GLOBAL.name)
|
||||||
|
|
|
@ -20,18 +20,40 @@
|
||||||
let userInfoModal
|
let userInfoModal
|
||||||
let changePasswordModal
|
let changePasswordModal
|
||||||
|
|
||||||
const menu = [
|
$: menu = buildMenu($auth.isAdmin)
|
||||||
{ title: "Apps", href: "/builder/portal/apps" },
|
|
||||||
{ title: "Users", href: "/builder/portal/manage/users", heading: "Manage" },
|
const buildMenu = admin => {
|
||||||
{ title: "Auth", href: "/builder/portal/manage/auth" },
|
let menu = [{ title: "Apps", href: "/builder/portal/apps" }]
|
||||||
{ title: "Email", href: "/builder/portal/manage/email" },
|
if (admin) {
|
||||||
{
|
menu = menu.concat([
|
||||||
title: "Organisation",
|
{
|
||||||
href: "/builder/portal/settings/organisation",
|
title: "Users",
|
||||||
heading: "Settings",
|
href: "/builder/portal/manage/users",
|
||||||
},
|
heading: "Manage",
|
||||||
{ title: "Theming", href: "/builder/portal/settings/theming" },
|
},
|
||||||
]
|
{ title: "Auth", href: "/builder/portal/manage/auth" },
|
||||||
|
{ title: "Email", href: "/builder/portal/manage/email" },
|
||||||
|
{
|
||||||
|
title: "Organisation",
|
||||||
|
href: "/builder/portal/settings/organisation",
|
||||||
|
heading: "Settings",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Theming",
|
||||||
|
href: "/builder/portal/settings/theming",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
menu = menu.concat([
|
||||||
|
{
|
||||||
|
title: "Theming",
|
||||||
|
href: "/builder/portal/settings/theming",
|
||||||
|
heading: "Settings",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
return menu
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// Prevent non-builders from accessing the portal
|
// Prevent non-builders from accessing the portal
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script>
|
||||||
|
import { Page } from "@budibase/bbui"
|
||||||
|
import { auth } from "stores/portal"
|
||||||
|
import { redirect } from "@roxi/routify"
|
||||||
|
|
||||||
|
// Only admins allowed here
|
||||||
|
$: {
|
||||||
|
if (!$auth.isAdmin) {
|
||||||
|
$redirect("../")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $auth.isAdmin}
|
||||||
|
<Page>
|
||||||
|
<slot />
|
||||||
|
</Page>
|
||||||
|
{/if}
|
|
@ -1,7 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Page } from "@budibase/bbui"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Page>
|
|
||||||
<slot />
|
|
||||||
</Page>
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { email } from "stores/portal"
|
import { email } from "stores/portal"
|
||||||
|
|
||||||
email.templates.fetch()
|
email.templates.fetch()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { fetchData } from "helpers"
|
import { fetchData } from "helpers"
|
||||||
import { users } from "stores/portal"
|
import { users, auth } from "stores/portal"
|
||||||
|
|
||||||
import TagsRenderer from "./_components/TagsTableRenderer.svelte"
|
import TagsRenderer from "./_components/TagsTableRenderer.svelte"
|
||||||
import UpdateRolesModal from "./_components/UpdateRolesModal.svelte"
|
import UpdateRolesModal from "./_components/UpdateRolesModal.svelte"
|
||||||
|
@ -56,13 +56,21 @@
|
||||||
|
|
||||||
let toggleDisabled = false
|
let toggleDisabled = false
|
||||||
|
|
||||||
async function toggleBuilderAccess({ detail }) {
|
async function toggleFlag(flagName, detail) {
|
||||||
toggleDisabled = true
|
toggleDisabled = true
|
||||||
await users.save({ ...$userFetch?.data, builder: { global: detail } })
|
await users.save({ ...$userFetch?.data, [flagName]: { global: detail } })
|
||||||
await userFetch.refresh()
|
await userFetch.refresh()
|
||||||
toggleDisabled = false
|
toggleDisabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleBuilderAccess({ detail }) {
|
||||||
|
return toggleFlag("builder", detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleAdminAccess({ detail }) {
|
||||||
|
return toggleFlag("admin", detail)
|
||||||
|
}
|
||||||
|
|
||||||
async function openUpdateRolesModal({ detail }) {
|
async function openUpdateRolesModal({ detail }) {
|
||||||
selectedApp = detail
|
selectedApp = detail
|
||||||
editRolesModal.show()
|
editRolesModal.show()
|
||||||
|
@ -107,15 +115,27 @@
|
||||||
<Label size="L">Last name</Label>
|
<Label size="L">Last name</Label>
|
||||||
<Input disabled thin value={$userFetch?.data?.lastName} />
|
<Input disabled thin value={$userFetch?.data?.lastName} />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<!-- don't let a user remove the privileges that let them be here -->
|
||||||
<Label size="L">Development access</Label>
|
{#if userId !== $auth.user._id}
|
||||||
<Toggle
|
<div class="field">
|
||||||
text=""
|
<Label size="L">Development access</Label>
|
||||||
value={$userFetch?.data?.builder?.global}
|
<Toggle
|
||||||
on:change={toggleBuilderAccess}
|
text=""
|
||||||
disabled={toggleDisabled}
|
value={$userFetch?.data?.builder?.global}
|
||||||
/>
|
on:change={toggleBuilderAccess}
|
||||||
</div>
|
disabled={toggleDisabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Administration access</Label>
|
||||||
|
<Toggle
|
||||||
|
text=""
|
||||||
|
value={$userFetch?.data?.admin?.global}
|
||||||
|
on:change={toggleAdminAccess}
|
||||||
|
disabled={toggleDisabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="regenerate">
|
<div class="regenerate">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Page } from "@budibase/bbui"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Page>
|
|
||||||
<slot />
|
|
||||||
</Page>
|
|
|
@ -22,6 +22,7 @@
|
||||||
const schema = {
|
const schema = {
|
||||||
email: {},
|
email: {},
|
||||||
developmentAccess: { displayName: "Development Access", type: "boolean" },
|
developmentAccess: { displayName: "Development Access", type: "boolean" },
|
||||||
|
adminAccess: { displayName: "Admin Access", type: "boolean" },
|
||||||
// role: { type: "options" },
|
// role: { type: "options" },
|
||||||
group: {},
|
group: {},
|
||||||
// access: {},
|
// access: {},
|
||||||
|
@ -35,7 +36,8 @@
|
||||||
.map(user => ({
|
.map(user => ({
|
||||||
...user,
|
...user,
|
||||||
group: ["All users"],
|
group: ["All users"],
|
||||||
developmentAccess: user.builder.global,
|
developmentAccess: !!user.builder?.global,
|
||||||
|
adminAccess: !!user.admin?.global,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
let createUserModal
|
let createUserModal
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
$goto("./general")
|
$goto("./organisation")
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -11,10 +11,18 @@
|
||||||
Dropzone,
|
Dropzone,
|
||||||
notifications,
|
notifications,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { organisation } from "stores/portal"
|
import { auth, organisation } from "stores/portal"
|
||||||
import { post } from "builderStore/api"
|
import { post } from "builderStore/api"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
import { redirect } from "@roxi/routify"
|
||||||
|
|
||||||
|
// Only admins allowed here
|
||||||
|
$: {
|
||||||
|
if (!$auth.isAdmin) {
|
||||||
|
$redirect("../../portal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const values = writable({
|
const values = writable({
|
||||||
analytics: !analytics.disabled(),
|
analytics: !analytics.disabled(),
|
||||||
|
@ -64,68 +72,70 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout>
|
{#if $auth.isAdmin}
|
||||||
<Layout gap="XS" noPadding>
|
<Layout>
|
||||||
<Heading size="M">Organisation</Heading>
|
|
||||||
<Body>
|
|
||||||
Organisation settings is where you can edit your organisation name and
|
|
||||||
logo. You can also configure your platform URL and enable or disable
|
|
||||||
analytics.
|
|
||||||
</Body>
|
|
||||||
</Layout>
|
|
||||||
<Divider size="S" />
|
|
||||||
<Layout gap="XS" noPadding>
|
|
||||||
<Heading size="S">Information</Heading>
|
|
||||||
<Body size="S">Here you can update your logo and organization name.</Body>
|
|
||||||
</Layout>
|
|
||||||
<div class="fields">
|
|
||||||
<div class="field">
|
|
||||||
<Label size="L">Organization name</Label>
|
|
||||||
<Input thin bind:value={$values.company} />
|
|
||||||
</div>
|
|
||||||
<div class="field logo">
|
|
||||||
<Label size="L">Logo</Label>
|
|
||||||
<div class="file">
|
|
||||||
<Dropzone
|
|
||||||
value={[$values.logo]}
|
|
||||||
on:change={e => {
|
|
||||||
$values.logo = e.detail?.[0]
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Divider size="S" />
|
|
||||||
<Layout gap="XS" noPadding>
|
|
||||||
<Heading size="S">Platform</Heading>
|
|
||||||
<Body size="S">Here you can set up general platform settings.</Body>
|
|
||||||
</Layout>
|
|
||||||
<div class="fields">
|
|
||||||
<div class="field">
|
|
||||||
<Label size="L">Platform URL</Label>
|
|
||||||
<Input thin bind:value={$values.platformUrl} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Divider size="S" />
|
|
||||||
<Layout gap="S" noPadding>
|
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Heading size="S">Analytics</Heading>
|
<Heading size="M">Organisation</Heading>
|
||||||
<Body size="S">
|
<Body>
|
||||||
If you would like to send analytics that help us make Budibase better,
|
Organisation settings is where you can edit your organisation name and
|
||||||
please let us know below.
|
logo. You can also configure your platform URL and enable or disable
|
||||||
|
analytics.
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
<Divider size="S" />
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading size="S">Information</Heading>
|
||||||
|
<Body size="S">Here you can update your logo and organization name.</Body>
|
||||||
|
</Layout>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Label size="L">Send Analytics to Budibase</Label>
|
<Label size="L">Organization name</Label>
|
||||||
<Toggle text="" bind:value={$values.analytics} />
|
<Input thin bind:value={$values.company} />
|
||||||
|
</div>
|
||||||
|
<div class="field logo">
|
||||||
|
<Label size="L">Logo</Label>
|
||||||
|
<div class="file">
|
||||||
|
<Dropzone
|
||||||
|
value={[$values.logo]}
|
||||||
|
on:change={e => {
|
||||||
|
$values.logo = e.detail?.[0]
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Divider size="S" />
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading size="S">Platform</Heading>
|
||||||
|
<Body size="S">Here you can set up general platform settings.</Body>
|
||||||
|
</Layout>
|
||||||
|
<div class="fields">
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Platform URL</Label>
|
||||||
|
<Input thin bind:value={$values.platformUrl} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider size="S" />
|
||||||
|
<Layout gap="S" noPadding>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading size="S">Analytics</Heading>
|
||||||
|
<Body size="S">
|
||||||
|
If you would like to send analytics that help us make Budibase better,
|
||||||
|
please let us know below.
|
||||||
|
</Body>
|
||||||
|
</Layout>
|
||||||
|
<div class="fields">
|
||||||
|
<div class="field">
|
||||||
|
<Label size="L">Send Analytics to Budibase</Label>
|
||||||
|
<Toggle text="" bind:value={$values.analytics} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<div>
|
||||||
|
<Button disabled={loading} on:click={saveConfig} cta>Save</Button>
|
||||||
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
<div>
|
{/if}
|
||||||
<Button disabled={loading} on:click={saveConfig} cta>Save</Button>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.fields {
|
.fields {
|
||||||
|
|
|
@ -5,19 +5,27 @@ export function createAuthStore() {
|
||||||
const user = writable(null)
|
const user = writable(null)
|
||||||
const store = derived(user, $user => {
|
const store = derived(user, $user => {
|
||||||
let initials = null
|
let initials = null
|
||||||
|
let isAdmin = false
|
||||||
|
let isBuilder = false
|
||||||
if ($user) {
|
if ($user) {
|
||||||
if ($user.firstName) {
|
if ($user.firstName) {
|
||||||
initials = $user.firstName[0]
|
initials = $user.firstName[0]
|
||||||
if ($user.lastName) {
|
if ($user.lastName) {
|
||||||
initials += $user.lastName[0]
|
initials += $user.lastName[0]
|
||||||
}
|
}
|
||||||
} else {
|
} else if ($user.email) {
|
||||||
initials = $user.email[0]
|
initials = $user.email[0]
|
||||||
|
} else {
|
||||||
|
initials = "Unknown"
|
||||||
}
|
}
|
||||||
|
isAdmin = !!$user.admin?.global
|
||||||
|
isBuilder = !!$user.builder?.global
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
user: $user,
|
user: $user,
|
||||||
initials,
|
initials,
|
||||||
|
isAdmin,
|
||||||
|
isBuilder,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ const Router = require("@koa/router")
|
||||||
const controller = require("../../controllers/admin/email")
|
const controller = require("../../controllers/admin/email")
|
||||||
const { EmailTemplatePurpose } = require("../../../constants")
|
const { EmailTemplatePurpose } = require("../../../constants")
|
||||||
const joiValidator = require("../../../middleware/joi-validator")
|
const joiValidator = require("../../../middleware/joi-validator")
|
||||||
|
const adminOnly = require("../../../middleware/adminOnly")
|
||||||
const Joi = require("joi")
|
const Joi = require("joi")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
@ -21,6 +22,7 @@ function buildEmailSendValidation() {
|
||||||
router.post(
|
router.post(
|
||||||
"/api/admin/email/send",
|
"/api/admin/email/send",
|
||||||
buildEmailSendValidation(),
|
buildEmailSendValidation(),
|
||||||
|
adminOnly,
|
||||||
controller.sendEmail
|
controller.sendEmail
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -54,16 +54,8 @@ router
|
||||||
buildUserSaveValidation(),
|
buildUserSaveValidation(),
|
||||||
controller.save
|
controller.save
|
||||||
)
|
)
|
||||||
.get("/api/admin/users", controller.fetch)
|
.get("/api/admin/users", adminOnly, controller.fetch)
|
||||||
.post("/api/admin/users/init", controller.adminUser)
|
|
||||||
.get("/api/admin/users/self", controller.getSelf)
|
|
||||||
.post(
|
|
||||||
"/api/admin/users/self",
|
|
||||||
buildUserSaveValidation(true),
|
|
||||||
controller.updateSelf
|
|
||||||
)
|
|
||||||
.delete("/api/admin/users/:id", adminOnly, controller.destroy)
|
.delete("/api/admin/users/:id", adminOnly, controller.destroy)
|
||||||
.get("/api/admin/users/:id", controller.find)
|
|
||||||
.get("/api/admin/roles/:appId")
|
.get("/api/admin/roles/:appId")
|
||||||
.post(
|
.post(
|
||||||
"/api/admin/users/invite",
|
"/api/admin/users/invite",
|
||||||
|
@ -71,10 +63,20 @@ router
|
||||||
buildInviteValidation(),
|
buildInviteValidation(),
|
||||||
controller.invite
|
controller.invite
|
||||||
)
|
)
|
||||||
|
// non-admin endpoints
|
||||||
|
.post(
|
||||||
|
"/api/admin/users/self",
|
||||||
|
buildUserSaveValidation(true),
|
||||||
|
controller.updateSelf
|
||||||
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/admin/users/invite/accept",
|
"/api/admin/users/invite/accept",
|
||||||
buildInviteAcceptValidation(),
|
buildInviteAcceptValidation(),
|
||||||
controller.inviteAccept
|
controller.inviteAccept
|
||||||
)
|
)
|
||||||
|
.post("/api/admin/users/init", controller.adminUser)
|
||||||
|
.get("/api/admin/users/self", controller.getSelf)
|
||||||
|
// admin endpoint but needs to come at end (blocks other endpoints otherwise)
|
||||||
|
.get("/api/admin/users/:id", adminOnly, controller.find)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
module.exports = async (ctx, next) => {
|
module.exports = async (ctx, next) => {
|
||||||
if (!ctx.user || !ctx.user.admin || !ctx.user.admin.global) {
|
if (
|
||||||
|
!ctx.internal &&
|
||||||
|
(!ctx.user || !ctx.user.admin || !ctx.user.admin.global)
|
||||||
|
) {
|
||||||
ctx.throw(403, "Admin user only endpoint.")
|
ctx.throw(403, "Admin user only endpoint.")
|
||||||
}
|
}
|
||||||
return next()
|
return next()
|
||||||
|
|
Loading…
Reference in New Issue