@@ -18,6 +18,7 @@ import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
1818import { inject , injectable } from "inversify" ;
1919import { Authorizer } from "../authorization/authorizer" ;
2020import { ProjectsService } from "../projects/projects-service" ;
21+ import { TransactionalContext } from "@gitpod/gitpod-db/lib/typeorm/transactional-db-impl" ;
2122
2223@injectable ( )
2324export class OrganizationService {
@@ -183,90 +184,103 @@ export class OrganizationService {
183184 orgId : string ,
184185 memberId : string ,
185186 role : OrgMemberRole ,
187+ txCtx ?: TransactionalContext ,
186188 ) : Promise < void > {
187189 if ( userId ) {
188190 await this . auth . checkPermissionOnOrganization ( userId , "write_members" , orgId ) ;
189191 }
190- if ( role !== "owner" ) {
191- const members = await this . teamDB . findMembersByTeam ( orgId ) ;
192- if ( ! members . some ( ( m ) => m . userId !== memberId && m . role === "owner" ) ) {
193- throw new ApplicationError ( ErrorCodes . CONFLICT , "Cannot remove the last owner of an organization." ) ;
194- }
195- }
196- const members = await this . teamDB . findMembersByTeam ( orgId ) ;
197- const firstMember = members . filter ( ( m ) => m . userId !== BUILTIN_INSTLLATION_ADMIN_USER_ID ) . length === 0 ;
198- if ( firstMember ) {
199- // first member (that is not an admin) is going to be an owner
200- role = "owner" ;
201- log . info ( { userId : memberId } , "First member of organization, setting role to owner." ) ;
202- }
203-
192+ let members : OrgMemberInfo [ ] = [ ] ;
204193 try {
205- await this . teamDB . transaction ( async ( db ) => {
206- await db . addMemberToTeam ( memberId , orgId ) ;
207- await db . setTeamMemberRole ( memberId , orgId , role ) ;
194+ await this . teamDB . transaction ( txCtx , async ( teamDB , txCtx ) => {
195+ members = await teamDB . findMembersByTeam ( orgId ) ;
196+ const hasOtherRegularOwners =
197+ members . filter (
198+ ( m ) =>
199+ m . userId !== BUILTIN_INSTLLATION_ADMIN_USER_ID && //
200+ m . userId !== memberId && //
201+ m . role === "owner" ,
202+ ) . length > 0 ;
203+ if ( ! hasOtherRegularOwners ) {
204+ // first regular member is going to be an owner
205+ role = "owner" ;
206+ log . info ( { userId : memberId } , "First member of organization, setting role to owner." ) ;
207+ }
208+
209+ await teamDB . addMemberToTeam ( memberId , orgId ) ;
210+ await teamDB . setTeamMemberRole ( memberId , orgId , role ) ;
208211 await this . auth . addOrganizationRole ( orgId , memberId , role ) ;
212+ // we can remove the built-in installation admin if we have added an owner
213+ if ( ! hasOtherRegularOwners && members . some ( ( m ) => m . userId === BUILTIN_INSTLLATION_ADMIN_USER_ID ) ) {
214+ try {
215+ await this . removeOrganizationMember ( memberId , orgId , BUILTIN_INSTLLATION_ADMIN_USER_ID , txCtx ) ;
216+ } catch ( error ) {
217+ log . warn ( "Failed to remove built-in installation admin from organization." , error ) ;
218+ }
219+ }
209220 } ) ;
210221 } catch ( err ) {
211- //TODO simply removing the user is not necessarily the right thing to do here, as the user might have been a member before
212- await this . auth . removeOrganizationRole ( orgId , memberId , "member" ) ;
222+ await this . auth . removeOrganizationRole (
223+ orgId ,
224+ memberId ,
225+ members . find ( ( m ) => m . userId === memberId ) ?. role || "member" ,
226+ ) ;
213227 throw err ;
214228 }
215- // we can remove the built-in installation admin now
216- if ( firstMember ) {
217- try {
218- await this . removeOrganizationMember ( memberId , orgId , BUILTIN_INSTLLATION_ADMIN_USER_ID ) ;
219- } catch ( error ) {
220- log . warn ( "Failed to remove built-in installation admin from organization." , error ) ;
221- }
222- }
223229 }
224230
225- public async removeOrganizationMember ( userId : string , orgId : string , memberId : string ) : Promise < void > {
231+ public async removeOrganizationMember (
232+ userId : string ,
233+ orgId : string ,
234+ memberId : string ,
235+ txCtx ?: TransactionalContext ,
236+ ) : Promise < void > {
226237 // The user is leaving a team, if they are removing themselves from the team.
227238 if ( userId === memberId ) {
228239 await this . auth . checkPermissionOnOrganization ( userId , "read_info" , orgId ) ;
229240 } else {
230241 await this . auth . checkPermissionOnOrganization ( userId , "write_members" , orgId ) ;
231242 }
243+ let membership : OrgMemberInfo | undefined ;
244+ try {
245+ await this . teamDB . transaction ( txCtx , async ( db ) => {
246+ // Check for existing membership.
247+ const members = await db . findMembersByTeam ( orgId ) ;
248+ // cannot remove last owner
249+ if ( ! members . some ( ( m ) => m . userId !== memberId && m . role === "owner" ) ) {
250+ throw new ApplicationError ( ErrorCodes . CONFLICT , "Cannot remove the last owner of an organization." ) ;
251+ }
232252
233- // Check for existing membership.
234- const members = await this . teamDB . findMembersByTeam ( orgId ) ;
235- // cannot remove last owner
236- if ( ! members . some ( ( m ) => m . userId !== memberId && m . role === "owner" ) ) {
237- throw new ApplicationError ( ErrorCodes . CONFLICT , "Cannot remove the last owner of an organization." ) ;
238- }
239-
240- const membership = members . find ( ( m ) => m . userId === memberId ) ;
241- if ( ! membership ) {
242- throw new ApplicationError (
243- ErrorCodes . NOT_FOUND ,
244- `Could not find membership for user '${ memberId } ' in organization '${ orgId } '` ,
245- ) ;
246- }
253+ membership = members . find ( ( m ) => m . userId === memberId ) ;
254+ if ( ! membership ) {
255+ throw new ApplicationError (
256+ ErrorCodes . NOT_FOUND ,
257+ `Could not find membership for user '${ memberId } ' in organization '${ orgId } '` ,
258+ ) ;
259+ }
247260
248- // Check if user's account belongs to the Org.
249- const userToBeRemoved = await this . userDB . findUserById ( memberId ) ;
250- if ( ! userToBeRemoved ) {
251- throw new ApplicationError ( ErrorCodes . NOT_FOUND , `Could not find user '${ memberId } '` ) ;
252- }
253- // Only invited members can be removed from the Org, but organizational accounts cannot.
254- if ( userToBeRemoved . organizationId && orgId === userToBeRemoved . organizationId ) {
255- throw new ApplicationError (
256- ErrorCodes . PERMISSION_DENIED ,
257- `User's account '${ memberId } ' belongs to the organization '${ orgId } '` ,
258- ) ;
259- }
261+ // Check if user's account belongs to the Org.
262+ const userToBeRemoved = await this . userDB . findUserById ( memberId ) ;
263+ if ( ! userToBeRemoved ) {
264+ throw new ApplicationError ( ErrorCodes . NOT_FOUND , `Could not find user '${ memberId } '` ) ;
265+ }
266+ // Only invited members can be removed from the Org, but organizational accounts cannot.
267+ if ( userToBeRemoved . organizationId && orgId === userToBeRemoved . organizationId ) {
268+ throw new ApplicationError (
269+ ErrorCodes . PERMISSION_DENIED ,
270+ `User's account '${ memberId } ' belongs to the organization '${ orgId } '` ,
271+ ) ;
272+ }
260273
261- try {
262- await this . teamDB . transaction ( async ( db ) => {
263274 await db . removeMemberFromTeam ( userToBeRemoved . id , orgId ) ;
264275 await this . auth . removeOrganizationRole ( orgId , memberId , "member" ) ;
265276 } ) ;
266277 } catch ( err ) {
267- // Rollback to the original role the user had
268- await this . auth . addOrganizationRole ( orgId , memberId , membership . role ) ;
269- throw new ApplicationError ( ErrorCodes . INTERNAL_SERVER_ERROR , err ) ;
278+ if ( membership ) {
279+ // Rollback to the original role the user had
280+ await this . auth . addOrganizationRole ( orgId , memberId , membership . role ) ;
281+ }
282+ const code = ApplicationError . hasErrorCode ( err ) ? err . code : ErrorCodes . INTERNAL_SERVER_ERROR ;
283+ throw new ApplicationError ( code , err ) ;
270284 }
271285 this . analytics . track ( {
272286 userId,
0 commit comments