Skip to content

Commit eaff1a8

Browse files
committed
group membership
1 parent b746f79 commit eaff1a8

15 files changed

Lines changed: 274 additions & 67 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {Then, When} from "@cucumber/cucumber";
2+
import {SdkWorld} from "../support/world";
3+
import {expect} from "chai";
4+
5+
When('I ask for the feature dashboard', async function() {
6+
const world = this as SdkWorld;
7+
expect(world.application.environments.length, `Application ${world.application.name} has no environments`).to.be.gt(0);
8+
const data = await world.featureApi.findAllFeatureAndFeatureValuesForEnvironmentsByApplication(world.application.id, world.application.environments.map(e => e.id));
9+
expect(data.status).to.eq(200);
10+
world.dashboard = data.data;
11+
});
12+
13+
Then('I should see an empty list of features and {int} environment', async function (envCount: number) {
14+
const world = this as SdkWorld;
15+
expect(world.dashboard.environments.length).to.eq(1);
16+
expect(world.dashboard.features.length).to.eq(0);
17+
});

adks/e2e-sdk/app/steps/group.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ When('I get the portfolio admin group', async function() {
2222
const groups = await world.superuser.groupApi.findGroups(world.portfolio.id);
2323
const adminGroup = groups.data.find(g => g.admin);
2424
expect(adminGroup, `${JSON.stringify(groups.data)} - could not find admin group!`).to.not.be.undefined;
25-
world.group = adminGroup;
25+
const fullGroup = await world.superuser.groupApi.getGroup(adminGroup.id, false, true);
26+
world.group = fullGroup.data;
2627
});
2728

