From 8cf0288205e4169888c66b657f501df0f85e7060 Mon Sep 17 00:00:00 2001 From: Andrea Rosci Date: Mon, 20 Jan 2025 13:53:02 +0100 Subject: [PATCH] Add methods for the team application access model Change-type: minor --- DOCUMENTATION.md | 109 ++++++++ src/models/team-application-access.ts | 241 ++++++++++++++++++ src/models/team.ts | 10 + .../models/team-application-access.spec.ts | 193 ++++++++++++++ 4 files changed, 553 insertions(+) create mode 100644 src/models/team-application-access.ts create mode 100644 tests/integration/models/team-application-access.spec.ts diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 61479cb40..c4e3d27e9 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -353,6 +353,11 @@ const sdk = fromSharedOptions(); * [.get(handleOrId, [options])](#balena.models.organization.get) ⇒ Promise * [.remove(handleOrId)](#balena.models.organization.remove) ⇒ Promise * [.team](#balena.models.team) : object + * [.applicationAccess](#balena.models.team.applicationAccess) : object + * [.getAllByTeam(teamId, [options])](#balena.models.team.applicationAccess.getAllByTeam) ⇒ Promise + * [.get(teamApplicationAccessId, [options])](#balena.models.team.applicationAccess.get) ⇒ Promise + * [.update(teamApplicationAccessId, roleName)](#balena.models.team.applicationAccess.update) ⇒ Promise + * [.remove(teamApplicationAccessId)](#balena.models.team.applicationAccess.remove) ⇒ Promise * [.create(organizationSlugOrId, name)](#balena.models.team.create) ⇒ Promise * [.getAllByOrganization(organizationSlugOrId, [options])](#balena.models.team.getAllByOrganization) ⇒ Promise * [.get(teamId, [options])](#balena.models.team.get) ⇒ Promise @@ -761,6 +766,11 @@ balena.models.device.get(123).catch(function (error) { * [.get(handleOrId, [options])](#balena.models.organization.get) ⇒ Promise * [.remove(handleOrId)](#balena.models.organization.remove) ⇒ Promise * [.team](#balena.models.team) : object + * [.applicationAccess](#balena.models.team.applicationAccess) : object + * [.getAllByTeam(teamId, [options])](#balena.models.team.applicationAccess.getAllByTeam) ⇒ Promise + * [.get(teamApplicationAccessId, [options])](#balena.models.team.applicationAccess.get) ⇒ Promise + * [.update(teamApplicationAccessId, roleName)](#balena.models.team.applicationAccess.update) ⇒ Promise + * [.remove(teamApplicationAccessId)](#balena.models.team.applicationAccess.remove) ⇒ Promise * [.create(organizationSlugOrId, name)](#balena.models.team.create) ⇒ Promise * [.getAllByOrganization(organizationSlugOrId, [options])](#balena.models.team.getAllByOrganization) ⇒ Promise * [.get(teamId, [options])](#balena.models.team.get) ⇒ Promise @@ -5097,12 +5107,111 @@ balena.models.organization.remove(123); **Kind**: static namespace of [models](#balena.models) * [.team](#balena.models.team) : object + * [.applicationAccess](#balena.models.team.applicationAccess) : object + * [.getAllByTeam(teamId, [options])](#balena.models.team.applicationAccess.getAllByTeam) ⇒ Promise + * [.get(teamApplicationAccessId, [options])](#balena.models.team.applicationAccess.get) ⇒ Promise + * [.update(teamApplicationAccessId, roleName)](#balena.models.team.applicationAccess.update) ⇒ Promise + * [.remove(teamApplicationAccessId)](#balena.models.team.applicationAccess.remove) ⇒ Promise * [.create(organizationSlugOrId, name)](#balena.models.team.create) ⇒ Promise * [.getAllByOrganization(organizationSlugOrId, [options])](#balena.models.team.getAllByOrganization) ⇒ Promise * [.get(teamId, [options])](#balena.models.team.get) ⇒ Promise * [.rename(teamId, newName)](#balena.models.team.rename) ⇒ Promise * [.remove(teamId)](#balena.models.team.remove) ⇒ Promise + + +##### team.applicationAccess : object +**Kind**: static namespace of [team](#balena.models.team) + +* [.applicationAccess](#balena.models.team.applicationAccess) : object + * [.getAllByTeam(teamId, [options])](#balena.models.team.applicationAccess.getAllByTeam) ⇒ Promise + * [.get(teamApplicationAccessId, [options])](#balena.models.team.applicationAccess.get) ⇒ Promise + * [.update(teamApplicationAccessId, roleName)](#balena.models.team.applicationAccess.update) ⇒ Promise + * [.remove(teamApplicationAccessId)](#balena.models.team.applicationAccess.remove) ⇒ Promise + + + +###### applicationAccess.getAllByTeam(teamId, [options]) ⇒ Promise +This method get all team application access. + +**Kind**: static method of [applicationAccess](#balena.models.team.applicationAccess) +**Summary**: Get all team applications access +**Access**: public +**Fulfil**: Object[] - team application access + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| teamId | Number | | Required: the team id. | +| [options] | Object | {} | extra pine options to use | + +**Example** +```js +balena.models.team.applicationAccess.getAllByTeam(1239948).then(function(teamApplicationAccesses) { + console.log(teamApplicationAccesses); +}); +``` + + +###### applicationAccess.get(teamApplicationAccessId, [options]) ⇒ Promise +This method get specific team application access. + +**Kind**: static method of [applicationAccess](#balena.models.team.applicationAccess) +**Summary**: Get team applications access +**Access**: public +**Fulfil**: Object - TeamApplicationAccess + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| teamApplicationAccessId | Number | | Required: the team application access id. | +| [options] | Object | {} | extra pine options to use | + +**Example** +```js +balena.models.team.applicationAccess.get(1239948).then(function(teamApplicationAccess) { + console.log(teamApplicationAccess); +}); +``` + + +###### applicationAccess.update(teamApplicationAccessId, roleName) ⇒ Promise +This method update a team application access role. + +**Kind**: static method of [applicationAccess](#balena.models.team.applicationAccess) +**Summary**: Update team application access +**Access**: public +**Fulfil**: Object - TeamApplicationAccess + +| Param | Type | Description | +| --- | --- | --- | +| teamApplicationAccessId | Number | Required: the team application access id. | +| roleName | String | Required: The new role to assing (ApplicationMembershipRoles). | + +**Example** +```js +balena.models.team.update(123, 'developer').then(function(teamApplicationAccess) { + console.log(teamApplicationAccess); +}); +``` + + +###### applicationAccess.remove(teamApplicationAccessId) ⇒ Promise +This remove a team application access. + +**Kind**: static method of [applicationAccess](#balena.models.team.applicationAccess) +**Summary**: Remove team application access +**Access**: public +**Fulfil**: void + +| Param | Type | Description | +| --- | --- | --- | +| teamApplicationAccessId | Number | Required: the team application access id. | + +**Example** +```js +balena.models.team.remove(123).then(function(teams) { + console.log(teams); +}); +``` ##### team.create(organizationSlugOrId, name) ⇒ Promise diff --git a/src/models/team-application-access.ts b/src/models/team-application-access.ts new file mode 100644 index 000000000..2bf87db6b --- /dev/null +++ b/src/models/team-application-access.ts @@ -0,0 +1,241 @@ +/* +Copyright 2020 Balena + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import type * as BalenaSdk from '..'; +import type { InjectedDependenciesParam } from '..'; +import * as errors from 'balena-errors'; +import { mergePineOptions } from '../util'; + +const getTeamApplicationAccessModel = function ( + deps: InjectedDependenciesParam, +) { + const { pine, sdkInstance } = deps; + + const getRoleId = async (roleName: BalenaSdk.ApplicationMembershipRoles) => { + const role = await pine.get({ + resource: 'application_membership_role', + id: { + name: roleName, + }, + options: { + $select: 'id', + }, + }); + if (!role) { + throw new errors.BalenaApplicationMembershipRoleNotFound(roleName); + } + return role.id; + }; + + /** + * @summary Get all team applications access + * @name getAllByTeam + * @public + * @function + * @memberof balena.models.team.applicationAccess + * + * @description This method get all team application access. + * + * @param {Number} teamId - Required: the team id. + * @param {Object} [options={}] - extra pine options to use + * + * @fulfil {Object[]} - team application access + * @returns {Promise} + * + * @example + * balena.models.team.applicationAccess.getAllByTeam(1239948).then(function(teamApplicationAccesses) { + * console.log(teamApplicationAccesses); + * }); + */ + const getAllByTeam = async function ( + teamId: number, + options: BalenaSdk.PineOptions = {}, + ): Promise { + const team = await sdkInstance.models.team.get(teamId, { $select: 'id' }); + + return sdkInstance.pine.get({ + resource: 'team_application_access', + options: mergePineOptions( + { + $filter: { + team: team.id, + }, + }, + options, + ), + }); + }; + + /** + * @summary Get team applications access + * @name get + * @public + * @function + * @memberof balena.models.team.applicationAccess + * + * @description This method get specific team application access. + * + * @param {Number} teamApplicationAccessId - Required: the team application access id. + * @param {Object} [options={}] - extra pine options to use + * + * @fulfil {Object} - TeamApplicationAccess + * @returns {Promise} + * + * @example + * balena.models.team.applicationAccess.get(1239948).then(function(teamApplicationAccess) { + * console.log(teamApplicationAccess); + * }); + */ + const get = async function ( + teamApplicationAccessId: number, + options: BalenaSdk.PineOptions = {}, + ): Promise { + const teamApplicationAccess = await sdkInstance.pine.get({ + resource: 'team_application_access', + id: teamApplicationAccessId, + options, + }); + if (teamApplicationAccess == null) { + throw new Error('Team application access not found'); + } + return teamApplicationAccess; + }; + + /** + * @summary Add applications access to team + * @name add + * @public + * @function + * @memberof balena.models.team.applicationAccess.add + * + * @description This method add application access to team. + * + * @param {Number} teamId - Required: the team id the application access will be granted for. + * @param {Number|String} applicationIdOrSlug - Required: application id or slug + * @param {String} RoleName - Required: application membership role name (ApplicationMembershipRoles) + * + * @fulfil {Object} - TeamApplicationAccess + * @returns {Promise} + * + * @example + * balena.models.team.applicationAccess.add(1239948, 'MyAppSlug', 'developer').then(function(teamApplicationAccess) { + * console.log(teamApplicationAccess); + * }); + * + * @example + * balena.models.team.applicationAccess.add(1239948, 456789, 'observer').then(function(teamApplicationAccess) { + * console.log(teamApplicationAccess); + * }); + */ + const add = async function ( + teamId: number, + applicationIdOrSlug: number | string, + roleName: BalenaSdk.ApplicationMembershipRoles, + ): Promise { + const appId = ( + await sdkInstance.models.application.get(applicationIdOrSlug, { + $select: 'id', + }) + )?.id; + + if (appId == null) { + throw new errors.BalenaApplicationNotFound(applicationIdOrSlug); + } + const roleId = await getRoleId(roleName); + + return sdkInstance.pine.post({ + resource: 'team_application_access', + body: { + team: teamId, + grants_access_to__application: appId, + application_membership_role: roleId, + }, + }); + }; + + /** + * @summary Update team application access + * @name update + * @public + * @function + * @memberof balena.models.team.applicationAccess + * + * @description This method update a team application access role. + * + * @param {Number} teamApplicationAccessId - Required: the team application access id. + * @param {String} roleName - Required: The new role to assing (ApplicationMembershipRoles). + * + * @fulfil {Object} - TeamApplicationAccess + * @returns {Promise} + * + * @example + * balena.models.team.update(123, 'developer').then(function(teamApplicationAccess) { + * console.log(teamApplicationAccess); + * }); + */ + const update = async function ( + teamApplicationAccessId: number, + roleName: BalenaSdk.ApplicationMembershipRoles, + ): Promise<'OK'> { + const roleId = await getRoleId(roleName); + + return pine.patch({ + resource: 'team_application_access', + id: teamApplicationAccessId, + body: { + application_membership_role: roleId, + }, + }); + }; + + /** + * @summary Remove team application access + * @name remove + * @public + * @function + * @memberof balena.models.team.applicationAccess + * + * @description This remove a team application access. + * + * @param {Number} teamApplicationAccessId - Required: the team application access id. + * + * @fulfil {void} + * @returns {Promise} + * + * @example + * balena.models.team.remove(123).then(function(teams) { + * console.log(teams); + * }); + */ + const remove = async function ( + teamApplicationAccessId: number, + ): Promise { + await pine.delete({ + resource: 'team_application_access', + id: teamApplicationAccessId, + }); + }; + + return { + getAllByTeam, + get, + add, + update, + remove, + }; +}; + +export default getTeamApplicationAccessModel; diff --git a/src/models/team.ts b/src/models/team.ts index 810dcc821..ba1485380 100644 --- a/src/models/team.ts +++ b/src/models/team.ts @@ -22,6 +22,11 @@ import { isId, mergePineOptions } from '../util'; const getTeamModel = function (deps: InjectedDependenciesParam) { const { pine, sdkInstance } = deps; + /* eslint-disable @typescript-eslint/no-require-imports */ + const applicationAccessModel = ( + require('./team-application-access') as typeof import('./team-application-access') + ).default(deps); + /** * @summary Creates a new Team * @name create @@ -250,6 +255,11 @@ const getTeamModel = function (deps: InjectedDependenciesParam) { get, rename, remove, + /** + * @namespace balena.models.team.applicationAccess + * @memberof balena.models.team + */ + applicationAccess: applicationAccessModel, }; }; diff --git a/tests/integration/models/team-application-access.spec.ts b/tests/integration/models/team-application-access.spec.ts new file mode 100644 index 000000000..e17453232 --- /dev/null +++ b/tests/integration/models/team-application-access.spec.ts @@ -0,0 +1,193 @@ +import { expect } from 'chai'; +import { timeSuite } from '../../util'; +import { + balena, + givenInitialOrganization, + givenLoggedInUser, + TEST_TEAM_NAME, + TEST_APPLICATION_NAME_PREFIX, + givenAnApplication, +} from '../setup'; +import { getInitialOrganization } from '../utils'; +import type { ApplicationMembershipRoles } from '../../..'; + +describe('Team Application Access Model', function () { + timeSuite(before); + givenLoggedInUser(before); + givenInitialOrganization(before); + givenAnApplication(before); + + const ctx: Partial<{ + initialOrganization: any; + team: any; + application1: any; + application2: any; + teamApplicationAccessRead: any; + teamApplicationAccessWrite: any; + }> = {}; + + before(async function () { + ctx.initialOrganization = await getInitialOrganization(); + + const teamName = `${TEST_TEAM_NAME}_${Date.now()}`; + ctx.team = await balena.models.team.create( + ctx.initialOrganization.id, + teamName, + ); + + const appName = `${TEST_APPLICATION_NAME_PREFIX}_${Date.now()}_team_application_access`; + ctx.application1 = this.application; + ctx.application2 = await balena.models.application.create({ + name: appName, + organization: ctx.initialOrganization.id, + deviceType: 'raspberry-pi', + }); + + const roleName = 'observer'; + ctx.teamApplicationAccessRead = + await balena.models.team.applicationAccess.add( + ctx.team.id, + ctx.application1.id, + roleName, + ); + }); + + describe('[read operations]', function () { + describe('balena.models.team.applicationAccess.get()', function () { + it('should return a team application access', async function () { + const accesses = await balena.models.team.applicationAccess.get( + ctx.teamApplicationAccessRead.id, + ); + expect(accesses).to.deep.equal(ctx.teamApplicationAccessRead); + }); + it('should be rejected if the team application access does not exist', function () { + const promise = balena.models.team.applicationAccess.get(999999); + expect(promise).to.be.rejectedWith('Team application access not found'); + }); + }); + describe('balena.models.team.applicationAccess.getAllByTeam()', function () { + it('should return an array with one team application access', async function () { + const accesses = + await balena.models.team.applicationAccess.getAllByTeam(ctx.team.id); + expect(accesses).to.deep.equal([ctx.teamApplicationAccessRead]); + }); + it('should return an empty array with one team application access', async function () { + const accesses = + await balena.models.team.applicationAccess.getAllByTeam(ctx.team.id); + expect(accesses).to.deep.equal([ctx.teamApplicationAccessRead]); + }); + it('should be rejected if the team does not exist', async function () { + const promise = + await balena.models.team.applicationAccess.getAllByTeam(999999); + expect(promise).to.be.rejectedWith('Team not found: 999999'); + }); + }); + }); + + describe('[write operations]', function () { + describe('balena.models.team.applicationAccess.add()', function () { + it('should add an application access to a team', async function () { + const roleName = 'developer'; + ctx.teamApplicationAccessWrite = + await balena.models.team.applicationAccess.add( + ctx.team.id, + ctx.application2.id, + roleName, + ); + + expect(ctx.teamApplicationAccessWrite) + .to.have.property('team') + .that.deep.equals({ + __id: ctx.team.id, + }); + expect(ctx.teamApplicationAccessWrite) + .to.have.property('grants_access_to__application') + .that.deep.equals({ __id: ctx.application2.id }); + }); + it('should be rejected if the application does not exist', function () { + const roleName = 'developer'; + const promise = balena.models.team.applicationAccess.add( + ctx.team.id, + 999999, + roleName, + ); + + expect(promise).to.be.rejected.and.eventually.have.property( + 'code', + 'BalenaApplicationNotFound', + ); + }); + it('should be rejected if the role name does not exist', function () { + const roleName = 'randomName'; + const promise = balena.models.team.applicationAccess.add( + ctx.team.id, + ctx.application2.id, + // @ts-expect-error -- we are testing a failing case + roleName, + ); + + expect(promise).to.be.rejected.and.eventually.have.property( + 'code', + 'BalenaApplicationMembershipRoleNotFound', + ); + }); + }); + + describe('balena.models.team.applicationAccess.update()', function () { + it('should update the role of a team application access', async function () { + const newRoleName = 'observer'; + await balena.models.team.applicationAccess.update( + ctx.teamApplicationAccessWrite.id, + newRoleName, + ); + + const roleId = ( + await balena.pine.get({ + resource: 'application_membership_role', + id: { + name: newRoleName as ApplicationMembershipRoles, + }, + options: { + $select: 'id', + }, + }) + )?.[0]?.id; + + const updatedAccess = await balena.models.team.applicationAccess.get( + ctx.teamApplicationAccessWrite.id, + ); + expect(updatedAccess) + .to.have.property('application_membership_role') + .that.deep.equals({ __id: roleId }); + }); + it('should be rejected if the role name does not exist', function () { + const roleName = 'randomName'; + const promise = balena.models.team.applicationAccess.update( + ctx.teamApplicationAccessWrite.id, + // @ts-expect-error -- we are testing a failing case + roleName, + ); + + expect(promise).to.be.rejected.and.eventually.have.property( + 'code', + 'BalenaApplicationMembershipRoleNotFound', + ); + }); + }); + + describe('balena.models.team.applicationAccess.remove()', function () { + it('should remove a team application access', async function () { + await Promise.all( + [ctx.teamApplicationAccessRead, ctx.teamApplicationAccessWrite].map( + (team) => balena.models.team.applicationAccess.remove(team.id), + ), + ); + + const promise = balena.models.team.applicationAccess.getAllByTeam( + ctx.team.id, + ); + expect(promise).to.become([]); + }); + }); + }); +});