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
|
||||
if (apiKey && apiKey === env.INTERNAL_API_KEY) {
|
||||
ctx.isAuthenticated = true
|
||||
ctx.internal = true
|
||||
} else if (authCookie) {
|
||||
try {
|
||||
const db = database.getDB(StaticDatabases.GLOBAL.name)
|
||||
|
|
|
@ -20,18 +20,40 @@
|
|||
let userInfoModal
|
||||
let changePasswordModal
|
||||
|
||||
const menu = [
|
||||
{ title: "Apps", href: "/builder/portal/apps" },
|
||||
{ title: "Users", href: "/builder/portal/manage/users", heading: "Manage" },
|
||||
{ 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" },
|
||||
]
|
||||
$: menu = buildMenu($auth.isAdmin)
|
||||
|
||||
const buildMenu = admin => {
|
||||
let menu = [{ title: "Apps", href: "/builder/portal/apps" }]
|
||||
if (admin) {
|
||||
menu = menu.concat([
|
||||
{
|
||||
title: "Users",
|
||||
href: "/builder/portal/manage/users",
|
||||
heading: "Manage",
|
||||
},
|
||||
{ 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 () => {
|
||||
// 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>
|
||||
import { email } from "stores/portal"
|
||||
|
||||
email.templates.fetch()
|
||||
</script>
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { fetchData } from "helpers"
|
||||
import { users } from "stores/portal"
|
||||
import { users, auth } from "stores/portal"
|
||||
|
||||
import TagsRenderer from "./_components/TagsTableRenderer.svelte"
|
||||
import UpdateRolesModal from "./_components/UpdateRolesModal.svelte"
|
||||
|
@ -56,13 +56,21 @@
|
|||
|
||||
let toggleDisabled = false
|
||||
|
||||
async function toggleBuilderAccess({ detail }) {
|
||||
async function toggleFlag(flagName, detail) {
|
||||
toggleDisabled = true
|
||||
await users.save({ ...$userFetch?.data, builder: { global: detail } })
|
||||
await users.save({ ...$userFetch?.data, [flagName]: { global: detail } })
|
||||
await userFetch.refresh()
|
||||
toggleDisabled = false
|
||||
}
|
||||
|
||||
async function toggleBuilderAccess({ detail }) {
|
||||
return toggleFlag("builder", detail)
|
||||
}
|
||||
|
||||
async function toggleAdminAccess({ detail }) {
|
||||
return toggleFlag("admin", detail)
|
||||
}
|
||||
|
||||
async function openUpdateRolesModal({ detail }) {
|
||||
selectedApp = detail
|
||||
editRolesModal.show()
|
||||
|
@ -107,15 +115,27 @@
|
|||
<Label size="L">Last name</Label>
|
||||
<Input disabled thin value={$userFetch?.data?.lastName} />
|
||||
</div>
|
||||
<div class="field">
|
||||
<Label size="L">Development access</Label>
|
||||
<Toggle
|
||||
text=""
|
||||
value={$userFetch?.data?.builder?.global}
|
||||
on:change={toggleBuilderAccess}
|
||||
disabled={toggleDisabled}
|
||||
/>
|
||||
</div>
|
||||
<!-- don't let a user remove the privileges that let them be here -->
|
||||
{#if userId !== $auth.user._id}
|
||||
<div class="field">
|
||||
<Label size="L">Development access</Label>
|
||||
<Toggle
|
||||
text=""
|
||||
value={$userFetch?.data?.builder?.global}
|
||||
on:change={toggleBuilderAccess}
|
||||
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 class="regenerate">
|
||||
<ActionButton
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
<script>
|
||||
import { Page } from "@budibase/bbui"
|
||||
</script>
|
||||
|
||||
<Page>
|
||||
<slot />
|
||||
</Page>
|
|
@ -22,6 +22,7 @@
|
|||
const schema = {
|
||||
email: {},
|
||||
developmentAccess: { displayName: "Development Access", type: "boolean" },
|
||||
adminAccess: { displayName: "Admin Access", type: "boolean" },
|
||||
// role: { type: "options" },
|
||||
group: {},
|
||||
// access: {},
|
||||
|
@ -35,7 +36,8 @@
|
|||
.map(user => ({
|
||||
...user,
|
||||
group: ["All users"],
|
||||
developmentAccess: user.builder.global,
|
||||
developmentAccess: !!user.builder?.global,
|
||||
adminAccess: !!user.admin?.global,
|
||||
}))
|
||||
|
||||
let createUserModal
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
$goto("./general")
|
||||
$goto("./organisation")
|
||||
</script>
|
||||
|
|
|
@ -11,10 +11,18 @@
|
|||
Dropzone,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { organisation } from "stores/portal"
|
||||
import { auth, organisation } from "stores/portal"
|
||||
import { post } from "builderStore/api"
|
||||
import analytics from "analytics"
|
||||
import { writable } from "svelte/store"
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
// Only admins allowed here
|
||||
$: {
|
||||
if (!$auth.isAdmin) {
|
||||
$redirect("../../portal")
|
||||
}
|
||||
}
|
||||
|
||||
const values = writable({
|
||||
analytics: !analytics.disabled(),
|
||||
|
@ -64,68 +72,70 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Layout>
|
||||
<Layout gap="XS" noPadding>
|
||||
<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>
|
||||
{#if $auth.isAdmin}
|
||||
<Layout>
|
||||
<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.
|
||||
<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">Send Analytics to Budibase</Label>
|
||||
<Toggle text="" bind:value={$values.analytics} />
|
||||
<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>
|
||||
<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>
|
||||
<div>
|
||||
<Button disabled={loading} on:click={saveConfig} cta>Save</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.fields {
|
||||
|
|
|
@ -5,19 +5,27 @@ export function createAuthStore() {
|
|||
const user = writable(null)
|
||||
const store = derived(user, $user => {
|
||||
let initials = null
|
||||
let isAdmin = false
|
||||
let isBuilder = false
|
||||
if ($user) {
|
||||
if ($user.firstName) {
|
||||
initials = $user.firstName[0]
|
||||
if ($user.lastName) {
|
||||
initials += $user.lastName[0]
|
||||
}
|
||||
} else {
|
||||
} else if ($user.email) {
|
||||
initials = $user.email[0]
|
||||
} else {
|
||||
initials = "Unknown"
|
||||
}
|
||||
isAdmin = !!$user.admin?.global
|
||||
isBuilder = !!$user.builder?.global
|
||||
}
|
||||
return {
|
||||
user: $user,
|
||||
initials,
|
||||
isAdmin,
|
||||
isBuilder,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ const Router = require("@koa/router")
|
|||
const controller = require("../../controllers/admin/email")
|
||||
const { EmailTemplatePurpose } = require("../../../constants")
|
||||
const joiValidator = require("../../../middleware/joi-validator")
|
||||
const adminOnly = require("../../../middleware/adminOnly")
|
||||
const Joi = require("joi")
|
||||
|
||||
const router = Router()
|
||||
|
@ -21,6 +22,7 @@ function buildEmailSendValidation() {
|
|||
router.post(
|
||||
"/api/admin/email/send",
|
||||
buildEmailSendValidation(),
|
||||
adminOnly,
|
||||
controller.sendEmail
|
||||
)
|
||||
|
||||
|
|
|
@ -54,16 +54,8 @@ router
|
|||
buildUserSaveValidation(),
|
||||
controller.save
|
||||
)
|
||||
.get("/api/admin/users", 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
|
||||
)
|
||||
.get("/api/admin/users", adminOnly, controller.fetch)
|
||||
.delete("/api/admin/users/:id", adminOnly, controller.destroy)
|
||||
.get("/api/admin/users/:id", controller.find)
|
||||
.get("/api/admin/roles/:appId")
|
||||
.post(
|
||||
"/api/admin/users/invite",
|
||||
|
@ -71,10 +63,20 @@ router
|
|||
buildInviteValidation(),
|
||||
controller.invite
|
||||
)
|
||||
// non-admin endpoints
|
||||
.post(
|
||||
"/api/admin/users/self",
|
||||
buildUserSaveValidation(true),
|
||||
controller.updateSelf
|
||||
)
|
||||
.post(
|
||||
"/api/admin/users/invite/accept",
|
||||
buildInviteAcceptValidation(),
|
||||
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
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
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.")
|
||||
}
|
||||
return next()
|
||||
|
|
Loading…
Reference in New Issue