diff --git a/api/src/domain/loaders/load-domain-connections-by-organizations-id.js b/api/src/domain/loaders/load-domain-connections-by-organizations-id.js index ac265265bc..b50a59eb57 100644 --- a/api/src/domain/loaders/load-domain-connections-by-organizations-id.js +++ b/api/src/domain/loaders/load-domain-connections-by-organizations-id.js @@ -314,7 +314,22 @@ export const loadDomainConnectionsByOrgId = sortString = aql`ASC` } - let domainFilters = aql`` + let domainFilters = aql` + LET hidden = ( + FOR v, e IN 1..1 ANY domain._id claims + FILTER e._from == ${orgId} + RETURN e.hidden + )[0] + LET claimTags = ( + FOR v, e IN 1..1 ANY domain._id claims + FILTER e._from == ${orgId} + LET translatedTags = ( + FOR tag IN e.tags || [] + RETURN TRANSLATE(${language}, tag) + ) + RETURN translatedTags + )[0] + ` if (typeof filters !== 'undefined') { filters.forEach(({ filterCategory, comparison, filterValue }) => { if (comparison === '==') { @@ -486,21 +501,6 @@ export const loadDomainConnectionsByOrgId = ${loopString} FILTER domain._key IN domainKeys ${showArchivedDomains} - LET hidden = ( - FOR v, e IN 1..1 ANY domain._id claims - FILTER e._from == ${orgId} - RETURN e.hidden - )[0] - LET claimTags = ( - FOR v, e IN 1..1 ANY domain._id claims - FILTER e._from == ${orgId} - LET translatedTags = ( - FOR tag IN e.tags || [] - RETURN TRANSLATE(${language}, tag) - ) - RETURN translatedTags - )[0] - ${domainFilters} ${afterTemplate} diff --git a/api/src/domain/mutations/__tests__/add-organizations-domains.test.js b/api/src/domain/mutations/__tests__/add-organizations-domains.test.js new file mode 100644 index 0000000000..b1e8adab2c --- /dev/null +++ b/api/src/domain/mutations/__tests__/add-organizations-domains.test.js @@ -0,0 +1,402 @@ +import { ensure, dbNameFromFile } from 'arango-tools' +import { graphql, GraphQLSchema } from 'graphql' +import { toGlobalId } from 'graphql-relay' +import { setupI18n } from '@lingui/core' +import englishMessages from '../../../locale/en/messages' +import frenchMessages from '../../../locale/fr/messages' + +import { createQuerySchema } from '../../../query' +import { createMutationSchema } from '../../../mutation' +import { cleanseInput } from '../../../validators' +import { + checkPermission, + userRequired, + saltedHash, + verifiedRequired, + tfaRequired, +} from '../../../auth' +import { loadDomainByDomain } from '../../loaders' +import { loadOrgByKey } from '../../../organization/loaders' +import { loadUserByKey } from '../../../user/loaders' +import dbschema from '../../../../database.json' +import { collectionNames } from '../../../collection-names' + +const { DB_PASS: rootPass, DB_URL: url, HASHING_SECRET } = process.env + +describe('given the addOrganizationsDomains mutation', () => { + let query, drop, i18n, truncate, schema, collections, transaction, user, org + + const consoleOutput = [] + const mockedInfo = (output) => consoleOutput.push(output) + const mockedWarn = (output) => consoleOutput.push(output) + const mockedError = (output) => consoleOutput.push(output) + beforeAll(async () => { + console.info = mockedInfo + console.warn = mockedWarn + console.error = mockedError + // Create GQL Schema + schema = new GraphQLSchema({ + query: createQuerySchema(), + mutation: createMutationSchema(), + }) + }) + afterEach(() => { + consoleOutput.length = 0 + }) + describe('given a successful bulk domain creation', () => { + beforeAll(async () => { + ;({ query, drop, truncate, collections, transaction } = await ensure({ + variables: { + dbname: dbNameFromFile(__filename), + username: 'root', + rootPassword: rootPass, + password: rootPass, + url, + }, + + schema: dbschema, + })) + }) + beforeEach(async () => { + user = await collections.users.save({ + userName: 'test.account@istio.actually.exists', + emailValidated: true, + tfaSendMethod: 'email', + }) + org = await collections.organizations.save({ + orgDetails: { + en: { + slug: 'treasury-board-secretariat', + acronym: 'TBS', + name: 'Treasury Board of Canada Secretariat', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'secretariat-conseil-tresor', + acronym: 'SCT', + name: 'Secrétariat du Conseil Trésor du Canada', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, + }) + }) + afterEach(async () => { + await truncate() + }) + afterAll(async () => { + await drop() + }) + + describe('user has super admin permission level', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + beforeEach(async () => { + await collections.affiliations.save({ + _from: org._id, + _to: user._id, + permission: 'super_admin', + }) + }) + it('creates domains', async () => { + const response = await graphql( + schema, + ` + mutation { + addOrganizationsDomains( + input: { + orgId: "${toGlobalId('organizations', org._key)}" + domains: ["test.domain.gov", "test.domain2.gov"] + hideNewDomains: false + tagNewDomains: false + audit: false + } + ) { + result { + ... on DomainBulkResult { + status + } + ... on DomainError { + code + description + } + } + } + } + `, + null, + { + request: { + language: 'en', + }, + i18n, + query, + collections: collectionNames, + transaction, + userKey: user._key, + auth: { + checkPermission: checkPermission({ userKey: user._key, query }), + saltedHash: saltedHash(HASHING_SECRET), + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query }), + }), + verifiedRequired: verifiedRequired({}), + tfaRequired: tfaRequired({}), + }, + loaders: { + loadDomainByDomain: loadDomainByDomain({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + }, + validators: { cleanseInput }, + }, + ) + const expectedResponse = { + data: { + addOrganizationsDomains: { + result: { + status: `Successfully added 2 domain(s) to treasury-board-secretariat.`, + }, + }, + }, + } + expect(response).toEqual(expectedResponse) + + expect(consoleOutput).toEqual([ + `User: ${user._key} successfully added 2 domain(s) to org: treasury-board-secretariat.`, + ]) + }) + describe('audit flag is true', () => { + it('creates additional logs for each domain added', async () => { + const response = await graphql( + schema, + ` + mutation { + addOrganizationsDomains( + input: { + orgId: "${toGlobalId('organizations', org._key)}" + domains: ["test.domain.gov", "test.domain2.gov"] + hideNewDomains: false + tagNewDomains: false + audit: true + } + ) { + result { + ... on DomainBulkResult { + status + } + ... on DomainError { + code + description + } + } + } + } + `, + null, + { + request: { + language: 'en', + }, + i18n, + query, + collections: collectionNames, + transaction, + userKey: user._key, + auth: { + checkPermission: checkPermission({ userKey: user._key, query }), + saltedHash: saltedHash(HASHING_SECRET), + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query }), + }), + verifiedRequired: verifiedRequired({}), + tfaRequired: tfaRequired({}), + }, + loaders: { + loadDomainByDomain: loadDomainByDomain({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + }, + validators: { cleanseInput }, + }, + ) + const expectedResponse = { + data: { + addOrganizationsDomains: { + result: { + status: `Successfully added 2 domain(s) to treasury-board-secretariat.`, + }, + }, + }, + } + expect(response).toEqual(expectedResponse) + + expect(consoleOutput).toEqual([ + `User: ${user._key} successfully added domain: test.domain.gov to org: treasury-board-secretariat.`, + `User: ${user._key} successfully added domain: test.domain2.gov to org: treasury-board-secretariat.`, + ]) + }) + }) + }) + }) + + describe('given an unsuccessful bulk domain creation', () => { + beforeAll(async () => { + ;({ query, drop, truncate, collections, transaction } = await ensure({ + variables: { + dbname: dbNameFromFile(__filename), + username: 'root', + rootPassword: rootPass, + password: rootPass, + url, + }, + + schema: dbschema, + })) + }) + beforeEach(async () => { + user = await collections.users.save({ + userName: 'test.account@istio.actually.exists', + emailValidated: true, + tfaSendMethod: 'email', + }) + org = await collections.organizations.save({ + orgDetails: { + en: { + slug: 'treasury-board-secretariat', + acronym: 'TBS', + name: 'Treasury Board of Canada Secretariat', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'secretariat-conseil-tresor', + acronym: 'SCT', + name: 'Secrétariat du Conseil Trésor du Canada', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, + }) + }) + afterEach(async () => { + await truncate() + }) + afterAll(async () => { + await drop() + }) + describe('user does not have permission to add domains', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + beforeEach(async () => { + await collections.affiliations.save({ + _from: org._id, + _to: user._id, + permission: 'user', + }) + }) + it('returns an error message', async () => { + const response = await graphql( + schema, + ` + mutation { + addOrganizationsDomains( + input: { + orgId: "${toGlobalId('organizations', org._key)}" + domains: ["test.domain.gov", "test.domain2.gov"] + hideNewDomains: false + tagNewDomains: false + audit: false + } + ) { + result { + ... on DomainBulkResult { + status + } + ... on DomainError { + code + description + } + } + } + } + `, + null, + { + request: { + language: 'en', + }, + i18n, + query, + collections: collectionNames, + transaction, + userKey: user._key, + auth: { + checkPermission: checkPermission({ userKey: user._key, query }), + saltedHash: saltedHash(HASHING_SECRET), + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query }), + }), + verifiedRequired: verifiedRequired({}), + tfaRequired: tfaRequired({}), + }, + loaders: { + loadDomainByDomain: loadDomainByDomain({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + }, + validators: { cleanseInput }, + }, + ) + const expectedResponse = { + data: { + addOrganizationsDomains: { + result: { + code: 400, + description: `Permission Denied: Please contact organization user for help with creating domains.`, + }, + }, + }, + } + expect(response).toEqual(expectedResponse) + }) + }) + }) +}) diff --git a/api/src/domain/mutations/__tests__/remove-organizations-domains.test.js b/api/src/domain/mutations/__tests__/remove-organizations-domains.test.js new file mode 100644 index 0000000000..4c12f334aa --- /dev/null +++ b/api/src/domain/mutations/__tests__/remove-organizations-domains.test.js @@ -0,0 +1,684 @@ +import { ensure, dbNameFromFile } from 'arango-tools' +import { graphql, GraphQLSchema } from 'graphql' +import { toGlobalId } from 'graphql-relay' +import { setupI18n } from '@lingui/core' +import englishMessages from '../../../locale/en/messages' +import frenchMessages from '../../../locale/fr/messages' + +import { createQuerySchema } from '../../../query' +import { createMutationSchema } from '../../../mutation' +import { cleanseInput } from '../../../validators' +import { + checkPermission, + userRequired, + verifiedRequired, + tfaRequired, +} from '../../../auth' +import { loadDomainByDomain } from '../../loaders' +import { loadOrgByKey } from '../../../organization/loaders' +import { loadUserByKey } from '../../../user/loaders' +import dbschema from '../../../../database.json' +import { collectionNames } from '../../../collection-names' + +const { DB_PASS: rootPass, DB_URL: url } = process.env + +describe('given the addOrganizationsDomains mutation', () => { + let query, + drop, + i18n, + truncate, + schema, + collections, + transaction, + user, + org, + domain, + domain2, + org2 + + const consoleOutput = [] + const mockedInfo = (output) => consoleOutput.push(output) + const mockedWarn = (output) => consoleOutput.push(output) + const mockedError = (output) => consoleOutput.push(output) + beforeAll(async () => { + console.info = mockedInfo + console.warn = mockedWarn + console.error = mockedError + // Create GQL Schema + schema = new GraphQLSchema({ + query: createQuerySchema(), + mutation: createMutationSchema(), + }) + }) + afterEach(() => { + consoleOutput.length = 0 + }) + describe('given a successful bulk domain removal', () => { + beforeAll(async () => { + ;({ query, drop, truncate, collections, transaction } = await ensure({ + variables: { + dbname: dbNameFromFile(__filename), + username: 'root', + rootPassword: rootPass, + password: rootPass, + url, + }, + + schema: dbschema, + })) + }) + beforeEach(async () => { + user = await collections.users.save({ + userName: 'test.account@istio.actually.exists', + emailValidated: true, + tfaSendMethod: 'email', + }) + org = await collections.organizations.save({ + orgDetails: { + en: { + slug: 'treasury-board-secretariat', + acronym: 'TBS', + name: 'Treasury Board of Canada Secretariat', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'secretariat-conseil-tresor', + acronym: 'SCT', + name: 'Secrétariat du Conseil Trésor du Canada', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, + }) + org2 = await collections.organizations.save({ + orgDetails: { + en: { + slug: 'test-org', + acronym: 'TO', + name: 'Test Org', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'secretariat-conseil-tresor', + acronym: 'SCT', + name: 'Secrétariat du Conseil Trésor du Canada', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, + }) + }) + afterEach(async () => { + await truncate() + }) + afterAll(async () => { + await drop() + }) + + describe('user has super admin permission level', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + beforeEach(async () => { + domain = await collections.domains.save({ + domain: 'test.gc.ca', + slug: 'test-gc-ca', + archived: false, + }) + await collections.claims.save({ + _from: org._id, + _to: domain._id, + }) + const dkim = await collections.dkim.save({ dkim: true }) + await collections.domainsDKIM.save({ + _from: domain._id, + _to: dkim._id, + }) + const dkimResult = await collections.dkimResults.save({ + dkimResult: true, + }) + await collections.dkimToDkimResults.save({ + _from: dkim._id, + _to: dkimResult._id, + }) + const dmarc = await collections.dmarc.save({ dmarc: true }) + await collections.domainsDMARC.save({ + _from: domain._id, + _to: dmarc._id, + }) + const spf = await collections.spf.save({ spf: true }) + await collections.domainsSPF.save({ + _from: domain._id, + _to: spf._id, + }) + const https = await collections.https.save({ https: true }) + await collections.domainsHTTPS.save({ + _from: domain._id, + _to: https._id, + }) + const ssl = await collections.ssl.save({ ssl: true }) + await collections.domainsSSL.save({ + _from: domain._id, + _to: ssl._id, + }) + const dmarcSummary = await collections.dmarcSummaries.save({ + dmarcSummary: true, + }) + await collections.domainsToDmarcSummaries.save({ + _from: domain._id, + _to: dmarcSummary._id, + }) + + domain2 = await collections.domains.save({ + domain: 'test2.gc.ca', + slug: 'test2-gc-ca', + archived: false, + }) + await collections.claims.save({ + _from: org._id, + _to: domain2._id, + }) + await collections.claims.save({ + _from: org2._id, + _to: domain2._id, + }) + + await collections.affiliations.save({ + _from: org._id, + _to: user._id, + permission: 'super_admin', + }) + }) + it('removes domains', async () => { + const response = await graphql( + schema, + ` + mutation { + removeOrganizationsDomains( + input: { + orgId: "${toGlobalId('organization', org._key)}" + domains: ["test.gc.ca", "test2.gc.ca"] + archiveDomains: false + audit: false + } + ) { + result { + ... on DomainBulkResult { + status + } + ... on DomainError { + code + description + } + } + } + } + `, + null, + { + request: { + language: 'en', + }, + i18n, + query, + collections: collectionNames, + transaction, + userKey: user._key, + publish: jest.fn(), + auth: { + checkPermission: checkPermission({ userKey: user._key, query }), + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query }), + }), + verifiedRequired: verifiedRequired({}), + tfaRequired: tfaRequired({}), + }, + loaders: { + loadDomainByDomain: loadDomainByDomain({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + }, + validators: { cleanseInput }, + }, + ) + const expectedResponse = { + data: { + removeOrganizationsDomains: { + result: { + status: `Successfully removed 2 domain(s) from treasury-board-secretariat.`, + }, + }, + }, + } + expect(response).toEqual(expectedResponse) + + expect(consoleOutput).toEqual([ + `User: ${user._key} successfully removed 2 domain(s) from org: treasury-board-secretariat.`, + ]) + }) + it(`"audit" flag is true`, async () => { + const response = await graphql( + schema, + ` + mutation { + removeOrganizationsDomains( + input: { + orgId: "${toGlobalId('organization', org._key)}" + domains: ["test.gc.ca", "test2.gc.ca"] + archiveDomains: false + audit: true + } + ) { + result { + ... on DomainBulkResult { + status + } + ... on DomainError { + code + description + } + } + } + } + `, + null, + { + request: { + language: 'en', + }, + i18n, + query, + collections: collectionNames, + transaction, + userKey: user._key, + publish: jest.fn(), + auth: { + checkPermission: checkPermission({ userKey: user._key, query }), + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query }), + }), + verifiedRequired: verifiedRequired({}), + tfaRequired: tfaRequired({}), + }, + loaders: { + loadDomainByDomain: loadDomainByDomain({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + }, + validators: { cleanseInput }, + }, + ) + const expectedResponse = { + data: { + removeOrganizationsDomains: { + result: { + status: `Successfully removed 2 domain(s) from treasury-board-secretariat.`, + }, + }, + }, + } + expect(response).toEqual(expectedResponse) + + expect(consoleOutput).toEqual([ + `User: ${user._key} successfully removed domain: test.gc.ca from org: treasury-board-secretariat.`, + `User: ${user._key} successfully removed domain: test2.gc.ca from org: treasury-board-secretariat.`, + ]) + }) + it(`"archive" flag is true`, async () => { + const response = await graphql( + schema, + ` + mutation { + removeOrganizationsDomains( + input: { + orgId: "${toGlobalId('organization', org._key)}" + domains: ["test.gc.ca", "test2.gc.ca"] + archiveDomains: true + audit: false + } + ) { + result { + ... on DomainBulkResult { + status + } + ... on DomainError { + code + description + } + } + } + } + `, + null, + { + request: { + language: 'en', + }, + i18n, + query, + collections: collectionNames, + transaction, + userKey: user._key, + publish: jest.fn(), + auth: { + checkPermission: checkPermission({ userKey: user._key, query }), + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query }), + }), + verifiedRequired: verifiedRequired({}), + tfaRequired: tfaRequired({}), + }, + loaders: { + loadDomainByDomain: loadDomainByDomain({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + }, + validators: { cleanseInput }, + }, + ) + const expectedResponse = { + data: { + removeOrganizationsDomains: { + result: { + status: `Successfully removed 2 domain(s) from treasury-board-secretariat.`, + }, + }, + }, + } + expect(response).toEqual(expectedResponse) + + expect(consoleOutput).toEqual([ + `User: ${user._key} successfully removed 2 domain(s) from org: treasury-board-secretariat.`, + ]) + }) + }) + }) + + describe('given an unsuccessful bulk domain creation', () => { + beforeAll(async () => { + ;({ query, drop, truncate, collections, transaction } = await ensure({ + variables: { + dbname: dbNameFromFile(__filename), + username: 'root', + rootPassword: rootPass, + password: rootPass, + url, + }, + + schema: dbschema, + })) + }) + beforeEach(async () => { + user = await collections.users.save({ + userName: 'test.account@istio.actually.exists', + emailValidated: true, + tfaSendMethod: 'email', + }) + org = await collections.organizations.save({ + verified: true, + orgDetails: { + en: { + slug: 'treasury-board-secretariat', + acronym: 'TBS', + name: 'Treasury Board of Canada Secretariat', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'secretariat-conseil-tresor', + acronym: 'SCT', + name: 'Secrétariat du Conseil Trésor du Canada', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, + }) + org2 = await collections.organizations.save({ + verified: false, + orgDetails: { + en: { + slug: 'test-org', + acronym: 'TO', + name: 'Test Org', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + fr: { + slug: 'secretariat-conseil-tresor', + acronym: 'SCT', + name: 'Secrétariat du Conseil Trésor du Canada', + zone: 'FED', + sector: 'TBS', + country: 'Canada', + province: 'Ontario', + city: 'Ottawa', + }, + }, + }) + }) + afterEach(async () => { + await truncate() + }) + afterAll(async () => { + await drop() + }) + describe('user has admin permission level', () => { + beforeAll(() => { + i18n = setupI18n({ + locale: 'en', + localeData: { + en: { plurals: {} }, + fr: { plurals: {} }, + }, + locales: ['en', 'fr'], + messages: { + en: englishMessages.messages, + fr: frenchMessages.messages, + }, + }) + }) + beforeEach(async () => { + domain = await collections.domains.save({ + domain: 'test.gc.ca', + slug: 'test-gc-ca', + archived: false, + }) + await collections.claims.save({ + _from: org._id, + _to: domain._id, + }) + + domain2 = await collections.domains.save({ + domain: 'test2.gc.ca', + slug: 'test2-gc-ca', + archived: false, + }) + await collections.claims.save({ + _from: org._id, + _to: domain2._id, + }) + await collections.claims.save({ + _from: org2._id, + _to: domain2._id, + }) + + await collections.affiliations.save({ + _from: org._id, + _to: user._id, + permission: 'admin', + }) + await collections.affiliations.save({ + _from: org2._id, + _to: user._id, + permission: 'admin', + }) + }) + describe('org is verified', () => { + it('returns error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeOrganizationsDomains( + input: { + orgId: "${toGlobalId('organization', org._key)}" + domains: ["test.gc.ca", "test2.gc.ca"] + archiveDomains: false + audit: false + } + ) { + result { + ... on DomainBulkResult { + status + } + ... on DomainError { + code + description + } + } + } + } + `, + null, + { + request: { + language: 'en', + }, + i18n, + query, + collections: collectionNames, + transaction, + userKey: user._key, + publish: jest.fn(), + auth: { + checkPermission: checkPermission({ userKey: user._key, query }), + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query }), + }), + verifiedRequired: verifiedRequired({}), + tfaRequired: tfaRequired({}), + }, + loaders: { + loadDomainByDomain: loadDomainByDomain({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + }, + validators: { cleanseInput }, + }, + ) + const expectedResponse = { + data: { + removeOrganizationsDomains: { + result: { + code: 403, + description: `Permission Denied: Please contact super admin for help with removing domain.`, + }, + }, + }, + } + expect(response).toEqual(expectedResponse) + }) + }) + describe('archive flag is true', () => { + it('returns error', async () => { + const response = await graphql( + schema, + ` + mutation { + removeOrganizationsDomains( + input: { + orgId: "${toGlobalId('organization', org2._key)}" + domains: ["test2.gc.ca"] + archiveDomains: true + audit: false + } + ) { + result { + ... on DomainBulkResult { + status + } + ... on DomainError { + code + description + } + } + } + } + `, + null, + { + request: { + language: 'en', + }, + i18n, + query, + collections: collectionNames, + transaction, + userKey: user._key, + publish: jest.fn(), + auth: { + checkPermission: checkPermission({ userKey: user._key, query }), + userRequired: userRequired({ + userKey: user._key, + loadUserByKey: loadUserByKey({ query }), + }), + verifiedRequired: verifiedRequired({}), + tfaRequired: tfaRequired({}), + }, + loaders: { + loadDomainByDomain: loadDomainByDomain({ query }), + loadOrgByKey: loadOrgByKey({ query, language: 'en' }), + }, + validators: { cleanseInput }, + }, + ) + const expectedResponse = { + data: { + removeOrganizationsDomains: { + result: { + code: 403, + description: `Permission Denied: Please contact organization admin for help with archiving domains.`, + }, + }, + }, + } + expect(response).toEqual(expectedResponse) + }) + }) + }) + }) +}) diff --git a/api/src/domain/mutations/add-organizations-domains.js b/api/src/domain/mutations/add-organizations-domains.js new file mode 100644 index 0000000000..f58f5ef6c6 --- /dev/null +++ b/api/src/domain/mutations/add-organizations-domains.js @@ -0,0 +1,357 @@ +import { GraphQLNonNull, GraphQLList, GraphQLID, GraphQLBoolean } from 'graphql' +import { mutationWithClientMutationId, fromGlobalId } from 'graphql-relay' +import { t } from '@lingui/macro' + +import { bulkModifyDomainsUnion } from '../unions' +import { Domain } from '../../scalars' +import { logActivity } from '../../audit-logs/mutations/log-activity' + +export const addOrganizationsDomains = new mutationWithClientMutationId({ + name: 'AddOrganizationsDomains', + description: + 'Mutation used to create multiple new domains for an organization.', + inputFields: () => ({ + orgId: { + type: GraphQLNonNull(GraphQLID), + description: + 'The global id of the organization you wish to assign this domain to.', + }, + domains: { + type: GraphQLNonNull(new GraphQLList(Domain)), + description: 'Url that you would like to be added to the database.', + }, + hideNewDomains: { + type: GraphQLBoolean, + description: 'New domains will be hidden.', + }, + tagNewDomains: { + type: GraphQLBoolean, + description: 'New domains will be tagged with NEW.', + }, + audit: { + type: GraphQLBoolean, + description: 'Audit logs will be created.', + }, + }), + outputFields: () => ({ + result: { + type: bulkModifyDomainsUnion, + description: + '`BulkModifyDomainsUnion` returning either a `DomainBulkResult`, or `DomainErrorType` object.', + resolve: (payload) => payload, + }, + }), + mutateAndGetPayload: async ( + args, + { + i18n, + request, + query, + collections, + transaction, + userKey, + auth: { + checkPermission, + saltedHash, + userRequired, + verifiedRequired, + tfaRequired, + }, + loaders: { loadDomainByDomain, loadOrgByKey }, + validators: { cleanseInput }, + }, + ) => { + // Get User + const user = await userRequired() + + verifiedRequired({ user }) + tfaRequired({ user }) + + // Cleanse input + const { type: _orgType, id: orgId } = fromGlobalId(cleanseInput(args.orgId)) + + let domains + if (typeof args.domains !== 'undefined') { + domains = args.domains.map((domain) => cleanseInput(domain)) + } else { + domains = [] + } + + let hideNewDomains + if (typeof args.hideNewDomains !== 'undefined') { + hideNewDomains = args.hideNewDomains + } else { + hideNewDomains = false + } + + let tagNewDomains + if (typeof args.tagNewDomains !== 'undefined') { + tagNewDomains = args.tagNewDomains + } else { + tagNewDomains = false + } + let tags = [] + if (tagNewDomains) { + tags = [{ en: 'NEW', fr: 'NOUVEAU' }] + } + + let audit + if (typeof args.audit !== 'undefined') { + audit = args.audit + } else { + audit = false + } + + // Check to see if org exists + const org = await loadOrgByKey.load(orgId) + + if (typeof org === 'undefined') { + console.warn( + `User: ${userKey} attempted to add domains to an organization: ${orgId} that does not exist.`, + ) + return { + _type: 'error', + code: 400, + description: i18n._(t`Unable to add domains in unknown organization.`), + } + } + + // Check to see if user belongs to org + const permission = await checkPermission({ orgId: org._id }) + + if (permission !== 'super_admin') { + console.warn( + `User: ${userKey} attempted to add domains in: ${org.slug}, however they do not have permission to do so.`, + ) + return { + _type: 'error', + code: 400, + description: i18n._( + t`Permission Denied: Please contact organization user for help with creating domains.`, + ), + } + } + + const updatedProperties = [] + if (typeof tags !== 'undefined' && tags.length > 0) { + updatedProperties.push({ + name: 'tags', + oldValue: [], + newValue: tags, + }) + } + if (hideNewDomains) { + updatedProperties.push({ + name: 'hidden', + oldValue: null, + newValue: hideNewDomains, + }) + } + + let domainCount = 0 + + for (const domain of domains) { + const insertDomain = { + domain: domain.toLowerCase(), + lastRan: null, + selectors: [], + hash: saltedHash(domain.toLowerCase()), + status: { + dkim: null, + dmarc: null, + https: null, + spf: null, + ssl: null, + }, + archived: false, + } + + // Check to see if domain already belongs to same org + let checkDomainCursor + try { + checkDomainCursor = await query` + WITH claims, domains, organizations + LET domainIds = (FOR domain IN domains FILTER domain.domain == ${insertDomain.domain} RETURN { id: domain._id }) + FOR domainId IN domainIds + LET domainEdges = (FOR v, e IN 1..1 ANY domainId.id claims RETURN { _from: e._from }) + FOR domainEdge IN domainEdges + LET org = DOCUMENT(domainEdge._from) + FILTER org._key == ${org._key} + RETURN MERGE({ _id: org._id, _key: org._key, _rev: org._rev }, TRANSLATE(${request.language}, org.orgDetails)) + ` + } catch (err) { + console.error( + `Database error occurred while running check to see if domain already exists in an org: ${err}`, + ) + continue + } + + let checkOrgDomain + try { + checkOrgDomain = await checkDomainCursor.next() + } catch (err) { + console.error( + `Cursor error occurred while running check to see if domain already exists in an org: ${err}`, + ) + continue + } + + if (typeof checkOrgDomain !== 'undefined') { + console.warn( + `User: ${userKey} attempted to create a domain for: ${org.slug}, however that org already has that domain claimed.`, + ) + continue + } + + // Check to see if domain already exists in db + const checkDomain = await loadDomainByDomain.load(insertDomain.domain) + + // Setup Transaction + const trx = await transaction(collections) + + let insertedDomainCursor + if (typeof checkDomain === 'undefined') { + try { + insertedDomainCursor = await trx.step( + () => + query` + WITH domains + INSERT ${insertDomain} INTO domains + RETURN MERGE( + { + id: NEW._key, + _type: "domain" + }, + NEW + ) + `, + ) + } catch (err) { + console.error( + `Transaction step error occurred for user: ${userKey} when inserting new domain: ${err}`, + ) + continue + } + + let insertedDomain + try { + insertedDomain = await insertedDomainCursor.next() + } catch (err) { + console.error( + `Cursor error occurred for user: ${userKey} after inserting new domain and gathering its domain info: ${err}`, + ) + continue + } + + try { + await trx.step( + () => + query` + WITH claims + INSERT { + _from: ${org._id}, + _to: ${insertedDomain._id}, + tags: ${tags}, + hidden: ${hideNewDomains} + } INTO claims + `, + ) + } catch (err) { + console.error( + `Transaction step error occurred for user: ${userKey} when inserting new domain edge: ${err}`, + ) + continue + } + } else { + try { + await trx.step( + () => + query` + WITH claims + INSERT { + _from: ${org._id}, + _to: ${checkDomain._id}, + tags: ${tags}, + hidden: ${hideNewDomains} + } INTO claims + `, + ) + } catch (err) { + console.error( + `Transaction step error occurred for user: ${userKey} when inserting domain edge: ${err}`, + ) + continue + } + } + + try { + await trx.commit() + } catch (err) { + console.error( + `Transaction commit error occurred while user: ${userKey} was creating domains: ${err}`, + ) + throw new Error(i18n._(t`Unable to create domains. Please try again.`)) + } + + if (audit) { + console.info( + `User: ${userKey} successfully added domain: ${insertDomain.domain} to org: ${org.slug}.`, + ) + await logActivity({ + transaction, + collections, + query, + initiatedBy: { + id: user._key, + userName: user.userName, + role: permission, + }, + action: 'add', + target: { + resource: insertDomain.domain, + updatedProperties, + organization: { + id: org._key, + name: org.name, + }, + resourceType: 'domain', + }, + }) + } + domainCount += 1 + } + + if (!audit) { + console.info( + `User: ${userKey} successfully added ${domainCount} domain(s) to org: ${org.slug}.`, + ) + await logActivity({ + transaction, + collections, + query, + initiatedBy: { + id: user._key, + userName: user.userName, + role: permission, + }, + action: 'add', + target: { + resource: `${domainCount} domains`, + updatedProperties, + organization: { + id: org._key, + name: org.name, + }, + resourceType: 'domain', + }, + }) + } + + return { + _type: 'result', + status: i18n._( + t`Successfully added ${domainCount} domain(s) to ${org.slug}.`, + ), + } + }, +}) diff --git a/api/src/domain/mutations/index.js b/api/src/domain/mutations/index.js index 8ca2adeb59..ea4a263f01 100644 --- a/api/src/domain/mutations/index.js +++ b/api/src/domain/mutations/index.js @@ -1,6 +1,8 @@ +export * from './add-organizations-domains' export * from './create-domain' export * from './favourite-domain' export * from './remove-domain' +export * from './remove-organizations-domains' export * from './request-scan' export * from './unfavourite-domain' export * from './update-domain' diff --git a/api/src/domain/mutations/remove-organizations-domains.js b/api/src/domain/mutations/remove-organizations-domains.js new file mode 100644 index 0000000000..9c396e2690 --- /dev/null +++ b/api/src/domain/mutations/remove-organizations-domains.js @@ -0,0 +1,532 @@ +import { GraphQLNonNull, GraphQLID, GraphQLBoolean, GraphQLList } from 'graphql' +import { mutationWithClientMutationId, fromGlobalId } from 'graphql-relay' +import { t } from '@lingui/macro' + +import { bulkModifyDomainsUnion } from '../unions' +import { logActivity } from '../../audit-logs/mutations/log-activity' +import { Domain } from '../../scalars' + +export const removeOrganizationsDomains = new mutationWithClientMutationId({ + name: 'RemoveOrganizationsDomains', + description: 'This mutation allows the removal of unused domains.', + inputFields: () => ({ + domains: { + type: GraphQLNonNull(new GraphQLList(Domain)), + description: 'Domains you wish to remove from the organization.', + }, + orgId: { + type: GraphQLNonNull(GraphQLID), + description: 'The organization you wish to remove the domain from.', + }, + archiveDomains: { + type: GraphQLBoolean, + description: 'Domains will be archived.', + }, + audit: { + type: GraphQLBoolean, + description: 'Audit logs will be created.', + }, + }), + outputFields: () => ({ + result: { + type: GraphQLNonNull(bulkModifyDomainsUnion), + description: + '`BulkModifyDomainsUnion` returning either a `DomainBulkResult`, or `DomainErrorType` object.', + resolve: (payload) => payload, + }, + }), + mutateAndGetPayload: async ( + args, + { + i18n, + query, + collections, + transaction, + userKey, + auth: { checkPermission, userRequired, verifiedRequired, tfaRequired }, + validators: { cleanseInput }, + loaders: { loadDomainByDomain, loadOrgByKey }, + }, + ) => { + // Get User + const user = await userRequired() + + verifiedRequired({ user }) + tfaRequired({ user }) + + // Cleanse Input + let domains + if (typeof args.domains !== 'undefined') { + domains = args.domains.map((domain) => cleanseInput(domain)) + } else { + domains = [] + } + const { type: _orgType, id: orgId } = fromGlobalId(cleanseInput(args.orgId)) + + let audit + if (typeof args.audit !== 'undefined') { + audit = args.audit + } else { + audit = false + } + + let archiveDomains + if (typeof args.archiveDomains !== 'undefined') { + archiveDomains = args.archiveDomains + } else { + archiveDomains = false + } + + // Get Org from db + const org = await loadOrgByKey.load(orgId) + + // Check to see if org exists + if (typeof org === 'undefined') { + console.warn( + `User: ${userKey} attempted to remove domains in org: ${orgId} however there is no organization associated with that id.`, + ) + return { + _type: 'error', + code: 400, + description: i18n._( + t`Unable to remove domains from unknown organization.`, + ), + } + } + + // Get permission + const permission = await checkPermission({ orgId: org._id }) + + // Check to see if domain belongs to verified check org + if (org.verified && permission !== 'super_admin') { + console.warn( + `User: ${userKey} attempted to remove domains in ${org.slug} but does not have permission to remove a domain from a verified check org.`, + ) + return { + _type: 'error', + code: 403, + description: i18n._( + t`Permission Denied: Please contact super admin for help with removing domain.`, + ), + } + } + + if (permission !== 'super_admin' && permission !== 'admin') { + console.warn( + `User: ${userKey} attempted to remove domains in ${org.slug} however they do not have permission in that org.`, + ) + return { + _type: 'error', + code: 403, + description: i18n._( + t`Permission Denied: Please contact organization admin for help with removing domains.`, + ), + } + } + + if (archiveDomains && permission !== 'super_admin') { + console.warn( + `User: ${userKey} attempted to archive domains in ${org.slug} however they do not have permission in that org.`, + ) + return { + _type: 'error', + code: 403, + description: i18n._( + t`Permission Denied: Please contact organization admin for help with archiving domains.`, + ), + } + } + + let domainCount = 0 + + for (const domain of domains) { + // Setup Transaction + const trx = await transaction(collections) + + // Get domain from db + const checkDomain = await loadDomainByDomain.load(domain) + + // Check to see if domain exists + if (typeof checkDomain === 'undefined') { + console.warn( + `User: ${userKey} attempted to remove ${domain} however no domain is associated with that id.`, + ) + continue + } + + if (archiveDomains && permission === 'super_admin') { + // Archive Domain + try { + await trx.step( + () => query` + UPDATE ${checkDomain} WITH { archived: true } IN domains + `, + ) + + if (audit) { + await logActivity({ + transaction, + collections, + query, + initiatedBy: { + id: user._key, + userName: user.userName, + role: permission, + }, + action: 'update', + target: { + resource: `${domains.length} domains`, + updatedProperties: [ + { + name: 'archived', + oldValue: checkDomain?.archived, + newValue: true, + }, + ], + organization: { + id: org._key, + name: org.name, + }, // name of resource being acted upon + resourceType: 'domain', // user, org, domain + }, + }) + } + } catch (err) { + console.error( + `Database error occurred for user: ${userKey} when attempting to archive domain: ${domain}, error: ${err}`, + ) + continue + } + } else { + // Check to see if more than one organization has a claim to this domain + let countCursor + try { + countCursor = await query` + WITH claims, domains, organizations + FOR v, e IN 1..1 ANY ${checkDomain._id} claims + RETURN v + ` + } catch (err) { + console.error( + `Database error occurred for user: ${userKey}, when counting domain claims for domain: ${checkDomain.domain}, error: ${err}`, + ) + continue + } + + // check if org has claim to domain + const orgsClaimingDomain = await countCursor.all() + const orgHasDomainClaim = orgsClaimingDomain.some((orgVertex) => { + return orgVertex._id === org._id + }) + + if (!orgHasDomainClaim) { + console.error( + `Error occurred for user: ${userKey}, when attempting to remove domain "${domain}" from organization with slug "${org.slug}". Organization does not have claim for domain.`, + ) + continue + } + + // check to see if org removing domain has ownership + let dmarcCountCursor + try { + dmarcCountCursor = await query` + WITH domains, organizations, ownership + FOR v IN 1..1 OUTBOUND ${org._id} ownership + FILTER v._id == ${checkDomain._id} + RETURN true + ` + } catch (err) { + console.error( + `Database error occurred for user: ${userKey}, when counting ownership claims for domain: ${checkDomain.domain}, error: ${err}`, + ) + continue + } + + if (dmarcCountCursor.count === 1) { + try { + await trx.step( + () => query` + WITH ownership, organizations, domains, dmarcSummaries, domainsToDmarcSummaries + LET dmarcSummaryEdges = ( + FOR v, e IN 1..1 OUTBOUND ${checkDomain._id} domainsToDmarcSummaries + RETURN { edgeKey: e._key, dmarcSummaryId: e._to } + ) + LET removeDmarcSummaryEdges = ( + FOR dmarcSummaryEdge IN dmarcSummaryEdges + REMOVE dmarcSummaryEdge.edgeKey IN domainsToDmarcSummaries + OPTIONS { waitForSync: true } + ) + LET removeDmarcSummary = ( + FOR dmarcSummaryEdge IN dmarcSummaryEdges + LET key = PARSE_IDENTIFIER(dmarcSummaryEdge.dmarcSummaryId).key + REMOVE key IN dmarcSummaries + OPTIONS { waitForSync: true } + ) + RETURN true + `, + ) + } catch (err) { + console.error( + `Trx step error occurred when removing dmarc summary data for user: ${userKey} while attempting to remove domain: ${checkDomain.domain}, error: ${err}`, + ) + continue + } + + try { + await trx.step( + () => query` + WITH ownership, organizations, domains + LET domainEdges = ( + FOR v, e IN 1..1 INBOUND ${checkDomain._id} ownership + REMOVE e._key IN ownership + OPTIONS { waitForSync: true } + ) + RETURN true + `, + ) + } catch (err) { + console.error( + `Trx step error occurred when removing ownership data for user: ${userKey} while attempting to remove domain: ${checkDomain.domain}, error: ${err}`, + ) + continue + } + } + + if (countCursor.count <= 1) { + // Remove scan data + + try { + // Remove DKIM data + await trx.step(async () => { + await query` + WITH dkim, dkimResults, domains + FOR dkimV, domainsDkimEdge IN 1..1 OUTBOUND ${checkDomain._id} domainsDKIM + FOR dkimResult, dkimToDkimResultsEdge In 1..1 OUTBOUND dkimV._id dkimToDkimResults + REMOVE dkimResult IN dkimResults + REMOVE dkimToDkimResultsEdge IN dkimToDkimResults + OPTIONS { waitForSync: true } + REMOVE dkimV IN dkim + REMOVE domainsDkimEdge IN domainsDKIM + OPTIONS { waitForSync: true } + ` + }) + } catch (err) { + console.error( + `Trx step error occurred while user: ${userKey} attempted to remove DKIM data for ${checkDomain.domain} in org: ${org.slug}, error: ${err}`, + ) + continue + } + + try { + // Remove DMARC data + await trx.step(async () => { + await query` + WITH dmarc, domains + FOR dmarcV, domainsDmarcEdge IN 1..1 OUTBOUND ${checkDomain._id} domainsDMARC + REMOVE dmarcV IN dmarc + REMOVE domainsDmarcEdge IN domainsDMARC + ` + }) + } catch (err) { + console.error( + `Trx step error occurred while user: ${userKey} attempted to remove DMARC data for ${checkDomain.domain} in org: ${org.slug}, error: ${err}`, + ) + continue + } + + try { + // Remove HTTPS data + await trx.step(async () => { + await query` + WITH https, domains + FOR httpsV, domainsHttpsEdge IN 1..1 OUTBOUND ${checkDomain._id} domainsHTTPS + REMOVE httpsV IN https + REMOVE domainsHttpsEdge IN domainsHTTPS + OPTIONS { waitForSync: true } + ` + }) + } catch (err) { + console.error( + `Trx step error occurred while user: ${userKey} attempted to remove HTTPS data for ${checkDomain.domain} in org: ${org.slug}, error: ${err}`, + ) + continue + } + + try { + // Remove SPF data + await trx.step(async () => { + await query` + WITH spf, domains + FOR spfV, domainsSpfEdge IN 1..1 OUTBOUND ${checkDomain._id} domainsSPF + REMOVE spfV IN spf + REMOVE domainsSpfEdge IN domainsSPF + OPTIONS { waitForSync: true } + ` + }) + } catch (err) { + console.error( + `Trx step error occurred while user: ${userKey} attempted to remove SPF data for ${checkDomain.domain} in org: ${org.slug}, error: ${err}`, + ) + continue + } + + try { + // Remove SSL data + await trx.step(async () => { + await query` + WITH ssl, domains + FOR sslV, domainsSslEdge IN 1..1 OUTBOUND ${checkDomain._id} domainsSSL + REMOVE sslV IN ssl + REMOVE domainsSslEdge IN domainsSSL + OPTIONS { waitForSync: true } + ` + }) + } catch (err) { + console.error( + `Trx step error occurred while user: ${userKey} attempted to remove SSL data for ${checkDomain.domain} in org: ${org.slug}, error: ${err}`, + ) + continue + } + + try { + // Remove domain + await trx.step(async () => { + await query` + FOR claim IN claims + FILTER claim._to == ${checkDomain._id} + REMOVE claim IN claims + REMOVE ${checkDomain} IN domains + ` + }) + } catch (err) { + console.error( + `Trx step error occurred while user: ${userKey} attempted to remove domain ${checkDomain.domain} in org: ${org.slug}, error: ${err}`, + ) + continue + } + } else { + try { + await trx.step(async () => { + await query` + WITH claims, domains, organizations + LET domainEdges = (FOR v, e IN 1..1 INBOUND ${checkDomain._id} claims RETURN { _key: e._key, _from: e._from, _to: e._to }) + LET edgeKeys = ( + FOR domainEdge IN domainEdges + FILTER domainEdge._to == ${checkDomain._id} + FILTER domainEdge._from == ${org._id} + RETURN domainEdge._key + ) + FOR edgeKey IN edgeKeys + REMOVE edgeKey IN claims + OPTIONS { waitForSync: true } + ` + }) + } catch (err) { + console.error( + `Trx step error occurred while user: ${userKey} attempted to remove claim for ${checkDomain.domain} in org: ${org.slug}, error: ${err}`, + ) + continue + } + } + + // Commit transaction + try { + await trx.commit() + } catch (err) { + console.error( + `Trx commit error occurred while user: ${userKey} attempted to remove domains in org: ${org.slug}, error: ${err}`, + ) + continue + } + + if (audit) { + console.info( + `User: ${userKey} successfully removed domain: ${domain} from org: ${org.slug}.`, + ) + await logActivity({ + transaction, + collections, + query, + initiatedBy: { + id: user._key, + userName: user.userName, + role: permission, + }, + action: 'remove', + target: { + resource: checkDomain.domain, + organization: { + id: org._key, + name: org.name, + }, + resourceType: 'domain', + }, + }) + } + } + domainCount += 1 + } + + // Log activity + if (!audit) { + console.info( + `User: ${userKey} successfully removed ${domainCount} domain(s) from org: ${org.slug}.`, + ) + if (archiveDomains) { + await logActivity({ + transaction, + collections, + query, + initiatedBy: { + id: user._key, + userName: user.userName, + role: permission, + }, + action: 'update', + target: { + resource: `${domainCount} domains`, + updatedProperties: [ + { + name: 'archived', + oldValue: false, + newValue: true, + }, + ], + organization: { + id: org._key, + name: org.name, + }, + resourceType: 'domain', + }, + }) + } else { + await logActivity({ + transaction, + collections, + query, + initiatedBy: { + id: user._key, + userName: user.userName, + role: permission, + }, + action: 'remove', + target: { + resource: `${domainCount} domains`, + organization: { + id: org._key, + name: org.name, + }, + resourceType: 'domain', + }, + }) + } + } + + return { + _type: 'result', + status: i18n._( + t`Successfully removed ${domainCount} domain(s) from ${org.slug}.`, + ), + } + }, +}) diff --git a/api/src/domain/objects/domain-bulk-result.js b/api/src/domain/objects/domain-bulk-result.js new file mode 100644 index 0000000000..618af9bb15 --- /dev/null +++ b/api/src/domain/objects/domain-bulk-result.js @@ -0,0 +1,14 @@ +import { GraphQLObjectType, GraphQLString } from 'graphql' + +export const domainBulkResultType = new GraphQLObjectType({ + name: 'DomainBulkResult', + description: + 'This object is used to inform the user that no errors were encountered while mutating a domain.', + fields: () => ({ + status: { + type: GraphQLString, + description: 'Informs the user if the domain mutation was successful.', + resolve: ({ status }) => status, + }, + }), +}) diff --git a/api/src/domain/objects/domain-result.js b/api/src/domain/objects/domain-result.js index 72eebb2e09..790efd21f2 100644 --- a/api/src/domain/objects/domain-result.js +++ b/api/src/domain/objects/domain-result.js @@ -5,11 +5,11 @@ import { domainType } from './domain' export const domainResultType = new GraphQLObjectType({ name: 'DomainResult', description: - 'This object is used to inform the user that no errors were encountered while removing a domain.', + 'This object is used to inform the user that no errors were encountered while mutating a domain.', fields: () => ({ status: { type: GraphQLString, - description: 'Informs the user if the domain removal was successful.', + description: 'Informs the user if the domain mutation was successful.', resolve: ({ status }) => status, }, domain: { diff --git a/api/src/domain/objects/index.js b/api/src/domain/objects/index.js index aeb566d5f0..16b7863931 100644 --- a/api/src/domain/objects/index.js +++ b/api/src/domain/objects/index.js @@ -4,3 +4,4 @@ export * from './domain-error' export * from './domain-result' export * from './domain-status' export * from './vulnerability-tag' +export * from './domain-bulk-result' diff --git a/api/src/domain/unions/bulk-modify-domains-union.js b/api/src/domain/unions/bulk-modify-domains-union.js new file mode 100644 index 0000000000..6014e42e30 --- /dev/null +++ b/api/src/domain/unions/bulk-modify-domains-union.js @@ -0,0 +1,17 @@ +import { GraphQLUnionType } from 'graphql' +import { domainErrorType, domainBulkResultType } from '../objects' + +export const bulkModifyDomainsUnion = new GraphQLUnionType({ + name: 'BulkModifyDomainsUnion', + description: `This union is used with the \`AddOrganizationsDomains\` and \`RemoveOrganizationsDomains\` mutation, + allowing for users to add/remove multiple domains belonging to their org, + and support any errors that may occur`, + types: [domainErrorType, domainBulkResultType], + resolveType({ _type }) { + if (_type === 'result') { + return domainBulkResultType + } else { + return domainErrorType + } + }, +}) diff --git a/api/src/domain/unions/index.js b/api/src/domain/unions/index.js index 79289f6b1d..d5400b7efc 100644 --- a/api/src/domain/unions/index.js +++ b/api/src/domain/unions/index.js @@ -1,3 +1,4 @@ +export * from './bulk-modify-domains-union' export * from './create-domain-union' export * from './remove-domain-union' export * from './update-domain-union' diff --git a/api/src/locale/en/messages.po b/api/src/locale/en/messages.po index 48318cd213..82f6e3e92c 100644 --- a/api/src/locale/en/messages.po +++ b/api/src/locale/en/messages.po @@ -19,7 +19,7 @@ msgstr "" msgid "Authentication error. Please sign in." msgstr "Authentication error. Please sign in." -#: src/organization/objects/organization.js:183 +#: src/organization/objects/organization.js:188 msgid "Cannot query affiliations on organization without admin permission or higher." msgstr "Cannot query affiliations on organization without admin permission or higher." @@ -86,10 +86,10 @@ msgstr "Organization name already in use, please choose another and try again." msgid "Organization name already in use. Please try again with a different name." msgstr "Organization name already in use. Please try again with a different name." -#: src/auth/check-domain-ownership.js:33 -#: src/auth/check-domain-ownership.js:45 -#: src/auth/check-domain-ownership.js:67 -#: src/auth/check-domain-ownership.js:78 +#: src/auth/check-domain-ownership.js:30 +#: src/auth/check-domain-ownership.js:42 +#: src/auth/check-domain-ownership.js:64 +#: src/auth/check-domain-ownership.js:75 msgid "Ownership check error. Unable to request domain information." msgstr "Ownership check error. Unable to request domain information." @@ -123,7 +123,7 @@ msgstr "Passing both `first` and `last` to paginate the `DmarcFailureTable` conn msgid "Passing both `first` and `last` to paginate the `DmarcSummaries` connection is not supported." msgstr "Passing both `first` and `last` to paginate the `DmarcSummaries` connection is not supported." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:182 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:163 #: src/domain/loaders/load-domain-connections-by-user-id.js:175 msgid "Passing both `first` and `last` to paginate the `Domain` connection is not supported." msgstr "Passing both `first` and `last` to paginate the `Domain` connection is not supported." @@ -203,7 +203,7 @@ msgstr "Passwords do not match." msgid "Permission Denied: Could not retrieve specified organization." msgstr "Permission Denied: Could not retrieve specified organization." -#: src/user/mutations/update-user-profile.js:114 +#: src/user/mutations/update-user-profile.js:115 msgid "Permission Denied: Multi-factor authentication is required for admin accounts" msgstr "Permission Denied: Multi-factor authentication is required for admin accounts" @@ -211,10 +211,18 @@ msgstr "Permission Denied: Multi-factor authentication is required for admin acc msgid "Permission Denied: Please contact org owner to transfer ownership." msgstr "Permission Denied: Please contact org owner to transfer ownership." +#: src/domain/mutations/remove-organizations-domains.js:135 +msgid "Permission Denied: Please contact organization admin for help with archiving domains." +msgstr "Permission Denied: Please contact organization admin for help with archiving domains." + #: src/domain/mutations/remove-domain.js:117 msgid "Permission Denied: Please contact organization admin for help with removing domain." msgstr "Permission Denied: Please contact organization admin for help with removing domain." +#: src/domain/mutations/remove-organizations-domains.js:122 +msgid "Permission Denied: Please contact organization admin for help with removing domains." +msgstr "Permission Denied: Please contact organization admin for help with removing domains." + #: src/organization/mutations/remove-organization.js:72 msgid "Permission Denied: Please contact organization admin for help with removing organization." msgstr "Permission Denied: Please contact organization admin for help with removing organization." @@ -241,6 +249,7 @@ msgstr "Permission Denied: Please contact organization admin for help with user msgid "Permission Denied: Please contact organization admin for help with user role changes." msgstr "Permission Denied: Please contact organization admin for help with user role changes." +#: src/domain/mutations/add-organizations-domains.js:131 #: src/domain/mutations/create-domain.js:140 msgid "Permission Denied: Please contact organization user for help with creating domain." msgstr "Permission Denied: Please contact organization user for help with creating domain." @@ -249,7 +258,7 @@ msgstr "Permission Denied: Please contact organization user for help with creati msgid "Permission Denied: Please contact organization user for help with retrieving this domain." msgstr "Permission Denied: Please contact organization user for help with retrieving this domain." -#: src/domain/mutations/request-scan.js:82 +#: src/domain/mutations/request-scan.js:71 msgid "Permission Denied: Please contact organization user for help with scanning this domain." msgstr "Permission Denied: Please contact organization user for help with scanning this domain." @@ -258,6 +267,7 @@ msgid "Permission Denied: Please contact organization user for help with updatin msgstr "Permission Denied: Please contact organization user for help with updating this domain." #: src/domain/mutations/remove-domain.js:104 +#: src/domain/mutations/remove-organizations-domains.js:109 msgid "Permission Denied: Please contact super admin for help with removing domain." msgstr "Permission Denied: Please contact super admin for help with removing domain." @@ -266,8 +276,8 @@ msgid "Permission Denied: Please contact super admin for help with removing orga msgstr "Permission Denied: Please contact super admin for help with removing organization." #: src/domain/mutations/request-scan.js:67 -msgid "Permission Denied: Please contact super admin for help with scanning this domain." -msgstr "Permission Denied: Please contact super admin for help with scanning this domain." +#~ msgid "Permission Denied: Please contact super admin for help with scanning this domain." +#~ msgstr "Permission Denied: Please contact super admin for help with scanning this domain." #: src/organization/mutations/verify-organization.js:69 msgid "Permission Denied: Please contact super admin for help with verifying this organization." @@ -291,7 +301,7 @@ msgid "Permission error: Unable to close other user's account." msgstr "Permission error: Unable to close other user's account." #: src/auth/super-admin-required.js:15 -#: src/organization/queries/get-all-organization-domain-statuses.js:35 +#: src/organization/queries/get-all-organization-domain-statuses.js:36 msgid "Permissions error. You do not have sufficient permissions to access this data." msgstr "Permissions error. You do not have sufficient permissions to access this data." @@ -303,7 +313,7 @@ msgstr "Phone number has been successfully removed." msgid "Phone number has been successfully set, you will receive a verification text message shortly." msgstr "Phone number has been successfully set, you will receive a verification text message shortly." -#: src/user/mutations/update-user-profile.js:197 +#: src/user/mutations/update-user-profile.js:198 msgid "Profile successfully updated." msgstr "Profile successfully updated." @@ -324,7 +334,7 @@ msgstr "Requesting `{amount}` records on the `DmarcFailureTable` connection exce msgid "Requesting `{amount}` records on the `DmarcSummaries` connection exceeds the `{argSet}` limit of 100 records." msgstr "Requesting `{amount}` records on the `DmarcSummaries` connection exceeds the `{argSet}` limit of 100 records." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:205 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:186 #: src/domain/loaders/load-domain-connections-by-user-id.js:198 msgid "Requesting `{amount}` records on the `Domain` connection exceeds the `{argSet}` limit of 100 records." msgstr "Requesting `{amount}` records on the `Domain` connection exceeds the `{argSet}` limit of 100 records." @@ -394,11 +404,15 @@ msgstr "Requesting {amount} records on the `SPF` connection exceeds the `{argSet msgid "Requesting {amount} records on the `SSL` connection exceeds the `{argSet}` limit of 100 records." msgstr "Requesting {amount} records on the `SSL` connection exceeds the `{argSet}` limit of 100 records." +#: src/domain/mutations/add-organizations-domains.js:351 +msgid "Successfully added {domainCount} domains to {0}." +msgstr "Successfully added {domainCount} domains to {0}." + #: src/user/mutations/close-account.js:521 msgid "Successfully closed account." msgstr "Successfully closed account." -#: src/domain/mutations/request-scan.js:105 +#: src/domain/mutations/request-scan.js:94 msgid "Successfully dispatched one time scan." msgstr "Successfully dispatched one time scan." @@ -414,11 +428,11 @@ msgstr "Successfully invited user to organization, and sent notification email." msgid "Successfully left organization: {0}" msgstr "Successfully left organization: {0}" -#: src/domain/mutations/unfavourite-domain.js:145 +#: src/domain/mutations/unfavourite-domain.js:146 msgid "Successfully removed domain: {0} from favourites." msgstr "Successfully removed domain: {0} from favourites." -#: src/domain/mutations/remove-domain.js:378 +#: src/domain/mutations/remove-domain.js:395 msgid "Successfully removed domain: {0} from {1}." msgstr "Successfully removed domain: {0} from {1}." @@ -430,6 +444,10 @@ msgstr "Successfully removed organization: {0}." msgid "Successfully removed user from organization." msgstr "Successfully removed user from organization." +#: src/domain/mutations/remove-organizations-domains.js:530 +msgid "Successfully removed {domainCount} domains from {0}." +msgstr "Successfully removed {domainCount} domains from {0}." + #: src/affiliation/mutations/invite-user-to-org.js:167 msgid "Successfully sent invitation to service, and organization email." msgstr "Successfully sent invitation to service, and organization email." @@ -517,6 +535,7 @@ msgstr "Unable to close account of an undefined user." msgid "Unable to close account. Please try again." msgstr "Unable to close account. Please try again." +#: src/domain/mutations/add-organizations-domains.js:115 #: src/domain/mutations/create-domain.js:120 msgid "Unable to create domain in unknown organization." msgstr "Unable to create domain in unknown organization." @@ -536,6 +555,10 @@ msgstr "Unable to create domain, organization has already claimed it." msgid "Unable to create domain. Please try again." msgstr "Unable to create domain. Please try again." +#: src/domain/mutations/add-organizations-domains.js:293 +msgid "Unable to create domains. Please try again." +msgstr "Unable to create domains. Please try again." + #: src/organization/mutations/create-organization.js:201 #: src/organization/mutations/create-organization.js:224 #: src/organization/mutations/create-organization.js:235 @@ -548,16 +571,16 @@ msgstr "Unable to create organization. Please try again." #~ msgid "Unable to dispatch one time scan. Please try again." #~ msgstr "Unable to dispatch one time scan. Please try again." -#: src/domain/mutations/favourite-domain.js:93 +#: src/domain/mutations/favourite-domain.js:94 msgid "Unable to favourite domain, user has already favourited it." msgstr "Unable to favourite domain, user has already favourited it." -#: src/domain/mutations/favourite-domain.js:72 -#: src/domain/mutations/favourite-domain.js:82 -#: src/domain/mutations/favourite-domain.js:116 -#: src/domain/mutations/favourite-domain.js:125 -#: src/domain/mutations/unfavourite-domain.js:72 -#: src/domain/mutations/unfavourite-domain.js:82 +#: src/domain/mutations/favourite-domain.js:73 +#: src/domain/mutations/favourite-domain.js:83 +#: src/domain/mutations/favourite-domain.js:117 +#: src/domain/mutations/favourite-domain.js:126 +#: src/domain/mutations/unfavourite-domain.js:73 +#: src/domain/mutations/unfavourite-domain.js:83 msgid "Unable to favourite domain. Please try again." msgstr "Unable to favourite domain. Please try again." @@ -767,15 +790,15 @@ msgstr "Unable to load affiliation information. Please try again." msgid "Unable to load affiliation(s). Please try again." msgstr "Unable to load affiliation(s). Please try again." -#: src/organization/loaders/load-all-organization-domain-statuses.js:38 +#: src/organization/loaders/load-all-organization-domain-statuses.js:36 msgid "Unable to load all organization domain statuses. Please try again." msgstr "Unable to load all organization domain statuses. Please try again." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:464 -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:474 -#: src/domain/loaders/load-domain-connections-by-user-id.js:469 -#: src/domain/loaders/load-domain-tags-by-org-id.js:26 -#: src/user/loaders/load-my-tracker-by-user-id.js:42 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:547 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:557 +#: src/domain/loaders/load-domain-connections-by-user-id.js:471 +#: src/domain/loaders/load-domain-tags-by-org-id.js:27 +#: src/user/loaders/load-my-tracker-by-user-id.js:43 msgid "Unable to load domain(s). Please try again." msgstr "Unable to load domain(s). Please try again." @@ -832,7 +855,7 @@ msgstr "Unable to load owner information. Please try again." msgid "Unable to load summary. Please try again." msgstr "Unable to load summary. Please try again." -#: src/domain/loaders/load-domain-tags-by-org-id.js:36 +#: src/domain/loaders/load-domain-tags-by-org-id.js:37 msgid "Unable to load tags(s). Please try again." msgstr "Unable to load tags(s). Please try again." @@ -871,8 +894,8 @@ msgstr "Unable to load web summary. Please try again." msgid "Unable to query affiliation(s). Please try again." msgstr "Unable to query affiliation(s). Please try again." -#: src/domain/loaders/load-domain-connections-by-user-id.js:459 -#: src/user/loaders/load-my-tracker-by-user-id.js:32 +#: src/domain/loaders/load-domain-connections-by-user-id.js:461 +#: src/user/loaders/load-my-tracker-by-user-id.js:33 msgid "Unable to query domain(s). Please try again." msgstr "Unable to query domain(s). Please try again." @@ -902,21 +925,29 @@ msgstr "Unable to remove a user that already does not belong to this organizatio msgid "Unable to remove domain from unknown organization." msgstr "Unable to remove domain from unknown organization." -#: src/domain/mutations/remove-domain.js:133 -#: src/domain/mutations/remove-domain.js:147 -#: src/domain/mutations/remove-domain.js:180 -#: src/domain/mutations/remove-domain.js:199 -#: src/domain/mutations/remove-domain.js:225 +#: src/domain/mutations/remove-domain.js:148 +msgid "Unable to remove domain. Domain is not part of organization." +msgstr "Unable to remove domain. Domain is not part of organization." + +#: src/domain/mutations/remove-domain.js:134 +#: src/domain/mutations/remove-domain.js:165 +#: src/domain/mutations/remove-domain.js:198 +#: src/domain/mutations/remove-domain.js:217 #: src/domain/mutations/remove-domain.js:243 -#: src/domain/mutations/remove-domain.js:261 -#: src/domain/mutations/remove-domain.js:279 -#: src/domain/mutations/remove-domain.js:297 +#: src/domain/mutations/remove-domain.js:260 +#: src/domain/mutations/remove-domain.js:278 +#: src/domain/mutations/remove-domain.js:296 #: src/domain/mutations/remove-domain.js:314 -#: src/domain/mutations/remove-domain.js:337 -#: src/domain/mutations/remove-domain.js:348 +#: src/domain/mutations/remove-domain.js:331 +#: src/domain/mutations/remove-domain.js:354 +#: src/domain/mutations/remove-domain.js:365 msgid "Unable to remove domain. Please try again." msgstr "Unable to remove domain. Please try again." +#: src/domain/mutations/remove-organizations-domains.js:92 +msgid "Unable to remove domains from unknown organization." +msgstr "Unable to remove domains from unknown organization." + #: src/organization/mutations/remove-organization.js:107 #: src/organization/mutations/remove-organization.js:119 #: src/organization/mutations/remove-organization.js:151 @@ -1067,12 +1098,12 @@ msgstr "Unable to transfer ownership to a user outside the org. Please invite th msgid "Unable to two factor authenticate. Please try again." msgstr "Unable to two factor authenticate. Please try again." -#: src/domain/mutations/unfavourite-domain.js:93 +#: src/domain/mutations/unfavourite-domain.js:94 msgid "Unable to unfavourite domain, domain is not favourited." msgstr "Unable to unfavourite domain, domain is not favourited." -#: src/domain/mutations/unfavourite-domain.js:123 -#: src/domain/mutations/unfavourite-domain.js:134 +#: src/domain/mutations/unfavourite-domain.js:124 +#: src/domain/mutations/unfavourite-domain.js:135 msgid "Unable to unfavourite domain. Please try again." msgstr "Unable to unfavourite domain. Please try again." @@ -1123,8 +1154,8 @@ msgstr "Unable to update password, passwords do not match requirements. Please t msgid "Unable to update password. Please try again." msgstr "Unable to update password. Please try again." -#: src/user/mutations/update-user-profile.js:171 -#: src/user/mutations/update-user-profile.js:180 +#: src/user/mutations/update-user-profile.js:172 +#: src/user/mutations/update-user-profile.js:181 msgid "Unable to update profile. Please try again." msgstr "Unable to update profile. Please try again." @@ -1169,12 +1200,12 @@ msgstr "Unable to verify account. Please request a new email." msgid "Unable to verify account. Please try again." msgstr "Unable to verify account. Please try again." -#: src/user/queries/is-user-super-admin.js:23 +#: src/user/queries/is-user-super-admin.js:24 msgid "Unable to verify if user is a super admin, please try again." msgstr "Unable to verify if user is a super admin, please try again." -#: src/user/mutations/update-user-profile.js:102 -#: src/user/queries/is-user-admin.js:58 +#: src/user/mutations/update-user-profile.js:103 +#: src/user/queries/is-user-admin.js:59 msgid "Unable to verify if user is an admin, please try again." msgstr "Unable to verify if user is an admin, please try again." @@ -1237,7 +1268,7 @@ msgstr "You must provide a `first` or `last` value to properly paginate the `Dma msgid "You must provide a `first` or `last` value to properly paginate the `DmarcSummaries` connection." msgstr "You must provide a `first` or `last` value to properly paginate the `DmarcSummaries` connection." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:173 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:154 #: src/domain/loaders/load-domain-connections-by-user-id.js:166 msgid "You must provide a `first` or `last` value to properly paginate the `Domain` connection." msgstr "You must provide a `first` or `last` value to properly paginate the `Domain` connection." @@ -1311,7 +1342,7 @@ msgstr "You must provide a `year` value to access the `DmarcSummaries` connectio #: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:236 #: src/dmarc-summaries/loaders/load-full-pass-connections-by-sum-id.js:83 #: src/dmarc-summaries/loaders/load-spf-failure-connections-by-sum-id.js:83 -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:220 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:201 #: src/domain/loaders/load-domain-connections-by-user-id.js:213 #: src/email-scan/loaders/load-dkim-connections-by-domain-id.js:170 #: src/email-scan/loaders/load-dkim-results-connections-by-dkim-id.js:136 @@ -1365,7 +1396,7 @@ msgstr "`{argSet}` on the `DmarcFailureTable` connection cannot be less than zer msgid "`{argSet}` on the `DmarcSummaries` connection cannot be less than zero." msgstr "`{argSet}` on the `DmarcSummaries` connection cannot be less than zero." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:194 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:175 #: src/domain/loaders/load-domain-connections-by-user-id.js:187 msgid "`{argSet}` on the `Domain` connection cannot be less than zero." msgstr "`{argSet}` on the `Domain` connection cannot be less than zero." diff --git a/api/src/locale/fr/messages.po b/api/src/locale/fr/messages.po index 5724b8f62b..be7c260aed 100644 --- a/api/src/locale/fr/messages.po +++ b/api/src/locale/fr/messages.po @@ -19,7 +19,7 @@ msgstr "" msgid "Authentication error. Please sign in." msgstr "Erreur d'authentification. Veuillez vous connecter." -#: src/organization/objects/organization.js:183 +#: src/organization/objects/organization.js:188 msgid "Cannot query affiliations on organization without admin permission or higher." msgstr "Impossible d'interroger les affiliations sur l'organisation sans l'autorisation de l'administrateur ou plus." @@ -86,10 +86,10 @@ msgstr "Le nom de l'organisation est déjà utilisé, veuillez en choisir un aut msgid "Organization name already in use. Please try again with a different name." msgstr "Le nom de l'organisation est déjà utilisé. Veuillez réessayer avec un nom différent." -#: src/auth/check-domain-ownership.js:33 -#: src/auth/check-domain-ownership.js:45 -#: src/auth/check-domain-ownership.js:67 -#: src/auth/check-domain-ownership.js:78 +#: src/auth/check-domain-ownership.js:30 +#: src/auth/check-domain-ownership.js:42 +#: src/auth/check-domain-ownership.js:64 +#: src/auth/check-domain-ownership.js:75 msgid "Ownership check error. Unable to request domain information." msgstr "Erreur de vérification de la propriété. Impossible de demander des informations sur le domaine." @@ -123,7 +123,7 @@ msgstr "Passer à la fois `first` et `last` pour paginer la connexion `DmarcFail msgid "Passing both `first` and `last` to paginate the `DmarcSummaries` connection is not supported." msgstr "Passer à la fois `first` et `last` pour paginer la connexion `DmarcSummaries` n'est pas supporté." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:182 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:163 #: src/domain/loaders/load-domain-connections-by-user-id.js:175 msgid "Passing both `first` and `last` to paginate the `Domain` connection is not supported." msgstr "Passer à la fois `first` et `last` pour paginer la connexion `Domain` n'est pas supporté." @@ -203,7 +203,7 @@ msgstr "Les mots de passe ne correspondent pas." msgid "Permission Denied: Could not retrieve specified organization." msgstr "Permission refusée : Impossible de récupérer l'organisation spécifiée." -#: src/user/mutations/update-user-profile.js:114 +#: src/user/mutations/update-user-profile.js:115 msgid "Permission Denied: Multi-factor authentication is required for admin accounts" msgstr "Permission refusée : L'authentification multifactorielle est requise pour les comptes admin." @@ -211,10 +211,18 @@ msgstr "Permission refusée : L'authentification multifactorielle est requise po msgid "Permission Denied: Please contact org owner to transfer ownership." msgstr "Permission refusée : Veuillez contacter le propriétaire de l'org pour transférer la propriété." +#: src/domain/mutations/remove-organizations-domains.js:135 +msgid "Permission Denied: Please contact organization admin for help with archiving domains." +msgstr "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur l'archivage des domaines." + #: src/domain/mutations/remove-domain.js:117 msgid "Permission Denied: Please contact organization admin for help with removing domain." msgstr "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide afin de supprimer le domaine." +#: src/domain/mutations/remove-organizations-domains.js:122 +msgid "Permission Denied: Please contact organization admin for help with removing domains." +msgstr "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur la suppression des domaines." + #: src/organization/mutations/remove-organization.js:72 msgid "Permission Denied: Please contact organization admin for help with removing organization." msgstr "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide afin de supprimer l'organisation." @@ -241,6 +249,7 @@ msgstr "Permission refusée : Veuillez contacter l'administrateur de l'organisat msgid "Permission Denied: Please contact organization admin for help with user role changes." msgstr "Permission refusée : Veuillez contacter l'administrateur de l'organisation pour obtenir de l'aide sur les changements de rôle des utilisateurs." +#: src/domain/mutations/add-organizations-domains.js:131 #: src/domain/mutations/create-domain.js:140 msgid "Permission Denied: Please contact organization user for help with creating domain." msgstr "Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur la création du domaine." @@ -249,7 +258,7 @@ msgstr "Permission refusée : Veuillez contacter l'utilisateur de l'organisation msgid "Permission Denied: Please contact organization user for help with retrieving this domain." msgstr "Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide pour récupérer ce domaine." -#: src/domain/mutations/request-scan.js:82 +#: src/domain/mutations/request-scan.js:71 msgid "Permission Denied: Please contact organization user for help with scanning this domain." msgstr "Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur l'analyse de ce domaine." @@ -258,6 +267,7 @@ msgid "Permission Denied: Please contact organization user for help with updatin msgstr "Autorisation refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur la mise à jour de ce domaine." #: src/domain/mutations/remove-domain.js:104 +#: src/domain/mutations/remove-organizations-domains.js:109 msgid "Permission Denied: Please contact super admin for help with removing domain." msgstr "Permission refusée : Veuillez contacter l'utilisateur de l'organisation pour obtenir de l'aide sur la mise à jour de ce domaine." @@ -266,8 +276,8 @@ msgid "Permission Denied: Please contact super admin for help with removing orga msgstr "Permission refusée : Veuillez contacter le super administrateur pour qu'il vous aide à supprimer l'organisation." #: src/domain/mutations/request-scan.js:67 -msgid "Permission Denied: Please contact super admin for help with scanning this domain." -msgstr "Permission refusée : Veuillez contacter le super administrateur pour obtenir de l'aide sur l'analyse de ce domaine." +#~ msgid "Permission Denied: Please contact super admin for help with scanning this domain." +#~ msgstr "Permission refusée : Veuillez contacter le super administrateur pour obtenir de l'aide sur l'analyse de ce domaine." #: src/organization/mutations/verify-organization.js:69 msgid "Permission Denied: Please contact super admin for help with verifying this organization." @@ -291,7 +301,7 @@ msgid "Permission error: Unable to close other user's account." msgstr "Erreur de permission: Impossible de fermer le compte d'un autre utilisateur." #: src/auth/super-admin-required.js:15 -#: src/organization/queries/get-all-organization-domain-statuses.js:35 +#: src/organization/queries/get-all-organization-domain-statuses.js:36 msgid "Permissions error. You do not have sufficient permissions to access this data." msgstr "Erreur de permissions. Vous n'avez pas les autorisations suffisantes pour accéder à ces données." @@ -303,7 +313,7 @@ msgstr "Le numéro de téléphone a été supprimé avec succès." msgid "Phone number has been successfully set, you will receive a verification text message shortly." msgstr "Le numéro de téléphone a été configuré avec succès, vous recevrez bientôt un message de vérification." -#: src/user/mutations/update-user-profile.js:197 +#: src/user/mutations/update-user-profile.js:198 msgid "Profile successfully updated." msgstr "Le profil a été mis à jour avec succès." @@ -324,7 +334,7 @@ msgstr "La demande d'enregistrements `{amount}` sur la connexion `DkimFailureTab msgid "Requesting `{amount}` records on the `DmarcSummaries` connection exceeds the `{argSet}` limit of 100 records." msgstr "La demande d'enregistrements `{amount}` sur la connexion `DmarcSummaries` dépasse la limite `{argSet}` de 100 enregistrements." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:205 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:186 #: src/domain/loaders/load-domain-connections-by-user-id.js:198 msgid "Requesting `{amount}` records on the `Domain` connection exceeds the `{argSet}` limit of 100 records." msgstr "La demande d'enregistrements `{amount}` sur la connexion `Domain` dépasse la limite `{argSet}` de 100 enregistrements." @@ -394,11 +404,15 @@ msgstr "La demande de {amount} enregistrements sur la connexion `SPF` dépasse l msgid "Requesting {amount} records on the `SSL` connection exceeds the `{argSet}` limit of 100 records." msgstr "La demande de {amount} enregistrements sur la connexion `SSL` dépasse la limite `{argSet}` de 100 enregistrements." +#: src/domain/mutations/add-organizations-domains.js:351 +msgid "Successfully added {domainCount} domains to {0}." +msgstr "Ajouté avec succès les domaines {domainCount} à {0}." + #: src/user/mutations/close-account.js:521 msgid "Successfully closed account." msgstr "Le compte a été fermé avec succès." -#: src/domain/mutations/request-scan.js:105 +#: src/domain/mutations/request-scan.js:94 msgid "Successfully dispatched one time scan." msgstr "Un seul balayage a été effectué avec succès." @@ -414,11 +428,11 @@ msgstr "L'utilisateur a été invité avec succès à l'organisation et l'email msgid "Successfully left organization: {0}" msgstr "L'organisation a été quittée avec succès: {0}" -#: src/domain/mutations/unfavourite-domain.js:145 +#: src/domain/mutations/unfavourite-domain.js:146 msgid "Successfully removed domain: {0} from favourites." msgstr "A réussi à supprimer le domaine : {0} des favoris." -#: src/domain/mutations/remove-domain.js:378 +#: src/domain/mutations/remove-domain.js:395 msgid "Successfully removed domain: {0} from {1}." msgstr "A réussi à supprimer le domaine : {0} de {1}." @@ -430,6 +444,10 @@ msgstr "A réussi à supprimer l'organisation : {0}." msgid "Successfully removed user from organization." msgstr "L'utilisateur a été retiré de l'organisation avec succès." +#: src/domain/mutations/remove-organizations-domains.js:530 +msgid "Successfully removed {domainCount} domains from {0}." +msgstr "Suppression réussie des domaines {domainCount} de {0}." + #: src/affiliation/mutations/invite-user-to-org.js:167 msgid "Successfully sent invitation to service, and organization email." msgstr "Envoi réussi de l'invitation au service, et de l'email de l'organisation." @@ -517,6 +535,7 @@ msgstr "Impossible de fermer le compte d'un utilisateur non défini." msgid "Unable to close account. Please try again." msgstr "Impossible de fermer le compte. Veuillez réessayer." +#: src/domain/mutations/add-organizations-domains.js:115 #: src/domain/mutations/create-domain.js:120 msgid "Unable to create domain in unknown organization." msgstr "Impossible de créer un domaine dans une organisation inconnue." @@ -536,6 +555,10 @@ msgstr "Impossible de créer le domaine, l'organisation l'a déjà réclamé." msgid "Unable to create domain. Please try again." msgstr "Impossible de créer un domaine. Veuillez réessayer." +#: src/domain/mutations/add-organizations-domains.js:293 +msgid "Unable to create domains. Please try again." +msgstr "Impossible de créer des domaines. Veuillez réessayer." + #: src/organization/mutations/create-organization.js:201 #: src/organization/mutations/create-organization.js:224 #: src/organization/mutations/create-organization.js:235 @@ -548,16 +571,16 @@ msgstr "Impossible de créer une organisation. Veuillez réessayer." #~ msgid "Unable to dispatch one time scan. Please try again." #~ msgstr "Impossible d'envoyer un scan unique. Veuillez réessayer." -#: src/domain/mutations/favourite-domain.js:93 +#: src/domain/mutations/favourite-domain.js:94 msgid "Unable to favourite domain, user has already favourited it." msgstr "Impossible de favoriser le domaine, l'utilisateur l'a déjà favorisé." -#: src/domain/mutations/favourite-domain.js:72 -#: src/domain/mutations/favourite-domain.js:82 -#: src/domain/mutations/favourite-domain.js:116 -#: src/domain/mutations/favourite-domain.js:125 -#: src/domain/mutations/unfavourite-domain.js:72 -#: src/domain/mutations/unfavourite-domain.js:82 +#: src/domain/mutations/favourite-domain.js:73 +#: src/domain/mutations/favourite-domain.js:83 +#: src/domain/mutations/favourite-domain.js:117 +#: src/domain/mutations/favourite-domain.js:126 +#: src/domain/mutations/unfavourite-domain.js:73 +#: src/domain/mutations/unfavourite-domain.js:83 msgid "Unable to favourite domain. Please try again." msgstr "Impossible d'accéder au domaine favori. Veuillez réessayer." @@ -767,15 +790,15 @@ msgstr "Impossible de charger les informations d'affiliation. Veuillez réessaye msgid "Unable to load affiliation(s). Please try again." msgstr "Impossible de charger l'affiliation (s). Veuillez réessayer." -#: src/organization/loaders/load-all-organization-domain-statuses.js:38 +#: src/organization/loaders/load-all-organization-domain-statuses.js:36 msgid "Unable to load all organization domain statuses. Please try again." msgstr "Impossible de charger tous les statuts de domaine d'organisation. Veuillez réessayer." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:464 -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:474 -#: src/domain/loaders/load-domain-connections-by-user-id.js:469 -#: src/domain/loaders/load-domain-tags-by-org-id.js:26 -#: src/user/loaders/load-my-tracker-by-user-id.js:42 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:547 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:557 +#: src/domain/loaders/load-domain-connections-by-user-id.js:471 +#: src/domain/loaders/load-domain-tags-by-org-id.js:27 +#: src/user/loaders/load-my-tracker-by-user-id.js:43 msgid "Unable to load domain(s). Please try again." msgstr "Impossible de charger le(s) domaine(s). Veuillez réessayer." @@ -832,7 +855,7 @@ msgstr "Impossible de charger les informations sur le propriétaire. Veuillez r msgid "Unable to load summary. Please try again." msgstr "Impossible de charger le résumé. Veuillez réessayer." -#: src/domain/loaders/load-domain-tags-by-org-id.js:36 +#: src/domain/loaders/load-domain-tags-by-org-id.js:37 msgid "Unable to load tags(s). Please try again." msgstr "Impossible de charger les balises. Veuillez réessayer." @@ -871,8 +894,8 @@ msgstr "Impossible de charger le résumé web. Veuillez réessayer." msgid "Unable to query affiliation(s). Please try again." msgstr "Impossible de demander l'affiliation (s). Veuillez réessayer." -#: src/domain/loaders/load-domain-connections-by-user-id.js:459 -#: src/user/loaders/load-my-tracker-by-user-id.js:32 +#: src/domain/loaders/load-domain-connections-by-user-id.js:461 +#: src/user/loaders/load-my-tracker-by-user-id.js:33 msgid "Unable to query domain(s). Please try again." msgstr "Impossible d'interroger le(s) domaine(s). Veuillez réessayer." @@ -902,21 +925,29 @@ msgstr "Impossible de supprimer un utilisateur qui n'appartient déjà plus à c msgid "Unable to remove domain from unknown organization." msgstr "Impossible de supprimer le domaine d'une organisation inconnue." -#: src/domain/mutations/remove-domain.js:133 -#: src/domain/mutations/remove-domain.js:147 -#: src/domain/mutations/remove-domain.js:180 -#: src/domain/mutations/remove-domain.js:199 -#: src/domain/mutations/remove-domain.js:225 +#: src/domain/mutations/remove-domain.js:148 +msgid "Unable to remove domain. Domain is not part of organization." +msgstr "Impossible de supprimer le domaine. Le domaine ne fait pas partie de l'organisation." + +#: src/domain/mutations/remove-domain.js:134 +#: src/domain/mutations/remove-domain.js:165 +#: src/domain/mutations/remove-domain.js:198 +#: src/domain/mutations/remove-domain.js:217 #: src/domain/mutations/remove-domain.js:243 -#: src/domain/mutations/remove-domain.js:261 -#: src/domain/mutations/remove-domain.js:279 -#: src/domain/mutations/remove-domain.js:297 +#: src/domain/mutations/remove-domain.js:260 +#: src/domain/mutations/remove-domain.js:278 +#: src/domain/mutations/remove-domain.js:296 #: src/domain/mutations/remove-domain.js:314 -#: src/domain/mutations/remove-domain.js:337 -#: src/domain/mutations/remove-domain.js:348 +#: src/domain/mutations/remove-domain.js:331 +#: src/domain/mutations/remove-domain.js:354 +#: src/domain/mutations/remove-domain.js:365 msgid "Unable to remove domain. Please try again." msgstr "Impossible de supprimer le domaine. Veuillez réessayer." +#: src/domain/mutations/remove-organizations-domains.js:92 +msgid "Unable to remove domains from unknown organization." +msgstr "Impossible de supprimer les domaines d'une organisation inconnue." + #: src/organization/mutations/remove-organization.js:107 #: src/organization/mutations/remove-organization.js:119 #: src/organization/mutations/remove-organization.js:151 @@ -1067,12 +1098,12 @@ msgstr "Impossible de transférer la propriété à un utilisateur extérieur à msgid "Unable to two factor authenticate. Please try again." msgstr "Impossible de s'authentifier par deux facteurs. Veuillez réessayer." -#: src/domain/mutations/unfavourite-domain.js:93 +#: src/domain/mutations/unfavourite-domain.js:94 msgid "Unable to unfavourite domain, domain is not favourited." msgstr "Impossible de désactiver le domaine, le domaine n'est pas favorisé." -#: src/domain/mutations/unfavourite-domain.js:123 -#: src/domain/mutations/unfavourite-domain.js:134 +#: src/domain/mutations/unfavourite-domain.js:124 +#: src/domain/mutations/unfavourite-domain.js:135 msgid "Unable to unfavourite domain. Please try again." msgstr "Impossible de défavoriser le domaine. Veuillez réessayer." @@ -1123,8 +1154,8 @@ msgstr "Impossible de mettre à jour le mot de passe, les mots de passe ne corre msgid "Unable to update password. Please try again." msgstr "Impossible de mettre à jour le mot de passe. Veuillez réessayer." -#: src/user/mutations/update-user-profile.js:171 -#: src/user/mutations/update-user-profile.js:180 +#: src/user/mutations/update-user-profile.js:172 +#: src/user/mutations/update-user-profile.js:181 msgid "Unable to update profile. Please try again." msgstr "Impossible de mettre à jour le profil. Veuillez réessayer." @@ -1169,12 +1200,12 @@ msgstr "Impossible de vérifier le compte. Veuillez demander un nouvel e-mail." msgid "Unable to verify account. Please try again." msgstr "Impossible de vérifier le compte. Veuillez réessayer." -#: src/user/queries/is-user-super-admin.js:23 +#: src/user/queries/is-user-super-admin.js:24 msgid "Unable to verify if user is a super admin, please try again." msgstr "Impossible de vérifier si l'utilisateur est un super administrateur, veuillez réessayer." -#: src/user/mutations/update-user-profile.js:102 -#: src/user/queries/is-user-admin.js:58 +#: src/user/mutations/update-user-profile.js:103 +#: src/user/queries/is-user-admin.js:59 msgid "Unable to verify if user is an admin, please try again." msgstr "Impossible de vérifier si l'utilisateur est un administrateur, veuillez réessayer." @@ -1237,7 +1268,7 @@ msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctemen msgid "You must provide a `first` or `last` value to properly paginate the `DmarcSummaries` connection." msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `DmarcSummaries`." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:173 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:154 #: src/domain/loaders/load-domain-connections-by-user-id.js:166 msgid "You must provide a `first` or `last` value to properly paginate the `Domain` connection." msgstr "Vous devez fournir une valeur `first` ou `last` pour paginer correctement la connexion `Domain`." @@ -1311,7 +1342,7 @@ msgstr "Vous devez fournir une valeur `year` pour accéder à la connexion `Dmar #: src/dmarc-summaries/loaders/load-dmarc-sum-connections-by-user-id.js:236 #: src/dmarc-summaries/loaders/load-full-pass-connections-by-sum-id.js:83 #: src/dmarc-summaries/loaders/load-spf-failure-connections-by-sum-id.js:83 -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:220 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:201 #: src/domain/loaders/load-domain-connections-by-user-id.js:213 #: src/email-scan/loaders/load-dkim-connections-by-domain-id.js:170 #: src/email-scan/loaders/load-dkim-results-connections-by-dkim-id.js:136 @@ -1365,7 +1396,7 @@ msgstr "`{argSet}` sur la connexion `DmarcFailureTable` ne peut être inférieur msgid "`{argSet}` on the `DmarcSummaries` connection cannot be less than zero." msgstr "`{argSet}` sur la connexion `DmarcSummaries` ne peut être inférieur à zéro." -#: src/domain/loaders/load-domain-connections-by-organizations-id.js:194 +#: src/domain/loaders/load-domain-connections-by-organizations-id.js:175 #: src/domain/loaders/load-domain-connections-by-user-id.js:187 msgid "`{argSet}` on the `Domain` connection cannot be less than zero." msgstr "`{argSet}` sur la connexion `Domain` ne peut être inférieur à zéro." diff --git a/frontend/src/graphql/mutations.js b/frontend/src/graphql/mutations.js index d9c44cb41e..2acb76ccf6 100644 --- a/frontend/src/graphql/mutations.js +++ b/frontend/src/graphql/mutations.js @@ -633,4 +633,62 @@ export const UNFAVOURITE_DOMAIN = gql` } ` +export const ADD_ORGANIZATIONS_DOMAINS = gql` + mutation AddOrganizationsDomains( + $orgId: ID! + $domains: [DomainScalar]! + $hideNewDomains: Boolean + $tagNewDomains: Boolean + $audit: Boolean + ) { + addOrganizationsDomains( + input: { + orgId: $orgId + domains: $domains + hideNewDomains: $hideNewDomains + tagNewDomains: $tagNewDomains + audit: $audit + } + ) { + result { + ... on DomainBulkResult { + status + } + ... on DomainError { + code + description + } + } + } + } +` + +export const REMOVE_ORGANIZATIONS_DOMAINS = gql` + mutation RemoveOrganizationsDomains( + $orgId: ID! + $domains: [DomainScalar]! + $archiveDomains: Boolean + $audit: Boolean + ) { + removeOrganizationsDomains( + input: { + orgId: $orgId + domains: $domains + archiveDomains: $archiveDomains + audit: $audit + } + ) { + result { + ... on DomainBulkResult { + status + } + ... on DomainError { + code + description + } + } + } + } +` + export default ''