refactor groups into pro and some other pr comments

This commit is contained in:
Peter Clement 2022-07-26 20:04:29 +01:00
parent 3914501084
commit 0c831f369d
11 changed files with 77 additions and 106 deletions

View File

@ -28,9 +28,9 @@ class UsageLimitError extends HTTPError {
} }
class FeatureDisabledError extends HTTPError { class FeatureDisabledError extends HTTPError {
constructor(message, limitName) { constructor(message, featureName) {
super(message, 400, codes.FEATURE_DISABLED, type) super(message, 400, codes.FEATURE_DISABLED, type)
this.limitName = limitName this.featureName = featureName
} }
} }

View File

@ -7,7 +7,7 @@ import {
GroupUpdatedEvent, GroupUpdatedEvent,
GroupUsersAddedEvent, GroupUsersAddedEvent,
GroupUsersDeletedEvent, GroupUsersDeletedEvent,
GroupsAddedOnboarding, GroupAddedOnboardingEvent,
UserGroupRoles, UserGroupRoles,
} from "@budibase/types" } from "@budibase/types"
@ -34,24 +34,22 @@ export async function deleted(group: UserGroup) {
export async function usersAdded(emails: string[], group: UserGroup) { export async function usersAdded(emails: string[], group: UserGroup) {
const properties: GroupUsersAddedEvent = { const properties: GroupUsersAddedEvent = {
emails,
count: emails.length, count: emails.length,
groupId: group._id as string, groupId: group._id as string,
} }
await publishEvent(Event.USER_GROUP_USER_ADDED, properties) await publishEvent(Event.USER_GROUP_USERS_ADDED, properties)
} }
export async function usersDeleted(emails: string[], group: UserGroup) { export async function usersDeleted(emails: string[], group: UserGroup) {
const properties: GroupUsersDeletedEvent = { const properties: GroupUsersDeletedEvent = {
emails,
count: emails.length, count: emails.length,
groupId: group._id as string, groupId: group._id as string,
} }
await publishEvent(Event.USER_GROUP_USER_REMOVED, properties) await publishEvent(Event.USER_GROUP_USERS_REMOVED, properties)
} }
export async function createdOnboarding(groupId: string) { export async function createdOnboarding(groupId: string) {
const properties: GroupsAddedOnboarding = { const properties: GroupAddedOnboardingEvent = {
groupId: groupId, groupId: groupId,
onboarding: true, onboarding: true,
} }

View File

@ -24,6 +24,21 @@ describe("/users", () => {
describe("fetch", () => { describe("fetch", () => {
it("returns a list of users from an instance db", async () => {
await config.createUser("uuidx")
await config.createUser("uuidy")
const res = await request
.get(`/api/users/metadata`)
.set(config.defaultHeaders())
.expect("Content-Type", /json/)
.expect(200)
expect(res.body.length).toBe(3)
expect(res.body.find(u => u._id === `ro_ta_users_us_uuidx`)).toBeDefined()
expect(res.body.find(u => u._id === `ro_ta_users_us_uuidy`)).toBeDefined()
})
it("should apply authorization to endpoint", async () => { it("should apply authorization to endpoint", async () => {
await config.createUser() await config.createUser()
await checkPermissionsEndpoint({ await checkPermissionsEndpoint({

View File

@ -14,8 +14,9 @@ const env = require("../environment")
const { getAppId } = require("@budibase/backend-core/context") const { getAppId } = require("@budibase/backend-core/context")
const { groups } = require("@budibase/pro") const { groups } = require("@budibase/pro")
exports.updateAppRole = async (user, { appId } = {}) => { exports.updateAppRole = (user, { appId } = {}) => {
appId = appId || getAppId() appId = appId || getAppId()
if (!user || !user.roles) { if (!user || !user.roles) {
return user return user
} }
@ -31,21 +32,33 @@ exports.updateAppRole = async (user, { appId } = {}) => {
// if a role wasn't found then either set as admin (builder) or public (everyone else) // if a role wasn't found then either set as admin (builder) or public (everyone else)
if (!user.roleId && user.builder && user.builder.global) { if (!user.roleId && user.builder && user.builder.global) {
user.roleId = BUILTIN_ROLE_IDS.ADMIN user.roleId = BUILTIN_ROLE_IDS.ADMIN
} else if (!user.roleId && !user?.userGroups?.length) { } else if (!user.roleId) {
user.roleId = BUILTIN_ROLE_IDS.PUBLIC user.roleId = BUILTIN_ROLE_IDS.PUBLIC
} else if (!user.roleId && user?.userGroups?.length) {
let roleId = await groups.getGroupRoleId(user, appId)
user.roleId = roleId
} }
delete user.roles delete user.roles
return user return user
} }
function processUser(user, { appId } = {}) { async function checkGroupRoles(user, { appId } = {}) {
if (!user.roleId) {
let roleId = await groups.getGroupRoleId(user, appId)
user.roleId = roleId
}
return user
}
async function processUser(user, { appId } = {}) {
if (user) { if (user) {
delete user.password delete user.password
} }
return exports.updateAppRole(user, { appId }) user = await exports.updateAppRole(user, { appId })
if (user?.userGroups?.length) {
user = await checkGroupRoles(user, { appId })
}
return user
} }
exports.getCachedSelf = async (ctx, appId) => { exports.getCachedSelf = async (ctx, appId) => {
@ -93,6 +106,8 @@ exports.getGlobalUsers = async (users = null) => {
if (!appId) { if (!appId) {
return globalUsers return globalUsers
} }
console.log("maybe??")
return globalUsers.map(user => exports.updateAppRole(user)) return globalUsers.map(user => exports.updateAppRole(user))
} }

View File

@ -14,8 +14,8 @@ export interface User extends Document {
password?: string password?: string
status?: string status?: string
createdAt?: number createdAt?: number // override the default createdAt behaviour - users sdk historically set this to Date.now()
userGroups?: string[] // override the default createdAt behaviour - users sdk historically set this to Date.now() userGroups?: string[]
} }
export interface UserRoles { export interface UserRoles {

View File

@ -5,7 +5,7 @@ export interface UserGroup extends Document {
icon: string icon: string
color: string color: string
users: User[] users: User[]
apps: any apps: any[]
roles: UserGroupRoles roles: UserGroupRoles
createdAt?: number createdAt?: number
} }

View File

@ -155,10 +155,10 @@ export enum Event {
USER_GROUP_CREATED = "user_group:created", USER_GROUP_CREATED = "user_group:created",
USER_GROUP_UPDATED = "user_group:updated", USER_GROUP_UPDATED = "user_group:updated",
USER_GROUP_DELETED = "user_group:deleted", USER_GROUP_DELETED = "user_group:deleted",
USER_GROUP_USER_ADDED = "user_group_user:added", USER_GROUP_USERS_ADDED = "user_group:user_added",
USER_GROUP_USER_REMOVED = "user_group_user:deleted", USER_GROUP_USERS_REMOVED = "user_group_:users_deleted",
USER_GROUP_PERMISSIONS_EDITED = "user_group_permissions:edited", USER_GROUP_PERMISSIONS_EDITED = "user_group_:permissions_edited",
USER_GROUP_ONBOARDING = "user_group_onboarding:added", USER_GROUP_ONBOARDING = "user_group_:onboarding_added",
} }
// properties added at the final stage of the event pipeline // properties added at the final stage of the event pipeline

View File

@ -13,18 +13,16 @@ export interface GroupDeletedEvent extends BaseEvent {
} }
export interface GroupUsersAddedEvent extends BaseEvent { export interface GroupUsersAddedEvent extends BaseEvent {
emails: string[]
count: number count: number
groupId: string groupId: string
} }
export interface GroupUsersDeletedEvent extends BaseEvent { export interface GroupUsersDeletedEvent extends BaseEvent {
emails: string[]
count: number count: number
groupId: string groupId: string
} }
export interface GroupsAddedOnboarding extends BaseEvent { export interface GroupAddedOnboardingEvent extends BaseEvent {
groupId: string groupId: string
onboarding: boolean onboarding: boolean
} }

View File

@ -13,7 +13,7 @@ import {
cache, cache,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { checkAnyUserExists } from "../../../utilities/users" import { checkAnyUserExists } from "../../../utilities/users"
import { groups as groupUtils } from "@budibase/pro"
const MAX_USERS_UPLOAD_LIMIT = 1000 const MAX_USERS_UPLOAD_LIMIT = 1000
export const save = async (ctx: any) => { export const save = async (ctx: any) => {
@ -46,34 +46,8 @@ export const bulkCreate = async (ctx: any) => {
try { try {
let response = await users.bulkCreate(newUsersRequested, groups) let response = await users.bulkCreate(newUsersRequested, groups)
await groupUtils.bulkSaveGroupUsers(groupsToSave, response)
if (groupsToSave.length) {
let groupsPromises: any = []
groupsToSave.forEach(async (userGroup: UserGroup) => {
userGroup.users = [...userGroup.users, ...response]
groupsPromises.push(db.put(userGroup))
})
const groupResults = await Promise.all(groupsPromises)
await db.bulkDocs(groupResults)
let eventFns = []
for (const group of groupResults) {
eventFns.push(() => {
events.group.usersAdded(
response.map(u => u.email),
group
)
})
eventFns.push(() => {
events.group.createdOnboarding(group._id as string)
})
}
for (const fn of eventFns) {
await fn()
}
}
ctx.body = response ctx.body = response
} catch (err: any) { } catch (err: any) {
ctx.throw(err.status || 400, err) ctx.throw(err.status || 400, err)
@ -142,27 +116,9 @@ export const adminUser = async (ctx: any) => {
export const destroy = async (ctx: any) => { export const destroy = async (ctx: any) => {
const id = ctx.params.id const id = ctx.params.id
const db = tenancy.getGlobalDB()
let user: User = await db.get(id)
let groups = user.userGroups
await users.destroy(id, ctx.user) await users.destroy(id, ctx.user)
// Remove asssosicated groups
if (groups) {
let groupsPromises = []
for (const groupId of groups) {
let group = await db.get(groupId)
let updatedUsersGroup = group.users.filter(
(groupUser: any) => groupUser.email !== user.email
)
group.users = updatedUsersGroup
groupsPromises.push(db.put(group))
}
await db.bulkDocs(groupsPromises)
}
ctx.body = { ctx.body = {
message: `User ${id} deleted.`, message: `User ${id} deleted.`,
} }
@ -170,30 +126,9 @@ export const destroy = async (ctx: any) => {
export const bulkDelete = async (ctx: any) => { export const bulkDelete = async (ctx: any) => {
const { userIds } = ctx.request.body const { userIds } = ctx.request.body
const db = tenancy.getGlobalDB()
try { try {
let { groupsToModify, usersResponse } = await users.bulkDelete(userIds) let { groupsToModify, usersResponse } = await users.bulkDelete(userIds)
await groupUtils.bulkDeleteGroupUsers(groupsToModify)
// if there are groups to delete, do it here
if (Object.keys(groupsToModify).length) {
let groups = (
await db.allDocs({
include_docs: true,
keys: Object.keys(groupsToModify),
})
).rows.map((group: any) => group.doc)
let groupsPromises = []
for (const group of groups) {
let updatedUsersGroup = group.users.filter(
(groupUser: any) => !groupsToModify[group._id].includes(groupUser._id)
)
group.users = updatedUsersGroup
groupsPromises.push(db.put(group))
}
await db.bulkDocs(groupsPromises)
}
ctx.body = { ctx.body = {
message: `${usersResponse.length} user(s) deleted`, message: `${usersResponse.length} user(s) deleted`,
@ -314,7 +249,6 @@ export const inviteAccept = async (ctx: any) => {
return saved return saved
}) })
} catch (err: any) { } catch (err: any) {
console.log(err)
if (err.code === errors.codes.USAGE_LIMIT_EXCEEDED) { if (err.code === errors.codes.USAGE_LIMIT_EXCEEDED) {
// explicitly re-throw limit exceeded errors // explicitly re-throw limit exceeded errors
ctx.throw(400, err) ctx.throw(400, err)

View File

@ -1,2 +1 @@
export * from "./users" export * from "./users"
export * from "./events"

View File

@ -3,7 +3,6 @@ import { quotas } from "@budibase/pro"
import * as apps from "../../utilities/appService" import * as apps from "../../utilities/appService"
import * as eventHelpers from "./events" import * as eventHelpers from "./events"
import { import {
events,
tenancy, tenancy,
utils, utils,
db as dbUtils, db as dbUtils,
@ -16,8 +15,8 @@ import {
accounts, accounts,
migrations, migrations,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { MigrationType, UserGroup } from "@budibase/types" import { MigrationType, User } from "@budibase/types"
import { build } from "joi" import { groups as groupUtils } from "@budibase/pro/"
const PAGE_LIMIT = 8 const PAGE_LIMIT = 8
@ -107,10 +106,11 @@ export const buildUser = async (
) => { ) => {
let { password, _id } = user let { password, _id } = user
// get the password, make sure one is defined
let hashedPassword let hashedPassword
if (password) { if (password) {
hashedPassword = opts.hashPassword ? await utils.hash(password) : password hashedPassword = opts.hashPassword ? await utils.hash(password) : password
} else if (dbUser) {
hashedPassword = dbUser.password
} else if (opts.requirePassword) { } else if (opts.requirePassword) {
throw "Password must be specified." throw "Password must be specified."
} }
@ -125,7 +125,6 @@ export const buildUser = async (
password: hashedPassword, password: hashedPassword,
tenantId, tenantId,
} }
// make sure the roles object is always present // make sure the roles object is always present
if (!user.roles) { if (!user.roles) {
user.roles = {} user.roles = {}
@ -202,6 +201,7 @@ export const save = async (
const putUserFn = () => { const putUserFn = () => {
return db.put(builtUser) return db.put(builtUser)
} }
console.log(builtUser)
if (eventHelpers.isAddingBuilder(builtUser, dbUser)) { if (eventHelpers.isAddingBuilder(builtUser, dbUser)) {
response = await quotas.addDeveloper(putUserFn) response = await quotas.addDeveloper(putUserFn)
} else { } else {
@ -244,7 +244,10 @@ export const addTenant = async (
} }
} }
export const bulkCreate = async (newUsersRequested: any[], groups: any) => { export const bulkCreate = async (
newUsersRequested: User[],
groups: string[]
) => {
const db = tenancy.getGlobalDB() const db = tenancy.getGlobalDB()
const tenantId = tenancy.getTenantId() const tenantId = tenancy.getTenantId()
@ -291,16 +294,18 @@ export const bulkCreate = async (newUsersRequested: any[], groups: any) => {
}) })
const usersToBulkSave = await Promise.all(usersToSave) const usersToBulkSave = await Promise.all(usersToSave)
await quotas.addDevelopers(() => db.bulkDocs(usersToBulkSave), builderCount) const response = await quotas.addDevelopers(
() => db.bulkDocs(usersToBulkSave),
builderCount
)
// Post processing of bulk added users, i.e events and cache operations // Post processing of bulk added users, i.e events and cache operations
for (const user of usersToBulkSave) { for (const user of usersToBulkSave) {
delete user.password //await eventHelpers.handleSaveEvents(user, null)
await eventHelpers.handleSaveEvents(user, null) //await apps.syncUserInApps(user._id)
await apps.syncUserInApps(user._id)
} }
return usersToBulkSave return response
} }
export const bulkDelete = async (userIds: any) => { export const bulkDelete = async (userIds: any) => {
@ -356,6 +361,7 @@ export const bulkDelete = async (userIds: any) => {
export const destroy = async (id: string, currentUser: any) => { export const destroy = async (id: string, currentUser: any) => {
const db = tenancy.getGlobalDB() const db = tenancy.getGlobalDB()
const dbUser = await db.get(id) const dbUser = await db.get(id)
let groups = dbUser.userGroups
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
// root account holder can't be deleted from inside budibase // root account holder can't be deleted from inside budibase
@ -371,7 +377,13 @@ export const destroy = async (id: string, currentUser: any) => {
} }
await deprovisioning.removeUserFromInfoDB(dbUser) await deprovisioning.removeUserFromInfoDB(dbUser)
await db.remove(dbUser._id, dbUser._rev) await db.remove(dbUser._id, dbUser._rev)
if (groups) {
await groupUtils.deleteGroupUsers(groups, dbUser)
}
await eventHelpers.handleDeleteEvents(dbUser) await eventHelpers.handleDeleteEvents(dbUser)
await quotas.removeUser(dbUser) await quotas.removeUser(dbUser)
await cache.user.invalidateUser(dbUser._id) await cache.user.invalidateUser(dbUser._id)