8000 [pull] master from spinnaker:master by pull[bot] · Pull Request #42 · shdeep15/fiat · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

[pull] master from spinnaker:master #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package com.netflix.spinnaker.fiat.roles.google;

import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.batch.BatchRequest;
import com.google.api.client.googleapis.batch.json.JsonBatchCallback;
import com.google.api.client.googleapis.json.GoogleJsonError;
Expand All @@ -32,16 +31,22 @@
import com.google.api.services.directory.DirectoryScopes;
import com.google.api.services.directory.model.Group;
import com.google.api.services.directory.model.Groups;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.netflix.spinnaker.fiat.model.resources.Role;
import com.netflix.spinnaker.fiat.permissions.ExternalUser;
import com.netflix.spinnaker.fiat.roles.UserRolesProvider;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand All @@ -53,8 +58,6 @@
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.PropertyAccessor;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
Expand Down Expand Up @@ -180,7 +183,7 @@ public List<Role> loadRoles(ExternalUser user) {
}

try {
Groups groups = getGroupsFromEmail(userEmail);
Groups groups = getGroupsFromEmailRecursively(userEmail);
if (groups == null || groups.getGroups() == null || groups.getGroups().isEmpty()) {
return new ArrayList<>();
}
Expand All @@ -191,6 +194,54 @@ public List<Role> loadRoles(ExternalUser user) {
}
}

/**
* Retrieves all Google Groups associated with a given email address, including both direct and
* indirect group memberships, if configured to do so.
*
* <p>This method first fetches the groups the user is directly a member of via {@link
* #getGroupsFromEmail(String)}. If the configuration allows expanding indirect groups (i.e.,
* nested groups), it recursively traverses each group's membership to collect nested groups.
*
* <p>The method avoids cycles and duplicate group processing by maintaining a set of already
* collected group emails.
*
* @param email The email address whose group memberships should be retrieved.
* @return A {@link Groups} object containing all the direct and (optionally) indirect group
* memberships.
* @throws IOException If an error occurs while retrieving group information.
*/
protected Groups getGroupsFromEmailRecursively(String email) throws IOException {
final Groups groups = getGroupsFromEmail(email);
if (groups == null
|| groups.getGroups() == null
|| groups.getGroups().isEmpty()
|| !config.isExpandIndirectGroups()) {
return groups;
}
final Set<String> collectedGroup = new HashSet<>();
final Deque<String> stack = new ArrayDeque<>();
for (Group g : groups.getGroups()) {
stack.push(g.getEmail());
collectedGroup.add(g.getEmail());
}
while (!stack.isEmpty()) {
String nextEmail = stack.pop();
Groups subGroups = getGroupsFromEmail(nextEmail);
if (subGroups == null || subGroups.getGroups() == null || subGroups.getGroups().isEmpty()) {
continue;
}
for (Group g : subGroups.getGroups()) {
if (collectedGroup.contains(g.getEmail())) {
continue;
}
stack.push(g.getEmail());
groups.getGroups().add(g);
collectedGroup.add(g.getEmail());
}
}
return groups;
}

protected Groups getGroupsFromEmail(String email) throws IOException {
final Directory service = getDirectoryService();
final Groups groups =
Expand All @@ -211,12 +262,15 @@ protected Groups getGroupsFromEmail(String email) throws IOException {
return groups;
}

private GoogleCredential getGoogleCredential() {
private GoogleCredentials getGoogleCredential() {
try {
if (StringUtils.isNotEmpty(config.getCredentialPath())) {
return GoogleCredential.fromStream(new FileInputStream(config.getCredentialPath()));
if (StringUtils.isNotEmpty(config.getCredentialPath())
&& StringUtils.isNotEmpty(config.getAdminUsername())) {
return ServiceAccountCredentials.fromStream(new FileInputStream(config.getCredentialPath()))
.createScoped(SERVICE_ACCOUNT_SCOPES) // add other scopes as needed
.createDelegated(config.getAdminUsername());
} else {
return GoogleCredential.getApplicationDefault();
return GoogleCredentials.getApplicationDefault();
}
} catch (IOException ioe) {
throw new RuntimeException(ioe);
Expand All @@ -226,13 +280,10 @@ private GoogleCredential getGoogleCredential() {
private Directory getDirectoryService() {
HttpTransport httpTransport = new NetHttpTransport();
GsonFactory jacksonFactory = new GsonFactory();
GoogleCredential credential = getGoogleCredential();
GoogleCredentials credentials = getGoogleCredential();

PropertyAccessor accessor = PropertyAccessorFactory.forDirectFieldAccess(credential);
accessor.setPropertyValue("serviceAccountUser", config.getAdminUsername());
accessor.setPropertyValue("serviceAccountScopes", SERVICE_ACCOUNT_SCOPES);

return new Directory.Builder(httpTransport, jacksonFactory, credential)
return new Directory.Builder(
httpTransport, jacksonFactory, new HttpCredentialsAdapter(credentials))
.setApplicationName("Spinnaker-Fiat")
.build();
}
Expand Down Expand Up @@ -272,6 +323,9 @@ public static class Config {
/** Google Apps for Work domain, e.g. netflix.com */
private String domain;

/** expand indirect groups for emails */
private boolean expandIndirectGroups = false;

/**
* List of sources to derive role name from group metadata, this setting is additive to allow
* backwards compatibility
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.netflix.spinnaker.fiat.permissions.ExternalUser
import com.google.api.services.directory.model.Group;
import com.google.api.services.directory.model.Groups;
import spock.lang.Specification
import spock.lang.Unroll

class GoogleDirectoryUserRolesProviderSpec extends Specification {
GoogleDirectoryUserRolesProvider.Config config = new GoogleDirectoryUserRolesProvider.Config()
Expand All @@ -21,7 +22,7 @@ class GoogleDirectoryUserRolesProviderSpec extends Specification {

GoogleDirectoryUserRolesProvider provider = new GoogleDirectoryUserRolesProvider() {
@Override
Groups getGroupsFromEmail(String email) {
Groups getGroupsFromEmailRecursively(String email) {
return groups
}
}
Expand Down Expand Up @@ -75,9 +76,41 @@ class GoogleDirectoryUserRolesProviderSpec extends Specification {

then:
result6.name.size() == 0
}

@Unroll
def "should recursively collect all nested groups if expandIndirectGroups is #expandIndirectGroups"() {
given:
config.expandIndirectGroups = expandIndirectGroups
def provider = Spy(GoogleDirectoryUserRolesProvider) {
getGroupsFromEmail("root@example.com") >> new Groups(groups: [
new Group(email: "child1@example.com"),
new Group(email: "child2@example.com")
])
getGroupsFromEmail("child1@example.com") >> new Groups(groups: [
new Group(email: "grandchild1@example.com")
])
getGroupsFromEmail("child2@example.com") >> new Groups(groups: [
new Group(email: "grandchild2@example.com"),
new Group(email: "child1@example.com")
])
getGroupsFromEmail("grandchild1@example.com") >> new Groups(groups: [])
getGroupsFromEmail("grandchild2@example.com") >> null
}
provider.setConfig(config)

when:
def result = provider.getGroupsFromEmailRecursively("root@example.com")

then:
result.groups*.email.containsAll(groupsContent)
result.groups.size() == totalEmails

where:
expandIndirectGroups | totalEmails | groupsContent
true | 4 | ["child1@example.com", "child2@example.com", "grandchild1@example.com", "grandchild2@example.com"]
false | 2 | ["child1@example.com", "child2@example.com"]

}

private static ExternalUser externalUser(String id) {
Expand Down
0