Skip to content

Commit 8d7abba

Browse files
authored
b/433135671 Enable groups for GKE RBAC (#731)
* Introduce `gkeEnabled` to control whether a group should be usable for GKE RBAC * Automatically add (or remove) the group to `gke-security-groups` to expose it to GKE * Relax access settings for GKE-enabled groups to comply with the GKE requirements
1 parent 0511ac8 commit 8d7abba

16 files changed

Lines changed: 535 additions & 137 deletions

File tree

doc/site/sources/docs/policy-reference.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ A JIT group represents a job function or role and bundles all access that's requ
151151
perform this job function or role.
152152

153153

154-
```yaml hl_lines="7-12"
154+
```yaml hl_lines="7-13"
155155
...
156156
environment:
157157
...
@@ -160,6 +160,7 @@ environment:
160160
groups:
161161
- name: "datamart-admins"
162162
description: "Admin-level access to data and stuff"
163+
gkeEnabled: true
163164
access: []
164165
constraints:
165166
join: []
@@ -182,6 +183,15 @@ environment:
182183
: Text that describes the purpose of the JIT group. The text is shown in the user interface and
183184
is for informational purposes only.
184185

186+
`gkeEnabled` **Optional** (Default: `false`)
187+
188+
: When set to `true`, JIT Groups configures the group so that it can be used for
189+
[Google Kubernetes Engine RBAC](https://cloud.google.com/kubernetes-engine/docs/how-to/google-groups-rbac):
190+
191+
+ The Cloud Identity group's access settings are relaxed so that members of the group are
192+
allowed to list all group members.
193+
+ The Cloud Identity group is added to `gke-security-groups`.
194+
185195
`access` **Optional**
186196

187197
: [Access control list](#access-control-list) (ACL) for the JIT Group. The ACL controls

sources/src/main/java/com/google/solutions/jitaccess/apis/clients/CloudIdentityGroupsClient.java

Lines changed: 127 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
import com.google.solutions.jitaccess.auth.IamPrincipalId;
3434
import com.google.solutions.jitaccess.common.Coalesce;
3535
import jakarta.inject.Singleton;
36+
import org.crac.Resource;
3637
import org.jetbrains.annotations.NotNull;
38+
import org.jetbrains.annotations.Nullable;
3739

3840
import java.io.IOException;
3941
import java.time.Instant;
@@ -61,8 +63,8 @@ public class CloudIdentityGroupsClient {
6163
private final @NotNull HttpTransport.Options httpOptions;
6264

6365
/**
64-
* Settings for new groups:
65-
* <p>
66+
* Default, restrictive access settings:
67+
*
6668
* - Allow external members.
6769
* - Disable most self-service features on groups.google.com to
6870
* the extent possible.
@@ -87,6 +89,12 @@ public class CloudIdentityGroupsClient {
8789
.setWhoCanViewGroup("ALL_MANAGERS_CAN_VIEW")
8890
.setWhoCanViewMembership("ALL_MANAGERS_CAN_VIEW");
8991

92+
/**
93+
* GKE compatible access settings.
94+
*/
95+
private final @NotNull Groups GKE_COMPATIBLE = RESTRICTED_SETTINGS
96+
.setWhoCanViewMembership("ALL_MEMBERS_CAN_VIEW");
97+
9098
public CloudIdentityGroupsClient(
9199
@NotNull GoogleCredentials credentials,
92100
@NotNull Options options,
@@ -148,21 +156,27 @@ private static void translateAndThrowApiException(
148156
}
149157
}
150158

151-
152159
/**
153160
* Update group settings to restrictive defaults.
154161
*/
155-
private void restrictGroupSettings(@NotNull GroupId emailAddress) throws IOException {
162+
private void setGroupAccess(
163+
@NotNull GroupId emailAddress,
164+
@NotNull AccessProfile profile
165+
) throws IOException {
156166
var settingsClient = createSettingsClient();
157167

168+
var settings = profile == AccessProfile.GkeCompatible
169+
? this.GKE_COMPATIBLE
170+
: this.RESTRICTED_SETTINGS;
171+
158172
//
159173
// The group settings API is prone to fail for newly created groups.
160174
//
161175
for (int attempt = 0; attempt < MAX_GROUP_SETTINGS_PATCH_ATTEMPTS; attempt++) {
162176
try {
163177
settingsClient
164178
.groups()
165-
.update(emailAddress.email, this.RESTRICTED_SETTINGS)
179+
.update(emailAddress.email, settings)
166180
.execute();
167181

168182
//
@@ -271,29 +285,15 @@ private void restrictGroupSettings(@NotNull GroupId emailAddress) throws IOExcep
271285
return lookupGroup(createClient(), groupId);
272286
}
273287

274-
public enum GroupType {
275-
/**
276-
* Normal group. Creating this type of group doesn't require special
277-
* privileges.
278-
*/
279-
DiscussionForum,
280-
281-
/**
282-
* Security group. Creating this type of group requires the 'Groups Admin'
283-
* admin role, or an equivalent custom role that has the privilege to
284-
* assign security labels.
285-
*/
286-
Security
287-
}
288-
289288
/**
290289
* Create group in an idempotent way.
291290
*/
292291
public @NotNull GroupKey createGroup(
293292
@NotNull GroupId emailAddress,
294293
@NotNull GroupType type,
295294
@NotNull String displayName,
296-
@NotNull String description
295+
@NotNull String description,
296+
@NotNull AccessProfile accessProfile
297297
) throws AccessException, IOException {
298298
try {
299299
var labels = new HashMap<String, String>();
@@ -372,7 +372,7 @@ public enum GroupType {
372372
//
373373
// Lock down group settings.
374374
//
375-
restrictGroupSettings(emailAddress);
375+
setGroupAccess(emailAddress, accessProfile);
376376

377377
return groupKey;
378378
}
@@ -515,11 +515,12 @@ public void deleteGroup(
515515
/**
516516
* Delete a group membership in an idempotent way.
517517
*/
518-
public void deleteMembership(
518+
private void deleteMembership(
519+
@NotNull CloudIdentity client,
519520
@NotNull MembershipId membershipId
520521
) throws AccessException, IOException {
521522
try {
522-
createClient()
523+
client
523524
.groups()
524525
.memberships()
525526
.delete(membershipId.id)
@@ -537,6 +538,46 @@ public void deleteMembership(
537538
}
538539
}
539540

541+
/**
542+
* Delete a group membership in an idempotent way.
543+
*/
544+
public void deleteMembership(
545+
@NotNull MembershipId membershipId
546+
) throws AccessException, IOException {
547+
deleteMembership(createClient(), membershipId);
548+
}
549+
550+
/**
551+
* Delete a group membership in an idempotent way.
552+
*/
553+
public void deleteMembership(
554+
@NotNull GroupKey groupKey,
555+
@NotNull IamPrincipalId member
556+
) throws AccessException, IOException {
557+
var client = createClient();
558+
559+
//
560+
// Lookup membership, assuming it exists.
561+
//
562+
MembershipId membershipId;
563+
try
564+
{
565+
membershipId = lookupGroupMembership(
566+
client,
567+
groupKey,
568+
member);
569+
}
570+
catch (AccessException e)
571+
{
572+
//
573+
// Membership doesn't exist, so there's nothing to delete.
574+
//
575+
return;
576+
}
577+
578+
deleteMembership(client, membershipId);
579+
}
580+
540581
private @NotNull MembershipId updateMembership(
541582
@NotNull CloudIdentity client,
542583
@NotNull GroupKey groupKey,
@@ -572,15 +613,18 @@ public void deleteMembership(
572613
@NotNull CloudIdentity client,
573614
@NotNull GroupKey groupKey,
574615
@NotNull IamPrincipalId member,
575-
@NotNull Instant expiry
616+
@Nullable Instant expiry
576617
) throws AccessException, IOException {
577618
var role = new MembershipRole()
578-
.setName("MEMBER")
579-
.setExpiryDetail(new ExpiryDetail()
619+
.setName("MEMBER");
620+
621+
if (expiry != null) {
622+
role.setExpiryDetail(new ExpiryDetail()
580623
.setExpireTime(expiry
581624
.atOffset(ZoneOffset.UTC)
582625
.truncatedTo(ChronoUnit.SECONDS)
583626
.format(DateTimeFormatter.ISO_DATE_TIME)));
627+
}
584628

585629
try {
586630
//
@@ -619,6 +663,31 @@ public void deleteMembership(
619663
}
620664
}
621665

666+
/**
667+
* Permanently add a member to a group in an idempotent way.
668+
*/
669+
public @NotNull MembershipId addPermanentMembership(
670+
@NotNull GroupKey groupKey,
671+
@NotNull IamPrincipalId member
672+
) throws AccessException, IOException {
673+
return addMembership(createClient(), groupKey, member, null);
674+
}
675+
676+
/**
677+
* Permanently add a member to a group in an idempotent way.
678+
*/
679+
public @NotNull MembershipId addPermanentMembership(
680+
@NotNull GroupId groupId,
681+
@NotNull IamPrincipalId member
682+
) throws AccessException, IOException {
683+
var client = createClient();
684+
return addMembership(
685+
client,
686+
lookupGroup(client, groupId),
687+
member,
688+
null);
689+
}
690+
622691
/**
623692
* Add a member to a group in an idempotent way.
624693
*/
@@ -627,6 +696,7 @@ public void deleteMembership(
627696
@NotNull IamPrincipalId member,
628697
@NotNull Instant expiry
629698
) throws AccessException, IOException {
699+
Preconditions.checkNotNull(expiry, "expiry");
630700
return addMembership(createClient(), groupKey, member, expiry);
631701
}
632702

@@ -638,6 +708,8 @@ public void deleteMembership(
638708
@NotNull IamPrincipalId member,
639709
@NotNull Instant expiry
640710
) throws AccessException, IOException {
711+
Preconditions.checkNotNull(expiry, "expiry");
712+
641713
var client = createClient();
642714
return addMembership(
643715
client,
@@ -833,9 +905,36 @@ public void deleteMembership(
833905
}
834906

835907
//---------------------------------------------------------------------------
836-
// Inner classes.
908+
// Inner types.
837909
//---------------------------------------------------------------------------
838910

911+
public enum GroupType {
912+
/**
913+
* Normal group. Creating this type of group doesn't require special
914+
* privileges.
915+
*/
916+
DiscussionForum,
917+
918+
/**
919+
* Security group. Creating this type of group requires the 'Groups Admin'
920+
* admin role, or an equivalent custom role that has the privilege to
921+
* assign security labels.
922+
*/
923+
Security
924+
}
925+
926+
public enum AccessProfile {
927+
/**
928+
* Use restrictive access settings.
929+
*/
930+
Restricted,
931+
932+
/**
933+
* Use access settings that are restrictive, but still compatible GKE RBAC.
934+
*/
935+
GkeCompatible
936+
}
937+
839938
public record MembershipId(String id) {}
840939

841940
public record Options(

sources/src/main/java/com/google/solutions/jitaccess/catalog/legacy/LegacyPolicy.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ private RolePolicy(
436436
acl,
437437
constraints,
438438
privileges,
439+
false,
439440
NAME_MAX_LENGTH);
440441
}
441442

sources/src/main/java/com/google/solutions/jitaccess/catalog/policy/JitGroupPolicy.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,17 @@
3838
public class JitGroupPolicy extends AbstractPolicy {
3939
static final String NAME_PATTERN = "^[a-zA-Z0-9\\-]+$";
4040
static final int NAME_MAX_LENGTH = 24;
41+
4142
private final @NotNull List<Privilege> privileges;
43+
private final boolean gkeEnabled;
4244

4345
protected JitGroupPolicy(
4446
@NotNull String name,
4547
@NotNull String description,
4648
@Nullable AccessControlList acl,
4749
@NotNull Map<ConstraintClass, Collection<Constraint>> constraints,
4850
@NotNull List<Privilege> privileges,
51+
boolean gkeEnabled,
4952
int maxNameLength
5053
) {
5154
super(name, description, acl, constraints);
@@ -60,31 +63,33 @@ protected JitGroupPolicy(
6063
Preconditions.checkNotNull(privileges, "Privileges must not be null");
6164

6265
this.privileges = privileges;
66+
this.gkeEnabled = gkeEnabled;
6367
}
6468

6569
public JitGroupPolicy(
6670
@NotNull String name,
6771
@NotNull String description,
6872
@Nullable AccessControlList acl,
6973
@NotNull Map<ConstraintClass, Collection<Constraint>> constraints,
70-
@NotNull List<Privilege> privileges
74+
@NotNull List<Privilege> privileges,
75+
boolean gkeEnabled
7176
) {
72-
this(name, description, acl, constraints, privileges, NAME_MAX_LENGTH);
77+
this(name, description, acl, constraints, privileges, gkeEnabled, NAME_MAX_LENGTH);
7378
}
7479

7580
public JitGroupPolicy(
7681
@NotNull String name,
7782
@NotNull String description,
7883
@Nullable AccessControlList acl
7984
) {
80-
this(name, description, acl, Map.of(), List.of());
85+
this(name, description, acl, Map.of(), List.of(), false);
8186
}
8287

8388
public JitGroupPolicy(
8489
@NotNull String name,
8590
@NotNull String description
8691
) {
87-
this(name, description, null, Map.of(), List.of());
92+
this(name, description, null, Map.of(), List.of(), false);
8893
}
8994

9095
/**
@@ -112,6 +117,13 @@ public JitGroupPolicy(
112117
return this.privileges;
113118
}
114119

120+
/**
121+
* Indicates if this group can be used for GKE RBAC.
122+
*/
123+
public boolean isGkeEnabled() {
124+
return this.gkeEnabled;
125+
}
126+
115127
/**
116128
* Analyze access for a subject.
117129
*/

0 commit comments

Comments
 (0)