2829
When('I create a new group with application roles {string}', async function(roles: string) {
@@ -41,6 +42,34 @@ When('I create a new group with application roles {string}', async function(role
4142
world.group = response.data;
4243
});
4344

45+
When('I assign the superuser to the group', async function() {
46+
const world = this as SdkWorld;
47+
const userResponse = await world.superuser.personApi.getPerson("self");
48+
const userId = (userResponse.data as Person).id.id;
49+
const response = await world.superuser.groupApi.addPersonToGroup(world.group.id,
50+
userId, true);
51+
expect(response.status).to.eq(200);
52+
expect(response.data.members.find( p => p.id.id === userId)).to.not.be.undefined;
53+
});
54+
55+
Then('I can delete the supergroup from the group', async function() {
56+
const world = this as SdkWorld;
57+
const userResponse = await world.superuser.personApi.getPerson("self");
58+
const userId = (userResponse.data as Person).id.id;
59+
const response = await world.superuser.groupApi.deletePersonFromGroup(world.group.id, userId, true);
60+
expect(response.status).to.eq(200);
61+
expect(response.data.members.find( p => p.id.id === userId)).to.be.undefined;
62+
});
63+
64+
Then(/^the (superuser|user) is marked in the group as a (superuser|user)$/, async function(userSource: string, userType: string) {
65+
const world = this as SdkWorld;
66+
const user = (userSource === 'superuser') ? world.superuser : world.user;
67+
expect(user.me).to.not.be.undefined;
68+
expect(world.group).to.not.be.undefined;
69+
const suserValue = userType === 'superuser';
70+
expect(world.group.sMembers.find(p => p.superuser === suserValue && p.person.id === user.personId))
71+
});
72+
4473
Then('I assign the new user to the new group', async function() {
4574
const world = this as SdkWorld;
4675
expect(world.user).to.not.be.undefined;

adks/e2e-sdk/app/steps/setup.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import {expect} from 'chai';
22
import {Given, Then, When} from '@cucumber/cucumber';
33
import {
4-
Application, CreateApplication,
4+
CreateApplication,
55
CreatePortfolio,
66
Environment,
77
PortfolioServiceApi,
8-
RoleType,
9-
ServiceAccount,
108
ServiceAccountPermission,
11-
ServiceAccountServiceApi,
129
UpdateEnvironment
1310
} from '../apis/mr-service';
1411
import {makeid, sleep} from '../support/random';

adks/e2e-sdk/app/support/world.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {setDefaultTimeout, setWorldConstructor, World} from '@cucumber/cucumber';
22
import {
3-
Application,
3+
Application, ApplicationFeatureValues,
44
ApplicationRolloutStrategyServiceApi,
55
ApplicationServiceApi,
66
AuthServiceApi,
@@ -69,6 +69,7 @@ export class ApiUser {
6969
public readonly applicationStrategyApi: ApplicationRolloutStrategyServiceApi;
7070
public readonly featureFilterApi: FeatureFilterServiceApi;
7171
public readonly apiKey: string;
72+
public me: Person;
7273

7374
public serviceAccounts: Array<ServiceAccount> = [];
7475

@@ -80,6 +81,7 @@ export class ApiUser {
8081

8182
this.portfolioApi = new PortfolioServiceApi(this.adminApiConfig);
8283
this.personApi = new PersonServiceApi(this.adminApiConfig);
84+
8385
this.applicationApi = new ApplicationServiceApi(this.adminApiConfig);
8486
this.environmentApi = new EnvironmentServiceApi(this.adminApiConfig);
8587
this.environment2Api = new Environment2ServiceApi(this.adminApiConfig);
@@ -97,7 +99,21 @@ export class ApiUser {
9799
this.applicationStrategyApi = new ApplicationRolloutStrategyServiceApi(this.adminApiConfig);
98100
this.featureHistoryApi = new FeatureHistoryServiceApi(this.adminApiConfig);
99101
this.featureFilterApi = new FeatureFilterServiceApi(this.adminApiConfig);
102+
103+
if (apiKey) {
104+
this.refreshPerson();
105+
}
106+
}
107+
108+
public refreshPerson() {
109+
this.personApi.getPerson('self').then((person) => {
110+
this.me = person.data;
111+
}).catch((_) => {
112+
113+
});
100114
}
115+
116+
public get personId() { return this.me.id.id; }
101117
}
102118

103119
export class SdkWorld extends World {
@@ -129,6 +145,7 @@ export class SdkWorld extends World {
129145
public readonly superuser: ApiUser;
130146
public user: ApiUser | undefined;
131147
public currentUser: ApiUser;
148+
public dashboard: ApplicationFeatureValues | undefined;
132149

133150
constructor(props: any) {
134151
super(props);
@@ -269,6 +286,8 @@ export class SdkWorld extends World {
269286

270287
public set apiKey(val: TokenizedPerson) {
271288
this.adminApiConfig.accessToken = val.accessToken;
289+
this.superuser.refreshPerson();
290+
this.superuser.me = val.person;
272291
this.person = val.person;
273292
apiKey = val.accessToken;
274293
logger.info('Successfully logged in');
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
@allvariants @streamingvariants
2+
Feature: Certain permission combinations should work as expected
3+
4+
@portfolio-group
5+
Scenario: The group should be able to identify who is a superuser and who is not
6+
Given I create a new portfolio
7+
And I get the portfolio admin group
8+
And I create a new user
9+
And I assign the new user to the new group
10+
And I get the portfolio admin group
11+
And the superuser is marked in the group as a superuser
12+
And the user is marked in the group as a user
13+
14+
@delete-superuser-from-group
15+
Scenario: I add the superuser to the portfolio admin group and then remove them
16+
Given I create a new portfolio
17+
And I get the portfolio admin group
18+
When I assign the superuser to the group
19+
Then I can delete the supergroup from the group

admin-frontend/open_admin_app/lib/routes/manage_group_route.dart

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,13 @@ class ManageGroupRouteState extends State<ManageGroupRoute> {
110110
stream: bloc.groupLoaded,
111111
builder: (context, snapshot) {
112112
if (snapshot.hasData) {
113+
114+
var group = snapshot.data!;
113115
return Column(
114116
crossAxisAlignment: CrossAxisAlignment.start,
115117
children: <Widget>[
116118
if (bloc.mrClient
117-
.isPortfolioOrSuperAdmin(snapshot.data!.portfolioId))
119+
.isPortfolioOrSuperAdmin(group.portfolioId))
118120
Padding(
119121
padding: const EdgeInsets.all(8.0),
120122
child: FilledButton.icon(
@@ -125,7 +127,7 @@ class ManageGroupRouteState extends State<ManageGroupRoute> {
125127
.addOverlay((BuildContext context) {
126128
return AddMembersDialogWidget(
127129
bloc: bloc,
128-
group: snapshot.data!,
130+
group: group,
129131
);
130132
}),
131133
),
@@ -143,22 +145,22 @@ class ManageGroupRouteState extends State<ManageGroupRoute> {
143145
label: Text(
144146
AppLocalizations.of(context)!.columnName),
145147
onSort: (columnIndex, ascending) {
146-
onSortColumn(snapshot.data!.members,
148+
onSortColumn(group.sMembers,
147149
columnIndex, ascending);
148150
}),
149151
DataColumn(
150152
label: Text(
151153
AppLocalizations.of(context)!.columnEmail),
152154
onSort: (columnIndex, ascending) {
153-
onSortColumn(snapshot.data!.members,
155+
onSortColumn(group.sMembers,
154156
columnIndex, ascending);
155157
},
156158
),
157159
DataColumn(
158160
label: Text(AppLocalizations.of(context)!
159161
.columnMemberType),
160162
onSort: (columnIndex, ascending) {
161-
onSortColumn(snapshot.data!.members,
163+
onSortColumn(group.sMembers,
162164
columnIndex, ascending);
163165
},
164166
),
@@ -171,24 +173,22 @@ class ManageGroupRouteState extends State<ManageGroupRoute> {
171173
onSort: (i, a) => {}),
172174
],
173175
rows: [
174-
for (Person member in snapshot.data!.members)
176+
for (GroupPerson member in group.sMembers)
175177
DataRow(cells: [
176178
DataCell(
177-
Text(member.name ?? ''),
179+
Text(member.person.name ?? ''),
178180
),
179181
DataCell(Text(
180-
member.personType == PersonType.person
181-
? member.email!
182+
member.person.type == PersonType.person
183+
? member.person.email!
182184
: "")),
183185
DataCell(Text(
184-
member.personType == PersonType.person
186+
member.person.type == PersonType.person
185187
? AppLocalizations.of(context)!
186188
.memberTypeUser
187189
: AppLocalizations.of(context)!
188190
.memberTypeServiceAccount)),
189-
DataCell(bloc.mrClient
190-
.isPortfolioOrSuperAdmin(
191-
snapshot.data!.portfolioId)
191+
DataCell(!member.superuser
192192
? Tooltip(
193193
message: AppLocalizations.of(context)!
194194
.removeFromGroup,
@@ -197,14 +197,14 @@ class ManageGroupRouteState extends State<ManageGroupRoute> {
197197
onPressed: () async {
198198
try {
199199
await bloc.removeFromGroup(
200-
snapshot.data!, member);
200+
group, member.person.id);
201201
if (context.mounted) {
202202
bloc.mrClient.addSnackbar(
203203
Text(AppLocalizations.of(
204204
context)!
205205
.memberRemovedFromGroup(
206-
member.name ?? '',
207-
snapshot.data!
206+
member.person.name,
207+
group
208208
.name)));
209209
}
210210
} catch (e, s) {
@@ -230,39 +230,39 @@ class ManageGroupRouteState extends State<ManageGroupRoute> {
230230
);
231231
}
232232

233-
void onSortColumn(List<Person> people, int columnIndex, bool ascending) {
233+
void onSortColumn(List<GroupPerson> people, int columnIndex, bool ascending) {
234234
setState(() {
235235
if (columnIndex == 0) {
236236
if (ascending) {
237237
people.sort((a, b) {
238-
return a.name!.toLowerCase().compareTo(b.name!.toLowerCase());
238+
return a.person.name.toLowerCase().compareTo(b.person.name!.toLowerCase());
239239
});
240240
} else {
241241
people.sort((a, b) {
242-
return b.name!.toLowerCase().compareTo(a.name!.toLowerCase());
242+
return b.person.name.toLowerCase().compareTo(a.person.name.toLowerCase());
243243
});
244244
}
245245
}
246246
if (columnIndex == 1) {
247247
if (ascending) {
248248
people.sort((a, b) =>
249-
a.email!.toLowerCase().compareTo(b.email!.toLowerCase()));
249+
a.person.email!.toLowerCase().compareTo(b.person.email!.toLowerCase()));
250250
} else {
251251
people.sort((a, b) =>
252-
b.email!.toLowerCase().compareTo(a.email!.toLowerCase()));
252+
b.person.email!.toLowerCase().compareTo(a.person.email!.toLowerCase()));
253253
}
254254
}
255255
if (columnIndex == 2) {
256256
if (ascending) {
257-
people.sort((a, b) => a.personType
257+
people.sort((a, b) => a.person.type
258258
.toString()
259259
.toLowerCase()
260-
.compareTo(b.personType.toString().toLowerCase()));
260+
.compareTo(b.person.type.toString().toLowerCase()));
261261
} else {
262-
people.sort((a, b) => b.personType
262+
people.sort((a, b) => b.person.type
263263
.toString()
264264
.toLowerCase()
265-
.compareTo(a.personType.toString().toLowerCase()));
265+
.compareTo(a.person.type.toString().toLowerCase()));
266266
}
267267
}
268268
if (sortColumnIndex == columnIndex) {
@@ -430,10 +430,8 @@ class _AddMembersDialogWidgetState extends State<AddMembersDialogWidget> {
430430
keepCase: true,
431431
onPressed: () async {
432432
final group = widget.group;
433-
group.members = List.from(group.members)..addAll(membersToAdd);
434-
// remove duplicates
435-
group.members = group.members.toSet().toList();
436-
final success = await widget.bloc.updateGroup(group);
433+
434+
final success = await widget.bloc.addUsersToGroup(group, membersToAdd);
437435
if (success) {
438436
widget.bloc.mrClient.removeOverlay();
439437
if (context.mounted) {

admin-frontend/open_admin_app/lib/widgets/group/group_bloc.dart

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class GroupBloc implements Bloc {
5656
if (groupId != null && groupId.length > 1) {
5757
try {
5858
final fetchedGroup =
59-
await _groupServiceApi.getGroup(groupId, includeMembers: true);
59+
await _groupServiceApi.getGroup(groupId, includeMembersV2: true);
6060
// publish it out...
6161
group = fetchedGroup;
6262
_groupSource.add(fetchedGroup);
@@ -69,7 +69,7 @@ class GroupBloc implements Bloc {
6969
Future<void> deleteGroup(String groupId, bool includeMembers) async {
7070
try {
7171
await _groupServiceApi.deleteGroup(groupId,
72-
includeMembers: includeMembers);
72+
includeMembersV2: includeMembers);
7373
group = null;
7474
this.groupId = null;
7575
_groupSource.add(null);
@@ -79,24 +79,42 @@ class GroupBloc implements Bloc {
7979
}
8080
}
8181

82-
Future<void> removeFromGroup(Group group, Person person) async {
82+
Future<void> removeFromGroup(Group group, String personId) async {
8383
var data = await _groupServiceApi
84-
.deletePersonFromGroup(group.id, person.id!.id, includeMembers: true);
84+
.deletePersonFromGroup(group.id, personId, includeMembersV2: true);
8585
if (!_groupSource.isClosed) {
8686
_groupSource.add(data);
8787
}
8888
}
8989

90-
Future<bool> updateGroup(Group groupToUpdate, {String? name}) async {
90+
Future<bool> addUsersToGroup(Group group, List<Person> persons) async {
9191
try {
92-
if (name != null) {
93-
groupToUpdate.name = name;
94-
}
92+
final fetchedGroup = await _groupServiceApi.addPersonsToGroup(group.id, personId: persons.map((p) => p.id!.id).toList(), includeMembersV2: true);
93+
group = fetchedGroup;
94+
_groupSource.add(fetchedGroup);
95+
return true;
96+
} catch (e, s) {
97+
await mrClient.dialogError(e, s,
98+
messageTitle: 'Failed to add users to group',
99+
messageBody:
100+
'Failed to update group because of a duplicate or other conflict.');
101+
return false;
102+
}
103+
}
104+
105+
Future<bool> updateGroupName(Group groupToUpdate, String name) async {
106+
try {
107+
groupToUpdate.name = name;
108+
95109
final newGroup = await _groupServiceApi.updateGroupOnPortfolio(
96110
mrClient.currentPortfolio!.id, groupToUpdate,
97-
includeMembers: true, updateMembers: true);
111+
includeMembersV2: true, updateMembers: false);
112+
113+
// tell the portfolio groups to update as the name has changed.
98114
await getGroups(focusGroup: groupToUpdate);
115+
99116
group = newGroup;
117+
_groupSource.add(newGroup);
100118
groupId = groupToUpdate.id;
101119
return true;
102120
} catch (e, s) {

admin-frontend/open_admin_app/lib/widgets/group/group_update_widget.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ class _GroupUpdateDialogWidgetState extends State<GroupUpdateDialogWidget> {
100100

101101
Future<void> _callUpdateGroup(String name) {
102102
final groupName = name.trim();
103-
return widget.group == null ? widget.bloc.createGroup(groupName) : widget.bloc.updateGroup(widget.group!, name: groupName);
103+
return widget.group == null ? widget.bloc.createGroup(groupName) : widget.bloc.updateGroupName(widget.group!, groupName);
104104
}
105105
}
106106

0 commit comments

Comments
 (0)