Skip to content

Commit

Permalink
feat(condo): INFRA-395 allow users with rights sets to read specific …
Browse files Browse the repository at this point in the history
…messages 📞 (#5028)

* refactor(condo): INFRA-401 limit amount of user info in push transport

* feat(condo): INFRA-401 Message added to direct access

* feat(condo): INFRA-395 direct access logic added
  • Loading branch information
SavelevMatthew authored Jul 29, 2024
1 parent 7e20e8f commit 6317597
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 63 deletions.
11 changes: 10 additions & 1 deletion apps/condo/domains/notification/access/Message.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@

const { throwAuthenticationError } = require('@open-condo/keystone/apolloErrorFormatter')

async function canReadMessages ({ authentication: { item: user } }) {
const { DIRECTLY_AVAILABLE_TYPES } = require('@condo/domains/notification/constants/constants')
const { canDirectlyReadSchemaObjects } = require('@condo/domains/user/utils/directAccess')

async function canReadMessages ({ authentication: { item: user }, listKey }) {
if (!user) return throwAuthenticationError()
if (user.deletedAt) return false

if (user.isAdmin) return {}

const haveDirectRight = await canDirectlyReadSchemaObjects(user, listKey)

if (haveDirectRight) {
return { type_in: DIRECTLY_AVAILABLE_TYPES }
}

return { user: { id: user.id } }
}

Expand Down
6 changes: 6 additions & 0 deletions apps/condo/domains/notification/constants/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,11 @@ const HUAWEI_APP_TYPE_BY_APP_ID = {
const DEFAULT_TEMPLATE_FILE_EXTENSION = 'njk'
const DEFAULT_TEMPLATE_FILE_NAME = `default.${DEFAULT_TEMPLATE_FILE_EXTENSION}`

const DIRECTLY_AVAILABLE_TYPES = [
B2C_APP_MESSAGE_PUSH_TYPE,
VOIP_INCOMING_CALL_MESSAGE_TYPE,
]

module.exports = {
JSON_NO_REQUIRED_ATTR_ERROR,
JSON_SUSPICIOUS_ATTR_NAME_ERROR,
Expand Down Expand Up @@ -1142,5 +1147,6 @@ module.exports = {
TITLE_IS_REQUIRED_FOR_CUSTOM_CONTENT_MESSAGE_TYPE,
SEND_DAILY_STATISTICS_MESSAGE_TYPE,
APPS_WITH_DISABLED_NOTIFICATIONS_ENV,
DIRECTLY_AVAILABLE_TYPES,
}

128 changes: 68 additions & 60 deletions apps/condo/domains/notification/schema/Message.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,46 @@ const {
} = require('@open-condo/keystone/test.utils')

const { DUPLICATE_CONSTRAINT_VIOLATION_ERROR_MESSAGE } = require('@condo/domains/common/constants/errors')
const { INVITE_NEW_EMPLOYEE_MESSAGE_TYPE } = require('@condo/domains/notification/constants/constants')
const { INVITE_NEW_EMPLOYEE_MESSAGE_TYPE, VOIP_INCOMING_CALL_MESSAGE_TYPE } = require('@condo/domains/notification/constants/constants')
const { Message, createTestMessage, updateTestMessage } = require('@condo/domains/notification/utils/testSchema')
const { makeClientWithRegisteredOrganization, createTestOrganization } = require('@condo/domains/organization/utils/testSchema')
const {
makeClientWithNewRegisteredAndLoggedInUser,
createTestUserRightsSet,
updateTestUser,
} = require('@condo/domains/user/utils/testSchema')

describe('Message', () => {
test('admin: create Message', async () => {
const client = await makeLoggedInAdminClient()
let admin
let user
let anonymous
let permittedClient

beforeAll(async () => {
admin = await makeLoggedInAdminClient()
user = await makeClientWithRegisteredOrganization()
anonymous = await makeClient()
permittedClient = await makeClientWithNewRegisteredAndLoggedInUser()

const [rightsSet] = await createTestUserRightsSet(admin, {
canReadMessages: true,
})

await updateTestUser(admin, permittedClient.user.id, {
rightsSet: { connect: { id: rightsSet.id } },
})
})

const [obj, attrs] = await createTestMessage(client)
test('admin: create Message', async () => {
const [obj, attrs] = await createTestMessage(admin)
expect(obj.id).toMatch(UUID_RE)
expect(obj.dv).toEqual(1)
expect(obj.sender).toEqual(attrs.sender)
expect(obj.v).toEqual(1)
expect(obj.newId).toEqual(null)
expect(obj.deletedAt).toEqual(null)
expect(obj.createdBy).toEqual(expect.objectContaining({ id: client.user.id }))
expect(obj.updatedBy).toEqual(expect.objectContaining({ id: client.user.id }))
expect(obj.createdBy).toEqual(expect.objectContaining({ id: admin.user.id }))
expect(obj.updatedBy).toEqual(expect.objectContaining({ id: admin.user.id }))
expect(obj.createdAt).toMatch(DATETIME_RE)
expect(obj.updatedAt).toMatch(DATETIME_RE)
expect(obj.status).toEqual('sending')
Expand All @@ -44,62 +67,54 @@ describe('Message', () => {
})

test('cannot connect another organization', async () => {
const adminClient = await makeLoggedInAdminClient()
const [obj] = await createTestMessage(adminClient)
const [newOrganization] = await createTestOrganization(adminClient)
const [obj] = await createTestMessage(admin)
const [newOrganization] = await createTestOrganization(admin)
const payload = {
organization: { connect: newOrganization.id },
}
await catchErrorFrom(async () => {
await updateTestMessage(adminClient, obj.id, payload)
await updateTestMessage(admin, obj.id, payload)
}, (e) => {
expect(e.errors[0].message).toContain('Field "organization" is not defined by type "MessageUpdateInput"')
})
})


test('cannot disconnect organization', async () => {
const adminClient = await makeLoggedInAdminClient()
const [obj] = await createTestMessage(adminClient)
await createTestOrganization(adminClient)
const [obj] = await createTestMessage(admin)
await createTestOrganization(admin)
const payload = {
organization: { disconnectAll: true },
}
await catchErrorFrom(async () => {
await updateTestMessage(adminClient, obj.id, payload)
await updateTestMessage(admin, obj.id, payload)
}, (e) => {
expect(e.errors[0].message).toContain('Field "organization" is not defined by type "MessageUpdateInput"')
})
})

test('admin: update Message', async () => {
const client = await makeLoggedInAdminClient()

const [objCreated] = await createTestMessage(client)
const [obj] = await updateTestMessage(client, objCreated.id, { email: '[email protected]' })
const [objCreated] = await createTestMessage(admin)
const [obj] = await updateTestMessage(admin, objCreated.id, { email: '[email protected]' })
expect(obj.email).toEqual('[email protected]')
})

test('user: create Message', async () => {
const client = await makeClientWithRegisteredOrganization()
await expectToThrowAccessDeniedErrorToObj(async () => {
await createTestMessage(client)
await createTestMessage(user)
})
})

test('anonymous: create Message', async () => {
const client = await makeClient()
await expectToThrowAuthenticationErrorToObj(async () => {
await createTestMessage(client)
await createTestMessage(anonymous)
})
})

test('user: read Message', async () => {
const client = await makeClientWithRegisteredOrganization()
const [obj, attrs] = await createTestMessage(admin, { user: { connect: { id: user.user.id } } })

const admin = await makeLoggedInAdminClient()
const [obj, attrs] = await createTestMessage(admin, { user: { connect: { id: client.user.id } } })

const obj1 = await Message.getOne(client, { id: obj.id })
const obj1 = await Message.getOne(user, { id: obj.id })

expect(obj1.type).toEqual(INVITE_NEW_EMPLOYEE_MESSAGE_TYPE)
expect(obj1.id).toMatch(obj.id)
Expand All @@ -115,84 +130,81 @@ describe('Message', () => {
})

test('anonymous: read Message', async () => {
const client = await makeClient()

await expectToThrowAuthenticationErrorToObjects(async () => {
await Message.getAll(client)
await Message.getAll(anonymous)
})
})

test('user: update Message', async () => {
const client = await makeClientWithRegisteredOrganization()

const admin = await makeLoggedInAdminClient()
const [objCreated] = await createTestMessage(admin, { user: { connect: { id: client.user.id } } })
const [objCreated] = await createTestMessage(admin, { user: { connect: { id: user.user.id } } })

const payload = {}
await expectToThrowAccessDeniedErrorToObj(async () => {
await updateTestMessage(client, objCreated.id, payload)
await updateTestMessage(user, objCreated.id, payload)
})
})

test('anonymous: update Message', async () => {
const admin = await makeLoggedInAdminClient()
const [objCreated] = await createTestMessage(admin)

const client = await makeClient()
const payload = {}
await expectToThrowAuthenticationErrorToObj(async () => {
await updateTestMessage(client, objCreated.id, payload)
await updateTestMessage(anonymous, objCreated.id, payload)
})
})

test('user: delete Message', async () => {
const client = await makeClientWithRegisteredOrganization()

const admin = await makeLoggedInAdminClient()
const [objCreated] = await createTestMessage(admin, { user: { connect: { id: client.user.id } } })
const [objCreated] = await createTestMessage(admin, { user: { connect: { id: user.user.id } } })

await expectToThrowAccessDeniedErrorToObj(async () => {
await Message.delete(client, objCreated.id)
await Message.delete(user, objCreated.id)
})
})

test('anonymous: delete Message', async () => {
const admin = await makeLoggedInAdminClient()
const [objCreated] = await createTestMessage(admin)

const client = await makeClient()
await expectToThrowAccessDeniedErrorToObj(async () => {
await Message.delete(client, objCreated.id)
await Message.delete(anonymous, objCreated.id)
})
})

test('admin: create with wrong sender', async () => {
const admin = await makeLoggedInAdminClient()
await expectToThrowGraphQLRequestError(
async () => await createTestMessage(admin, { sender: 'invalid' }),
'Variable "$data" got invalid value "invalid" at "data.sender"; Expected type "SenderFieldInput" to be an object.',
)
})

test('User with direct rights set can read specific message types', async () => {
const [wrongMessage] = await createTestMessage(admin)
const [correctMessage] = await createTestMessage(admin, {
type: VOIP_INCOMING_CALL_MESSAGE_TYPE,
})

const messages = await Message.getAll(permittedClient, { id_in: [wrongMessage.id, correctMessage.id] })
expect(messages).toHaveLength(1)
expect(messages).toEqual([
expect.objectContaining({ id: correctMessage.id }),
])
})

describe('Message constraints', () => {
it('checks Message constraints with non-null uniqKey and user', async () => {
const client = await makeClientWithRegisteredOrganization()
const admin = await makeLoggedInAdminClient()
const uniqKey = faker.datatype.uuid()

const [obj] = await createTestMessage(admin, { user: { connect: { id: client.user.id } }, uniqKey })
const [obj] = await createTestMessage(admin, { user: { connect: { id: user.user.id } }, uniqKey })

expect(obj.uniqKey).toMatch(uniqKey)
expect(obj.user.id).toMatch(client.user.id)
expect(obj.user.id).toMatch(user.user.id)

await expectToThrowInternalError(
async () => await createTestMessage(admin, { user: { connect: { id: client.user.id } }, uniqKey }),
async () => await createTestMessage(admin, { user: { connect: { id: user.user.id } }, uniqKey }),
DUPLICATE_CONSTRAINT_VIOLATION_ERROR_MESSAGE,
)
})

it('checks Message constraints with non-null uniqKey and nullish user', async () => {
const admin = await makeLoggedInAdminClient()
const uniqKey = faker.datatype.uuid()

const [obj] = await createTestMessage(admin, { user: null, uniqKey })
Expand All @@ -207,22 +219,20 @@ describe('Message', () => {
})

it('checks that Message constraints with non-null uniqKey and nullish/non-null user do not interfere', async () => {
const client = await makeClientWithRegisteredOrganization()
const admin = await makeLoggedInAdminClient()
const uniqKey = faker.datatype.uuid()

const [obj] = await createTestMessage(admin, { user: { connect: { id: client.user.id } }, uniqKey })
const [obj] = await createTestMessage(admin, { user: { connect: { id: user.user.id } }, uniqKey })

expect(obj.uniqKey).toMatch(uniqKey)
expect(obj.user.id).toMatch(client.user.id)
expect(obj.user.id).toMatch(user.user.id)

const [obj1] = await createTestMessage(admin, { uniqKey })

expect(obj1.uniqKey).toMatch(uniqKey)
expect(obj1.user).toBeNull()

await expectToThrowInternalError(
async () => await createTestMessage(admin, { user: { connect: { id: client.user.id } }, uniqKey }),
async () => await createTestMessage(admin, { user: { connect: { id: user.user.id } }, uniqKey }),
DUPLICATE_CONSTRAINT_VIOLATION_ERROR_MESSAGE,
)

Expand All @@ -233,8 +243,6 @@ describe('Message', () => {
})

it('checks that there is no Message constraints for both nullish uniqKey and user', async () => {
const admin = await makeLoggedInAdminClient()

const [obj] = await createTestMessage(admin, { user: null, uniqKey: null })

expect(obj.uniqKey).toBeNull()
Expand Down
11 changes: 9 additions & 2 deletions apps/condo/domains/notification/transports/push.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const { get, isEmpty } = require('lodash')
const get = require('lodash/get')
const isEmpty = require('lodash/isEmpty')
const pick = require('lodash/pick')

const { find } = require('@open-condo/keystone/schema')

Expand Down Expand Up @@ -94,7 +96,12 @@ async function prepareMessageToSend (message) {
const { user, remoteClient } = message
const { notification, data } = await renderTemplate(PUSH_TRANSPORT, message)

return { notification, data, user, remoteClient }
return {
notification,
data,
user: pick(user, ['id']),
remoteClient,
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions apps/condo/domains/user/utils/directAccess/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const DIRECT_ACCESS_AVAILABLE_SCHEMAS = {
'B2CAppProperty',

// Notification domain
{ schemaName: 'Message', readonly: true },
'MessageBatch',

// Organization domain
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// auto generated by kmigrator
// KMIGRATOR:0413_userrightsset_canreadmessages_and_more:IyBHZW5lcmF0ZWQgYnkgRGphbmdvIDQuMS41IG9uIDIwMjQtMDctMjkgMDg6MjkKCmZyb20gZGphbmdvLmRiIGltcG9ydCBtaWdyYXRpb25zLCBtb2RlbHMKCgpjbGFzcyBNaWdyYXRpb24obWlncmF0aW9ucy5NaWdyYXRpb24pOgoKICAgIGRlcGVuZGVuY2llcyA9IFsKICAgICAgICAoJ19kamFuZ29fc2NoZW1hJywgJzA0MTJfbWFya2V0c2V0dGluZ2hpc3RvcnlyZWNvcmRfYW5kX21vcmUnKSwKICAgIF0KCiAgICBvcGVyYXRpb25zID0gWwogICAgICAgIG1pZ3JhdGlvbnMuQWRkRmllbGQoCiAgICAgICAgICAgIG1vZGVsX25hbWU9J3VzZXJyaWdodHNzZXQnLAogICAgICAgICAgICBuYW1lPSdjYW5SZWFkTWVzc2FnZXMnLAogICAgICAgICAgICBmaWVsZD1tb2RlbHMuQm9vbGVhbkZpZWxkKGRlZmF1bHQ9RmFsc2UpLAogICAgICAgICAgICBwcmVzZXJ2ZV9kZWZhdWx0PUZhbHNlLAogICAgICAgICksCiAgICAgICAgbWlncmF0aW9ucy5BZGRGaWVsZCgKICAgICAgICAgICAgbW9kZWxfbmFtZT0ndXNlcnJpZ2h0c3NldGhpc3RvcnlyZWNvcmQnLAogICAgICAgICAgICBuYW1lPSdjYW5SZWFkTWVzc2FnZXMnLAogICAgICAgICAgICBmaWVsZD1tb2RlbHMuQm9vbGVhbkZpZWxkKGJsYW5rPVRydWUsIG51bGw9VHJ1ZSksCiAgICAgICAgKSwKICAgIF0K

exports.up = async (knex) => {
await knex.raw(`
BEGIN;
--
-- Add field canReadMessages to userrightsset
--
ALTER TABLE "UserRightsSet" ADD COLUMN "canReadMessages" boolean DEFAULT false NOT NULL;
ALTER TABLE "UserRightsSet" ALTER COLUMN "canReadMessages" DROP DEFAULT;
--
-- Add field canReadMessages to userrightssethistoryrecord
--
ALTER TABLE "UserRightsSetHistoryRecord" ADD COLUMN "canReadMessages" boolean NULL;
COMMIT;
`)
}

exports.down = async (knex) => {
await knex.raw(`
BEGIN;
--
-- Add field canReadMessages to userrightssethistoryrecord
--
ALTER TABLE "UserRightsSetHistoryRecord" DROP COLUMN "canReadMessages" CASCADE;
--
-- Add field canReadMessages to userrightsset
--
ALTER TABLE "UserRightsSet" DROP COLUMN "canReadMessages" CASCADE;
COMMIT;
`)
}
Loading

0 comments on commit 6317597

Please sign in to comment.