Merge pull request #1538 from Budibase/fix/lockdown-admin

Locking down administration endpoints and routes
This commit is contained in:
Michael Drury 2021-05-24 13:49:22 +01:00 committed by GitHub
commit 0b728f07fa
14 changed files with 181 additions and 106 deletions

View File

@ -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)

View File

@ -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

View File

@ -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}

View File

@ -1,7 +0,0 @@
<script>
import { Page } from "@budibase/bbui"
</script>
<Page>
<slot />
</Page>

View File

@ -1,5 +1,6 @@
<script> <script>
import { email } from "stores/portal" import { email } from "stores/portal"
email.templates.fetch() email.templates.fetch()
</script> </script>

View File

@ -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

View File

@ -1,7 +0,0 @@
<script>
import { Page } from "@budibase/bbui"
</script>
<Page>
<slot />
</Page>

View File

@ -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

View File

@ -1,4 +1,4 @@
<script> <script>
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
$goto("./general") $goto("./organisation")
</script> </script>

View File

@ -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 {

View File

@ -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,
} }
}) })

View File

@ -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
) )

View File

@ -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

View File

@ -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()