mirror of
https://github.com/google/nomulus
synced 2026-01-04 04:04:22 +00:00
Allow adding existing users to registrar (#2616)
This commit is contained in:
@@ -166,9 +166,9 @@ export class BackendService {
|
||||
.pipe(catchError((err) => this.errorCatcher<User[]>(err)));
|
||||
}
|
||||
|
||||
createUser(registrarId: string): Observable<User> {
|
||||
createUser(registrarId: string, maybeUser: User | null): Observable<User> {
|
||||
return this.http
|
||||
.post<User>(`/console-api/users?registrarId=${registrarId}`, {})
|
||||
.post<User>(`/console-api/users?registrarId=${registrarId}`, maybeUser)
|
||||
.pipe(catchError((err) => this.errorCatcher<User>(err)));
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface UserData {
|
||||
supportEmail: string;
|
||||
supportPhoneNumber: string;
|
||||
technicalDocsUrl: string;
|
||||
userRoles?: Map<string, string>;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
|
||||
@@ -19,7 +19,7 @@ import { SelectedRegistrarModule } from '../app.module';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { SnackBarModule } from '../snackbar.module';
|
||||
import { User, UsersService, roleToDescription } from './users.service';
|
||||
import { UsersService, roleToDescription } from './users.service';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -3,6 +3,62 @@
|
||||
<div class="console-app__users-spinner">
|
||||
<mat-spinner />
|
||||
</div>
|
||||
} @else if(selectingExistingUser) {
|
||||
|
||||
<div class="console-app__users">
|
||||
<h1 class="mat-headline-4">Add existing user</h1>
|
||||
|
||||
<p>
|
||||
<button
|
||||
mat-icon-button
|
||||
aria-label="Back to users list"
|
||||
(click)="selectingExistingUser = false"
|
||||
>
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
</p>
|
||||
<h1>Select registrar from which to add a new user</h1>
|
||||
<p>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Registrar</mat-label>
|
||||
<mat-select
|
||||
[(ngModel)]="selectedRegistrarId"
|
||||
name="selectedRegistrarId"
|
||||
(selectionChange)="onRegistrarSelectionChange($event)"
|
||||
>
|
||||
@for (registrar of registrarService.registrars(); track registrar) {
|
||||
<mat-option [value]="registrar.registrarId">{{
|
||||
registrar.registrarId
|
||||
}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</p>
|
||||
@if(usersSelection.length) {
|
||||
<app-users-list
|
||||
[users]="usersSelection"
|
||||
(onSelect)="existingUserSelected($event)"
|
||||
/>
|
||||
<p class="console-app__users-add-existing">
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
aria-label="Add user"
|
||||
(click)="submitExistingUser()"
|
||||
[disabled]="!selectedExistingUser"
|
||||
>
|
||||
Add user
|
||||
</button>
|
||||
<button
|
||||
mat-stroked-button
|
||||
aria-label="Cancel adding existing user"
|
||||
(click)="selectingExistingUser = false"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
} @else if(usersService.currentlyOpenUserEmail()) {
|
||||
<app-user-edit></app-user-edit>
|
||||
} @else {
|
||||
@@ -10,39 +66,31 @@
|
||||
<div class="console-app__users-header">
|
||||
<h1 class="mat-headline-4">Users</h1>
|
||||
<div class="spacer"></div>
|
||||
<button
|
||||
mat-flat-button
|
||||
(click)="createNewUser()"
|
||||
aria-label="Create new user"
|
||||
color="primary"
|
||||
>
|
||||
Create a Viewer User
|
||||
</button>
|
||||
<div class="console-app__users-header-buttons">
|
||||
<button
|
||||
class="console-app__users-header-add"
|
||||
mat-stroked-button
|
||||
(click)="addExistingUser()"
|
||||
aria-label="Create new user"
|
||||
color="primary"
|
||||
>
|
||||
<mat-icon>add</mat-icon>
|
||||
Add existing user
|
||||
</button>
|
||||
<button
|
||||
mat-flat-button
|
||||
(click)="createNewUser()"
|
||||
aria-label="Create new user"
|
||||
color="primary"
|
||||
>
|
||||
Create a Viewer User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-table
|
||||
[dataSource]="dataSource"
|
||||
class="mat-elevation-z0"
|
||||
class="console-app__users-table"
|
||||
matSort
|
||||
>
|
||||
<ng-container
|
||||
*ngFor="let column of columns"
|
||||
[matColumnDef]="column.columnDef"
|
||||
>
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
{{ column.header }}
|
||||
</mat-header-cell>
|
||||
<mat-cell
|
||||
*matCellDef="let row"
|
||||
[innerHTML]="column.cell(row)"
|
||||
></mat-cell>
|
||||
</ng-container>
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
(click)="openDetails(row.emailAddress)"
|
||||
></mat-row>
|
||||
</mat-table>
|
||||
<app-users-list
|
||||
[users]="usersService.users()"
|
||||
(onSelect)="openDetails($event)"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</app-selected-registrar-wrapper>
|
||||
|
||||
@@ -13,26 +13,37 @@
|
||||
// limitations under the License.
|
||||
|
||||
.console-app {
|
||||
&__users {
|
||||
max-width: 1024px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
&__users-spinner {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
$min-width: 756px;
|
||||
$max-width: 1024px;
|
||||
|
||||
&__users-table {
|
||||
min-width: $min-width !important;
|
||||
max-width: $max-width;
|
||||
}
|
||||
|
||||
&__users-new {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
&__users-add-existing {
|
||||
margin-top: 20px;
|
||||
> button {
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
&__users-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
&-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
button {
|
||||
margin: 0 15px 15px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,29 +14,18 @@
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component, effect, ViewChild } from '@angular/core';
|
||||
import { Component, effect } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { SelectedRegistrarModule } from '../app.module';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { SnackBarModule } from '../snackbar.module';
|
||||
import { UserEditComponent } from './userEdit.component';
|
||||
import { roleToDescription, User, UsersService } from './users.service';
|
||||
|
||||
export const columns = [
|
||||
{
|
||||
columnDef: 'emailAddress',
|
||||
header: 'User email',
|
||||
cell: (record: User) => `${record.emailAddress || ''}`,
|
||||
},
|
||||
{
|
||||
columnDef: 'role',
|
||||
header: 'User role',
|
||||
cell: (record: User) => `${roleToDescription(record.role)}`,
|
||||
},
|
||||
];
|
||||
import { User, UsersService } from './users.service';
|
||||
import { UserDataService } from '../shared/services/userData.service';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { UsersListComponent } from './usersList.component';
|
||||
import { MatSelectChange } from '@angular/material/select';
|
||||
|
||||
@Component({
|
||||
selector: 'app-users',
|
||||
@@ -44,41 +33,45 @@ export const columns = [
|
||||
styleUrls: ['./users.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
FormsModule,
|
||||
MaterialModule,
|
||||
SnackBarModule,
|
||||
CommonModule,
|
||||
SelectedRegistrarModule,
|
||||
UsersListComponent,
|
||||
UserEditComponent,
|
||||
],
|
||||
providers: [UsersService],
|
||||
})
|
||||
export class UsersComponent {
|
||||
dataSource: MatTableDataSource<User>;
|
||||
columns = columns;
|
||||
displayedColumns = this.columns.map((c) => c.columnDef);
|
||||
isLoading = false;
|
||||
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
selectingExistingUser = false;
|
||||
selectedRegistrarId = '';
|
||||
usersSelection: User[] = [];
|
||||
selectedExistingUser: User | undefined;
|
||||
|
||||
constructor(
|
||||
protected registrarService: RegistrarService,
|
||||
protected usersService: UsersService,
|
||||
private userDataService: UserDataService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {
|
||||
this.dataSource = new MatTableDataSource<User>(usersService.users());
|
||||
|
||||
effect(() => {
|
||||
if (registrarService.registrarId()) {
|
||||
this.loadUsers();
|
||||
}
|
||||
});
|
||||
effect(() => {
|
||||
this.dataSource.data = usersService.users();
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.dataSource.sort = this.sort;
|
||||
addExistingUser() {
|
||||
this.selectingExistingUser = true;
|
||||
this.selectedRegistrarId = '';
|
||||
this.usersSelection = [];
|
||||
this.selectedExistingUser = undefined;
|
||||
}
|
||||
|
||||
existingUserSelected(user: User) {
|
||||
this.selectedExistingUser = user;
|
||||
}
|
||||
|
||||
loadUsers() {
|
||||
@@ -96,7 +89,7 @@ export class UsersComponent {
|
||||
|
||||
createNewUser() {
|
||||
this.isLoading = true;
|
||||
this.usersService.createNewUser().subscribe({
|
||||
this.usersService.createOrAddNewUser(null).subscribe({
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error || err.message);
|
||||
this.isLoading = false;
|
||||
@@ -107,7 +100,39 @@ export class UsersComponent {
|
||||
});
|
||||
}
|
||||
|
||||
openDetails(emailAddress: string) {
|
||||
this.usersService.currentlyOpenUserEmail.set(emailAddress);
|
||||
openDetails(user: User) {
|
||||
this.usersService.currentlyOpenUserEmail.set(user.emailAddress);
|
||||
}
|
||||
|
||||
onRegistrarSelectionChange(e: MatSelectChange) {
|
||||
if (e.value) {
|
||||
this.usersService.fetchUsersForRegistrar(e.value).subscribe({
|
||||
error: (err) => {
|
||||
this._snackBar.open(err.error || err.message);
|
||||
},
|
||||
next: (users) => {
|
||||
this.usersSelection = users;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
submitExistingUser() {
|
||||
this.isLoading = true;
|
||||
if (this.selectedExistingUser) {
|
||||
this.usersService
|
||||
.createOrAddNewUser(this.selectedExistingUser)
|
||||
.subscribe({
|
||||
error: (err) => {
|
||||
this._snackBar.open(err.error || err.message);
|
||||
this.isLoading = false;
|
||||
},
|
||||
complete: () => {
|
||||
this.isLoading = false;
|
||||
this.selectingExistingUser = false;
|
||||
this.loadUsers();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,10 @@ export class UsersService {
|
||||
private registrarService: RegistrarService
|
||||
) {}
|
||||
|
||||
fetchUsersForRegistrar(registrarId: string) {
|
||||
return this.backendService.getUsers(registrarId);
|
||||
}
|
||||
|
||||
fetchUsers() {
|
||||
return this.backendService
|
||||
.getUsers(this.registrarService.registrarId())
|
||||
@@ -56,14 +60,16 @@ export class UsersService {
|
||||
);
|
||||
}
|
||||
|
||||
createNewUser() {
|
||||
createOrAddNewUser(maybeExistingUser: User | null) {
|
||||
return this.backendService
|
||||
.createUser(this.registrarService.registrarId())
|
||||
.createUser(this.registrarService.registrarId(), maybeExistingUser)
|
||||
.pipe(
|
||||
tap((newUser: User) => {
|
||||
this.users.set([...this.users(), newUser]);
|
||||
this.currentlyOpenUserEmail.set(newUser.emailAddress);
|
||||
this.isNewUser = true;
|
||||
if (newUser) {
|
||||
this.users.set([...this.users(), newUser]);
|
||||
this.currentlyOpenUserEmail.set(newUser.emailAddress);
|
||||
this.isNewUser = true;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
24
console-webapp/src/app/users/usersList.component.html
Normal file
24
console-webapp/src/app/users/usersList.component.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<div class="console-app__users-table-wrapper">
|
||||
<mat-table
|
||||
[dataSource]="dataSource"
|
||||
class="mat-elevation-z0"
|
||||
class="console-app__users-table"
|
||||
matSort
|
||||
>
|
||||
<ng-container
|
||||
*ngFor="let column of columns"
|
||||
[matColumnDef]="column.columnDef"
|
||||
>
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
{{ column.header }}
|
||||
</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></mat-cell>
|
||||
</ng-container>
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
[class.rowSelected]="isRowSelected(row)"
|
||||
(click)="onClick(row)"
|
||||
></mat-row>
|
||||
</mat-table>
|
||||
</div>
|
||||
14
console-webapp/src/app/users/usersList.component.scss
Normal file
14
console-webapp/src/app/users/usersList.component.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
.console-app {
|
||||
&__users-table {
|
||||
min-width: 616px;
|
||||
.rowSelected {
|
||||
background-color: var(--light-highlight);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
&__users-table-wrapper {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
78
console-webapp/src/app/users/usersList.component.ts
Normal file
78
console-webapp/src/app/users/usersList.component.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// 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 { CommonModule } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
effect,
|
||||
EventEmitter,
|
||||
input,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { User, roleToDescription } from './users.service';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
|
||||
export const columns = [
|
||||
{
|
||||
columnDef: 'emailAddress',
|
||||
header: 'User email',
|
||||
cell: (record: User) => `${record.emailAddress || ''}`,
|
||||
},
|
||||
{
|
||||
columnDef: 'role',
|
||||
header: 'User role',
|
||||
cell: (record: User) => `${roleToDescription(record.role)}`,
|
||||
},
|
||||
];
|
||||
|
||||
@Component({
|
||||
selector: 'app-users-list',
|
||||
templateUrl: './usersList.component.html',
|
||||
styleUrls: ['./usersList.component.scss'],
|
||||
standalone: true,
|
||||
imports: [MaterialModule, CommonModule],
|
||||
providers: [],
|
||||
})
|
||||
export class UsersListComponent {
|
||||
columns = columns;
|
||||
displayedColumns = this.columns.map((c) => c.columnDef);
|
||||
dataSource: MatTableDataSource<User>;
|
||||
selectedRow!: User;
|
||||
users = input<User[]>([]);
|
||||
@Output() onSelect = new EventEmitter<User>();
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
|
||||
constructor() {
|
||||
this.dataSource = new MatTableDataSource<User>(this.users());
|
||||
effect(() => {
|
||||
this.dataSource.data = this.users();
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
onClick(row: User) {
|
||||
this.selectedRow = row;
|
||||
this.onSelect.emit(row);
|
||||
}
|
||||
|
||||
isRowSelected(row: User) {
|
||||
return row === this.selectedRow;
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,8 @@ public class ConsoleUserDataAction extends ConsoleApiAction {
|
||||
// auth checks.
|
||||
"isAdmin", user.getUserRoles().isAdmin(),
|
||||
"globalRole", user.getUserRoles().getGlobalRole(),
|
||||
// registrar-specific roles
|
||||
"userRoles", user.getUserRoles().getRegistrarRoles(),
|
||||
// Include static contact resources in this call to minimize round trips
|
||||
"productName", productName,
|
||||
"supportEmail", supportEmail,
|
||||
|
||||
@@ -48,6 +48,8 @@ import google.registry.tools.IamClient;
|
||||
import google.registry.util.StringGenerator;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
@@ -100,7 +102,12 @@ public class ConsoleUsersAction extends ConsoleApiAction {
|
||||
// Temporary flag while testing
|
||||
if (user.getUserRoles().isAdmin()) {
|
||||
checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS);
|
||||
tm().transact(this::runCreateInTransaction);
|
||||
if (userData.isPresent()) { // Adding existing user to registrar
|
||||
tm().transact(this::runAppendUserInTransaction);
|
||||
} else { // Adding new user to registrar
|
||||
tm().transact(this::runCreateInTransaction);
|
||||
}
|
||||
|
||||
} else {
|
||||
consoleApiParams.response().setStatus(SC_FORBIDDEN);
|
||||
}
|
||||
@@ -111,7 +118,7 @@ public class ConsoleUsersAction extends ConsoleApiAction {
|
||||
// Temporary flag while testing
|
||||
if (user.getUserRoles().isAdmin()) {
|
||||
checkPermission(user, registrarId, ConsolePermission.MANAGE_USERS);
|
||||
tm().transact(() -> runUpdateInTransaction());
|
||||
tm().transact(this::runUpdateInTransaction);
|
||||
} else {
|
||||
consoleApiParams.response().setStatus(SC_FORBIDDEN);
|
||||
}
|
||||
@@ -145,21 +152,46 @@ public class ConsoleUsersAction extends ConsoleApiAction {
|
||||
}
|
||||
}
|
||||
|
||||
private void runDeleteInTransaction() throws IOException {
|
||||
if (!isModifyingRequestValid()) {
|
||||
private void runAppendUserInTransaction() {
|
||||
if (!isModifyingRequestValid(false)) {
|
||||
return;
|
||||
}
|
||||
String email = this.userData.get().emailAddress;
|
||||
try {
|
||||
directory.users().delete(email).execute();
|
||||
} catch (IOException e) {
|
||||
setFailedResponse("Failed to delete the user workspace account", SC_INTERNAL_SERVER_ERROR);
|
||||
throw e;
|
||||
ImmutableList<User> allRegistrarUsers = getAllRegistrarUsers(registrarId);
|
||||
if (allRegistrarUsers.size() >= 4)
|
||||
throw new BadRequestException("Total users amount per registrar is limited to 4");
|
||||
|
||||
updateUserRegistrarRoles(
|
||||
this.userData.get().emailAddress,
|
||||
registrarId,
|
||||
RegistrarRole.valueOf(this.userData.get().role),
|
||||
false);
|
||||
consoleApiParams.response().setStatus(SC_OK);
|
||||
}
|
||||
|
||||
private void runDeleteInTransaction() throws IOException {
|
||||
if (!isModifyingRequestValid(true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String email = this.userData.get().emailAddress;
|
||||
User updatedUser =
|
||||
updateUserRegistrarRoles(
|
||||
email, registrarId, RegistrarRole.valueOf(this.userData.get().role), true);
|
||||
|
||||
// User has no registrars assigned
|
||||
if (updatedUser.getUserRoles().getRegistrarRoles().size() == 0) {
|
||||
try {
|
||||
directory.users().delete(email).execute();
|
||||
} catch (IOException e) {
|
||||
setFailedResponse("Failed to delete the user workspace account", SC_INTERNAL_SERVER_ERROR);
|
||||
throw e;
|
||||
}
|
||||
|
||||
VKey<User> key = VKey.create(User.class, email);
|
||||
tm().delete(key);
|
||||
User.revokeIapPermission(email, maybeGroupEmailAddress, cloudTasksUtils, null, iamClient);
|
||||
}
|
||||
|
||||
VKey<User> key = VKey.create(User.class, email);
|
||||
tm().delete(key);
|
||||
User.revokeIapPermission(email, maybeGroupEmailAddress, cloudTasksUtils, null, iamClient);
|
||||
|
||||
consoleApiParams.response().setStatus(SC_OK);
|
||||
}
|
||||
@@ -220,27 +252,19 @@ public class ConsoleUsersAction extends ConsoleApiAction {
|
||||
}
|
||||
|
||||
private void runUpdateInTransaction() {
|
||||
if (!isModifyingRequestValid()) {
|
||||
if (!isModifyingRequestValid(true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
UserData userData = this.userData.get();
|
||||
UserRoles userRoles =
|
||||
new UserRoles.Builder()
|
||||
.setRegistrarRoles(ImmutableMap.of(registrarId, RegistrarRole.valueOf(userData.role)))
|
||||
.build();
|
||||
User updatedUser =
|
||||
tm().loadByKeyIfPresent(VKey.create(User.class, userData.emailAddress))
|
||||
.get()
|
||||
.asBuilder()
|
||||
.setUserRoles(userRoles)
|
||||
.build();
|
||||
|
||||
tm().put(updatedUser);
|
||||
updateUserRegistrarRoles(
|
||||
this.userData.get().emailAddress,
|
||||
registrarId,
|
||||
RegistrarRole.valueOf(this.userData.get().role),
|
||||
false);
|
||||
consoleApiParams.response().setStatus(SC_OK);
|
||||
}
|
||||
|
||||
private boolean isModifyingRequestValid() {
|
||||
private boolean isModifyingRequestValid(boolean verifyAccess) {
|
||||
if (userData.isEmpty()
|
||||
|| isNullOrEmpty(userData.get().emailAddress)
|
||||
|| isNullOrEmpty(userData.get().role)) {
|
||||
@@ -252,7 +276,7 @@ public class ConsoleUsersAction extends ConsoleApiAction {
|
||||
.orElseThrow(
|
||||
() -> new BadRequestException(String.format("User %s doesn't exist", email)));
|
||||
|
||||
if (!userToUpdate.getUserRoles().getRegistrarRoles().containsKey(registrarId)) {
|
||||
if (verifyAccess && !userToUpdate.getUserRoles().getRegistrarRoles().containsKey(registrarId)) {
|
||||
setFailedResponse(
|
||||
String.format("Can't update user not associated with registrarId %s", registrarId),
|
||||
SC_FORBIDDEN);
|
||||
@@ -261,6 +285,36 @@ public class ConsoleUsersAction extends ConsoleApiAction {
|
||||
return true;
|
||||
}
|
||||
|
||||
private User updateUserRegistrarRoles(
|
||||
String email, String registrarId, RegistrarRole newRole, boolean isDelete) {
|
||||
User userToUpdate = tm().loadByKeyIfPresent(VKey.create(User.class, email)).get();
|
||||
Map<String, RegistrarRole> updatedRegistrarRoles;
|
||||
if (isDelete) {
|
||||
updatedRegistrarRoles =
|
||||
userToUpdate.getUserRoles().getRegistrarRoles().entrySet().stream()
|
||||
.filter(entry -> !Objects.equals(entry.getKey(), registrarId))
|
||||
.collect(ImmutableMap.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
} else {
|
||||
updatedRegistrarRoles =
|
||||
ImmutableMap.<String, RegistrarRole>builder()
|
||||
.putAll(userToUpdate.getUserRoles().getRegistrarRoles())
|
||||
.put(registrarId, newRole)
|
||||
.buildKeepingLast();
|
||||
}
|
||||
var updatedUser =
|
||||
userToUpdate
|
||||
.asBuilder()
|
||||
.setUserRoles(
|
||||
userToUpdate
|
||||
.getUserRoles()
|
||||
.asBuilder()
|
||||
.setRegistrarRoles(updatedRegistrarRoles)
|
||||
.build())
|
||||
.build();
|
||||
tm().put(updatedUser);
|
||||
return updatedUser;
|
||||
}
|
||||
|
||||
private ImmutableList<User> getAllRegistrarUsers(String registrarId) {
|
||||
return tm().transact(
|
||||
() ->
|
||||
|
||||
@@ -20,6 +20,7 @@ import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.gson.Gson;
|
||||
import google.registry.model.console.User;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
@@ -71,6 +72,8 @@ class ConsoleUserDataActionTest {
|
||||
GSON.fromJson(((FakeResponse) consoleApiParams.response()).getPayload(), Map.class);
|
||||
assertThat(jsonObject)
|
||||
.containsExactly(
|
||||
"userRoles",
|
||||
ImmutableMap.of(),
|
||||
"isAdmin",
|
||||
true,
|
||||
"technicalDocsUrl",
|
||||
|
||||
@@ -246,6 +246,50 @@ class ConsoleUsersActionTest {
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_removesRole() throws IOException {
|
||||
User user1 = DatabaseHelper.loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
AuthResult authResult =
|
||||
AuthResult.createUser(
|
||||
user1
|
||||
.asBuilder()
|
||||
.setUserRoles(user1.getUserRoles().asBuilder().setIsAdmin(true).build())
|
||||
.build());
|
||||
DatabaseHelper.persistResource(
|
||||
new User.Builder()
|
||||
.setEmailAddress("test4@test.com")
|
||||
.setUserRoles(
|
||||
new UserRoles()
|
||||
.asBuilder()
|
||||
.setRegistrarRoles(
|
||||
ImmutableMap.of(
|
||||
"TheRegistrar",
|
||||
RegistrarRole.PRIMARY_CONTACT,
|
||||
"SomeRegistrar",
|
||||
RegistrarRole.PRIMARY_CONTACT))
|
||||
.build())
|
||||
.build());
|
||||
|
||||
ConsoleUsersAction action =
|
||||
createAction(
|
||||
Optional.of(ConsoleApiParamsUtils.createFake(authResult)),
|
||||
Optional.of("DELETE"),
|
||||
Optional.of(
|
||||
new UserData("test4@test.com", RegistrarRole.ACCOUNT_MANAGER.toString(), null)));
|
||||
|
||||
action.cloudTasksUtils = cloudTasksHelper.getTestCloudTasksUtils();
|
||||
when(directory.users()).thenReturn(users);
|
||||
when(users.delete(any(String.class))).thenReturn(delete);
|
||||
action.run();
|
||||
var response = ((FakeResponse) consoleApiParams.response());
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
Optional<User> actualUser =
|
||||
DatabaseHelper.loadByKeyIfPresent(VKey.create(User.class, "test4@test.com"));
|
||||
assertThat(actualUser).isPresent();
|
||||
assertThat(actualUser.get().getUserRoles().getRegistrarRoles().containsKey("TheRegistrar"))
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFailure_limitedTo4UsersPerRegistrar() throws IOException {
|
||||
User user1 = DatabaseHelper.loadByKey(VKey.create(User.class, "test1@test.com"));
|
||||
|
||||
Reference in New Issue
Block a user