diff --git a/fiat-core/src/main/java/com/netflix/spinnaker/fiat/model/Authorization.java b/fiat-core/src/main/java/com/netflix/spinnaker/fiat/model/Authorization.java index 55eb3a8bb..74efcc3ae 100644 --- a/fiat-core/src/main/java/com/netflix/spinnaker/fiat/model/Authorization.java +++ b/fiat-core/src/main/java/com/netflix/spinnaker/fiat/model/Authorization.java @@ -16,9 +16,8 @@ package com.netflix.spinnaker.fiat.model; -import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; +import java.util.EnumSet; import java.util.Set; public enum Authorization { @@ -26,6 +25,6 @@ public enum Authorization { WRITE, EXECUTE; - public static Set ALL = - Collections.unmodifiableSet(new HashSet<>(Arrays.asList(values()))); + public static final Set ALL = + Collections.unmodifiableSet(EnumSet.allOf(Authorization.class)); } diff --git a/fiat-core/src/main/java/com/netflix/spinnaker/fiat/model/resources/Permissions.java b/fiat-core/src/main/java/com/netflix/spinnaker/fiat/model/resources/Permissions.java index 61024c277..1a4ea54f3 100644 --- a/fiat-core/src/main/java/com/netflix/spinnaker/fiat/model/resources/Permissions.java +++ b/fiat-core/src/main/java/com/netflix/spinnaker/fiat/model/resources/Permissions.java @@ -27,19 +27,19 @@ /** * Representation of authorization configuration for a resource. This object is immutable, which - * makes it challenging when working with Jackson's {{ObjectMapper}} and Spring's - * {{\@ConfigurationProperties}}. The {@link Builder} is a helper class for the latter use case. + * makes it challenging when working with Jackson's {@code ObjectMapper} and Spring's + * {@code @ConfigurationProperties}. The {@link Builder} is a helper class for the latter use case. */ @ToString @EqualsAndHashCode public class Permissions { - public static Permissions EMPTY = new Permissions.Builder().build(); + public static final Permissions EMPTY = Builder.fromMap(Collections.emptyMap()); private final Map> permissions; private Permissions(Map> p) { - this.permissions = p; + this.permissions = Collections.unmodifiableMap(p); } /** @@ -69,10 +69,6 @@ public boolean isAuthorized(Set userRoles) { return !getAuthorizations(userRoles).isEmpty(); } - public boolean isEmpty() { - return permissions.isEmpty(); - } - public Set getAuthorizations(Set userRoles) { val r = userRoles.stream().map(Role::getName).collect(Collectors.toList()); return getAuthorizations(r); @@ -90,7 +86,7 @@ public Set getAuthorizations(List userRoles) { } public List get(Authorization a) { - return permissions.get(a); + return permissions.getOrDefault(a, Collections.emptyList()); } /** @@ -105,6 +101,25 @@ public List get(Authorization a) { */ public static class Builder extends LinkedHashMap> { + private static Permissions fromMap(Map> authConfig) { + final Map> perms = new EnumMap<>(Authorization.class); + for (Authorization auth : Authorization.values()) { + perms.put( + auth, + Optional.ofNullable(authConfig.get(auth)) + .map( + groups -> + groups.stream() + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(String::toLowerCase) + .collect(Collectors.toList())) + .map(Collections::unmodifiableList) + .orElse(Collections.emptyList())); + } + return new Permissions(perms); + } + @JsonCreator public static Builder factory(Map> data) { return new Builder().set(data); @@ -127,19 +142,11 @@ public Builder add(Authorization a, List groups) { } public Permissions build() { - final Map> perms = new HashMap<>(); - this.forEach( - (auth, groups) -> { - List lowerGroups = - Collections.unmodifiableList( - groups.stream() - .map(String::trim) - .filter(s -> !s.isEmpty()) - .map(String::toLowerCase) - .collect(Collectors.toList())); - perms.put(auth, lowerGroups); - }); - return new Permissions(Collections.unmodifiableMap(perms)); + final Permissions result = fromMap(this); + if (!result.isRestricted()) { + return Permissions.EMPTY; + } + return result; } } } diff --git a/fiat-core/src/test/groovy/com/netflix/spinnaker/fiat/model/resources/PermissionsSpec.groovy b/fiat-core/src/test/groovy/com/netflix/spinnaker/fiat/model/resources/PermissionsSpec.groovy index 93805f7f0..37fc3749c 100644 --- a/fiat-core/src/test/groovy/com/netflix/spinnaker/fiat/model/resources/PermissionsSpec.groovy +++ b/fiat-core/src/test/groovy/com/netflix/spinnaker/fiat/model/resources/PermissionsSpec.groovy @@ -42,10 +42,18 @@ class PermissionsSpec extends Specification { .enable(SerializationFeature.INDENT_OUTPUT) .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS) - String permissionJson = '''{ - "READ" : [ "foo" ], - "WRITE" : [ "bar" ] -}''' + String permissionJson = '''\ + { + "READ" : [ "foo" ], + "WRITE" : [ "bar" ] + }'''.stripIndent() + + String permissionSerialized = '''\ + { + "READ" : [ "foo" ], + "WRITE" : [ "bar" ], + "EXECUTE" : [ ] + }'''.stripIndent() def "should deserialize"() { when: @@ -54,6 +62,7 @@ class PermissionsSpec extends Specification { then: p.get(R) == ["foo"] p.get(W) == ["bar"] + p.get(E) == [] when: Permissions.Builder b = mapper.readValue(permissionJson, Permissions.Builder) @@ -62,6 +71,7 @@ class PermissionsSpec extends Specification { then: p.get(R) == ["foo"] p.get(W) == ["bar"] + p.get(E) == [] } def "should serialize"() { @@ -70,7 +80,7 @@ class PermissionsSpec extends Specification { b.set([(R): ["foo"], (W): ["bar"]]) then: - permissionJson == mapper.writeValueAsString(b.build()) + permissionSerialized == mapper.writeValueAsString(b.build()) } def "can deserialize to builder from serialized Permissions"() { @@ -85,7 +95,6 @@ class PermissionsSpec extends Specification { then: p1 == p2 - b1 == b2 } def "should trim and lower"() { diff --git a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/permissions/DefaultPermissionsResolver.java b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/permissions/DefaultPermissionsResolver.java index 094913ec7..e021930d4 100644 --- a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/permissions/DefaultPermissionsResolver.java +++ b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/permissions/DefaultPermissionsResolver.java @@ -17,6 +17,7 @@ package com.netflix.spinnaker.fiat.permissions; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; import com.netflix.spinnaker.fiat.config.FiatAdminConfig; import com.netflix.spinnaker.fiat.config.UnrestrictedResourceConfig; import com.netflix.spinnaker.fiat.model.UserPermission; @@ -36,31 +37,35 @@ import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; -import lombok.NoArgsConstructor; import lombok.NonNull; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; @Component -@NoArgsConstructor @Slf4j public class DefaultPermissionsResolver implements PermissionsResolver { - @Autowired @Setter private UserRolesProvider userRolesProvider; - - @Autowired @Setter private ResourceProvider serviceAccountProvider; - - @Autowired @Setter private List> resourceProviders; - - @Autowired @Setter private FiatAdminConfig fiatAdminConfig; + private final UserRolesProvider userRolesProvider; + private final ResourceProvider serviceAccountProvider; + private final ImmutableList> resourceProviders; + private final FiatAdminConfig fiatAdminConfig; + private final ObjectMapper mapper; @Autowired - @Qualifier("objectMapper") - @Setter - private ObjectMapper mapper; + public DefaultPermissionsResolver( + UserRolesProvider userRolesProvider, + ResourceProvider serviceAccountProvider, + List> resourceProviders, + FiatAdminConfig fiatAdminConfig, + @Qualifier("objectMapper") ObjectMapper mapper) { + this.userRolesProvider = userRolesProvider; + this.serviceAccountProvider = serviceAccountProvider; + this.resourceProviders = ImmutableList.copyOf(resourceProviders); + this.fiatAdminConfig = fiatAdminConfig; + this.mapper = mapper; + } @Override public UserPermission resolveUnrestrictedUser() { @@ -123,13 +128,12 @@ private UserPermission getUserPermission(String userId, Set roles) { } @Override - @SuppressWarnings("unchecked") public Map resolve(@NonNull Collection users) { Map> allServiceAccountRoles = getServiceAccountRoles(); Collection serviceAccounts = users.stream() - .filter(user -> allServiceAccountRoles.keySet().contains(user.getId())) + .filter(user -> allServiceAccountRoles.containsKey(user.getId())) .collect(Collectors.toList()); // Service accounts should already have external roles set. Remove them from the list so they @@ -160,11 +164,10 @@ private Map> getAndMergeUserRoles( Map> userToRoles = userRolesProvider.multiLoadRoles(users); users.forEach( - user -> { - userToRoles - .computeIfAbsent(user.getId(), ignored -> new ArrayList<>()) - .addAll(user.getExternalRoles()); - }); + user -> + userToRoles + .computeIfAbsent(user.getId(), ignored -> new ArrayList<>()) + .addAll(user.getExternalRoles())); if (log.isDebugEnabled()) { try { diff --git a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/AccessControlledResourcePermissionSource.java b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/AccessControlledResourcePermissionSource.java new file mode 100644 index 000000000..51d9ab511 --- /dev/null +++ b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/AccessControlledResourcePermissionSource.java @@ -0,0 +1,35 @@ +/* + * Copyright 2019 Google, LLC + * + * 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. + */ + +package com.netflix.spinnaker.fiat.providers; + +import com.netflix.spinnaker.fiat.model.resources.Permissions; +import com.netflix.spinnaker.fiat.model.resources.Resource; +import java.util.Optional; +import javax.annotation.Nonnull; + +public final class AccessControlledResourcePermissionSource + implements ResourcePermissionSource { + + @Override + @Nonnull + public Permissions getPermissions(@Nonnull T resource) { + return Optional.ofNullable(resource) + .map(Resource.AccessControlled::getPermissions) + .filter(Permissions::isRestricted) + .orElse(Permissions.EMPTY); + } +} diff --git a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/AggregatingResourcePermissionProvider.java b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/AggregatingResourcePermissionProvider.java new file mode 100644 index 000000000..f8cdc19b1 --- /dev/null +++ b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/AggregatingResourcePermissionProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright 2019 Netflix, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.fiat.providers; + +import com.netflix.spinnaker.fiat.model.Authorization; +import com.netflix.spinnaker.fiat.model.resources.Permissions; +import com.netflix.spinnaker.fiat.model.resources.Resource; +import java.util.List; +import javax.annotation.Nonnull; + +/** + * AggregatingResourcePermissionProvider additively combines permissions from all + * ResourcePermissionSources to build a resulting Permission object. + * + * @param the type of Resource for this AggregatingResourcePermissionProvider + */ +public class AggregatingResourcePermissionProvider + implements ResourcePermissionProvider { + + private final List> resourcePermissionSources; + + public AggregatingResourcePermissionProvider( + List> resourcePermissionSources) { + this.resourcePermissionSources = resourcePermissionSources; + } + + @Override + @Nonnull + public Permissions getPermissions(@Nonnull T resource) { + Permissions.Builder builder = new Permissions.Builder(); + for (ResourcePermissionSource source : resourcePermissionSources) { + Permissions permissions = source.getPermissions(resource); + if (permissions.isRestricted()) { + for (Authorization auth : Authorization.values()) { + builder.add(auth, permissions.get(auth)); + } + } + } + + return builder.build(); + } +} diff --git a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/BaseProvider.java b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/BaseResourceProvider.java similarity index 97% rename from fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/BaseProvider.java rename to fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/BaseResourceProvider.java index adada3e5d..257214e9b 100644 --- a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/BaseProvider.java +++ b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/BaseResourceProvider.java @@ -32,7 +32,7 @@ import org.springframework.beans.factory.annotation.Autowired; @Slf4j -public abstract class BaseProvider implements ResourceProvider { +public abstract class BaseResourceProvider implements ResourceProvider { private static final Integer CACHE_KEY = 0; diff --git a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultAccountProvider.java b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultAccountResourceProvider.java similarity index 64% rename from fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultAccountProvider.java rename to fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultAccountResourceProvider.java index d9e36e835..4ec7ec5c8 100644 --- a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultAccountProvider.java +++ b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultAccountResourceProvider.java @@ -16,30 +16,37 @@ package com.netflix.spinnaker.fiat.providers; +import com.google.common.collect.ImmutableSet; import com.netflix.spinnaker.fiat.model.resources.Account; import com.netflix.spinnaker.fiat.providers.internal.ClouddriverService; -import java.util.HashSet; +import java.util.List; import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component -public class DefaultAccountProvider extends BaseProvider +public class DefaultAccountResourceProvider extends BaseResourceProvider implements ResourceProvider { private final ClouddriverService clouddriverService; + private final ResourcePermissionProvider permissionProvider; @Autowired - public DefaultAccountProvider(ClouddriverService clouddriverService) { - super(); + public DefaultAccountResourceProvider( + ClouddriverService clouddriverService, + ResourcePermissionProvider permissionProvider) { this.clouddriverService = clouddriverService; + this.permissionProvider = permissionProvider; } @Override protected Set loadAll() throws ProviderException { try { - return new HashSet<>(clouddriverService.getAccounts()); - } catch (Exception e) { + List accounts = clouddriverService.getAccounts(); + accounts.forEach( + account -> account.setPermissions(permissionProvider.getPermissions(account))); + return ImmutableSet.copyOf(accounts); + } catch (RuntimeException e) { throw new ProviderException(this.getClass(), e.getCause()); } } diff --git a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultApplicationProvider.java b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultApplicationResourceProvider.java similarity index 55% rename from fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultApplicationProvider.java rename to fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultApplicationResourceProvider.java index de73b8d30..39aa9d986 100644 --- a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultApplicationProvider.java +++ b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultApplicationResourceProvider.java @@ -16,44 +16,40 @@ package com.netflix.spinnaker.fiat.providers; -import com.netflix.spinnaker.fiat.model.Authorization; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableSet.toImmutableSet; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Streams; import com.netflix.spinnaker.fiat.model.resources.Application; -import com.netflix.spinnaker.fiat.model.resources.Permissions; import com.netflix.spinnaker.fiat.model.resources.Role; import com.netflix.spinnaker.fiat.providers.internal.ClouddriverService; import com.netflix.spinnaker.fiat.providers.internal.Front50Service; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.function.Function; -import java.util.stream.Collectors; -import lombok.NonNull; +import java.util.function.Predicate; -public class DefaultApplicationProvider extends BaseProvider +public class DefaultApplicationResourceProvider extends BaseResourceProvider implements ResourceProvider { private final Front50Service front50Service; private final ClouddriverService clouddriverService; + private final ResourcePermissionProvider permissionProvider; private final boolean allowAccessToUnknownApplications; - private final Authorization executeFallback; - public DefaultApplicationProvider( + public DefaultApplicationResourceProvider( Front50Service front50Service, ClouddriverService clouddriverService, - boolean allowAccessToUnknownApplications, - Authorization executeFallback) { - super(); - + ResourcePermissionProvider permissionProvider, + boolean allowAccessToUnknownApplications) { this.front50Service = front50Service; this.clouddriverService = clouddriverService; + this.permissionProvider = permissionProvider; this.allowAccessToUnknownApplications = allowAccessToUnknownApplications; - this.executeFallback = executeFallback; } @Override @@ -70,36 +66,42 @@ public Set getAllUnrestricted() throws ProviderException { @Override protected Set loadAll() throws ProviderException { try { - Map appByName = - front50Service.getAllApplicationPermissions().stream() - .collect(Collectors.toMap(Application::getName, Function.identity())); - - clouddriverService.getApplications().stream() - .filter(app -> !appByName.containsKey(app.getName())) - .forEach(app -> appByName.put(app.getName(), app)); - - Set applications; + List front50Applications = front50Service.getAllApplicationPermissions(); + List clouddriverApplications = clouddriverService.getApplications(); + + // Stream front50 first so that if there's a name collision, we'll keep that one instead of + // the clouddriver application (since front50 might have permissions stored on it, but the + // clouddriver version definitely won't) + List applications = + Streams.concat(front50Applications.stream(), clouddriverApplications.stream()) + .filter(distinctByKey(Application::getName)) + // Collect to a list instead of set since we're about to modify the applications + .collect(toImmutableList()); + + applications.forEach( + application -> + application.setPermissions(permissionProvider.getPermissions(application))); if (allowAccessToUnknownApplications) { // no need to include applications w/o explicit permissions if we're allowing access to // unknown applications by default - applications = - appByName.values().stream() - .filter(a -> !a.getPermissions().isEmpty()) - .collect(Collectors.toSet()); + return applications.stream() + .filter(a -> a.getPermissions().isRestricted()) + .collect(toImmutableSet()); } else { - applications = new HashSet<>(appByName.values()); + return ImmutableSet.copyOf(applications); } - - // Fallback authorization for legacy applications that are missing EXECUTE permissions - applications.forEach(this::ensureExecutePermission); - - return applications; - } catch (Exception e) { + } catch (RuntimeException e) { throw new ProviderException(this.getClass(), e); } } + // Keeps only the first object with the key + private static Predicate distinctByKey(Function keyExtractor) { + Set seenKeys = new HashSet<>(); + return t -> seenKeys.add(keyExtractor.apply(t)); + } + private Set getAllApplications( Set roles, boolean isAdmin, boolean isRestricted) { if (allowAccessToUnknownApplications) { @@ -118,30 +120,4 @@ private Set getAllApplications( return isRestricted ? super.getAllRestricted(roles, isAdmin) : super.getAllUnrestricted(); } - - /** - * Set EXECUTE authorization(s) for the application. For applications that already have EXECUTE - * set, this will be a no-op. For the remaining applications, we'll add EXECUTE based on the value - * of the `executeFallback` flag. - */ - private void ensureExecutePermission(@NonNull Application application) { - Permissions permissions = application.getPermissions(); - - if (permissions == null || !permissions.isRestricted()) { - return; - } - - Map> authorizations = - Arrays.stream(Authorization.values()) - .collect( - Collectors.toMap( - Function.identity(), - a -> Optional.ofNullable(permissions.get(a)).orElse(new ArrayList<>()))); - - if (authorizations.get(Authorization.EXECUTE).isEmpty()) { - authorizations.put(Authorization.EXECUTE, authorizations.get(this.executeFallback)); - } - - application.setPermissions(Permissions.Builder.factory(authorizations).build()); - } } diff --git a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultBuildServiceProvider.java b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultBuildServiceResourceProvider.java similarity index 65% rename from fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultBuildServiceProvider.java rename to fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultBuildServiceResourceProvider.java index 31134eecd..1e2a5f3ba 100644 --- a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultBuildServiceProvider.java +++ b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultBuildServiceResourceProvider.java @@ -17,9 +17,10 @@ package com.netflix.spinnaker.fiat.providers; +import com.google.common.collect.ImmutableSet; import com.netflix.spinnaker.fiat.model.resources.BuildService; import com.netflix.spinnaker.fiat.providers.internal.IgorService; -import java.util.HashSet; +import java.util.List; import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -27,22 +28,29 @@ @Component @ConditionalOnProperty("services.igor.enabled") -public class DefaultBuildServiceProvider extends BaseProvider +public class DefaultBuildServiceResourceProvider extends BaseResourceProvider implements ResourceProvider { private final IgorService igorService; + private final ResourcePermissionProvider permissionProvider; @Autowired - public DefaultBuildServiceProvider(IgorService igorService) { + public DefaultBuildServiceResourceProvider( + IgorService igorService, ResourcePermissionProvider permissionProvider) { super(); this.igorService = igorService; + this.permissionProvider = permissionProvider; } @Override protected Set loadAll() throws ProviderException { try { - return new HashSet<>(igorService.getAllBuildServices()); - } catch (Exception e) { + List buildServices = igorService.getAllBuildServices(); + buildServices.forEach( + buildService -> + buildService.setPermissions(permissionProvider.getPermissions(buildService))); + return ImmutableSet.copyOf(buildServices); + } catch (RuntimeException e) { throw new ProviderException(this.getClass(), e.getCause()); } } diff --git a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultResourcePermissionProvider.java b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultResourcePermissionProvider.java new file mode 100644 index 000000000..9a6b4baba --- /dev/null +++ b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultResourcePermissionProvider.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019 Netflix, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.fiat.providers; + +import com.netflix.spinnaker.fiat.model.resources.Permissions; +import com.netflix.spinnaker.fiat.model.resources.Resource; +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * A ResourcePermissionProvider that delegates to a single ResourcePermissionProvider. + * + * @param the type of Resource for this DefaultResourcePermissionProvider + */ +public class DefaultResourcePermissionProvider + implements ResourcePermissionProvider { + + private final ResourcePermissionSource resourcePermissionSource; + + public DefaultResourcePermissionProvider(ResourcePermissionSource resourcePermissionSource) { + this.resourcePermissionSource = Objects.requireNonNull(resourcePermissionSource); + } + + @Override + @Nonnull + public Permissions getPermissions(@Nonnull T resource) { + return resourcePermissionSource.getPermissions(resource); + } +} diff --git a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultServiceAccountProvider.java b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultServiceAccountResourceProvider.java similarity index 95% rename from fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultServiceAccountProvider.java rename to fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultServiceAccountResourceProvider.java index aadafcadd..88e852e01 100644 --- a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultServiceAccountProvider.java +++ b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/DefaultServiceAccountResourceProvider.java @@ -33,7 +33,7 @@ @Component @Slf4j -public class DefaultServiceAccountProvider extends BaseProvider +public class DefaultServiceAccountResourceProvider extends BaseResourceProvider implements ResourceProvider { private final Front50Service front50Service; @@ -41,7 +41,7 @@ public class DefaultServiceAccountProvider extends BaseProvider private final FiatRoleConfig fiatRoleConfig; @Autowired - public DefaultServiceAccountProvider( + public DefaultServiceAccountResourceProvider( Front50Service front50Service, FiatRoleConfig fiatRoleConfig) { super(); this.front50Service = front50Service; diff --git a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/Front50ApplicationResourcePermissionSource.java b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/Front50ApplicationResourcePermissionSource.java new file mode 100644 index 000000000..310bd0f88 --- /dev/null +++ b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/Front50ApplicationResourcePermissionSource.java @@ -0,0 +1,58 @@ +/* + * Copyright 2019 Google, LLC + * + * 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. + */ + +package com.netflix.spinnaker.fiat.providers; + +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; + +import com.netflix.spinnaker.fiat.model.Authorization; +import com.netflix.spinnaker.fiat.model.resources.Application; +import com.netflix.spinnaker.fiat.model.resources.Permissions; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; + +public final class Front50ApplicationResourcePermissionSource + implements ResourcePermissionSource { + + private final Authorization executeFallback; + + public Front50ApplicationResourcePermissionSource(Authorization executeFallback) { + this.executeFallback = executeFallback; + } + + @Override + @Nonnull + public Permissions getPermissions(@Nonnull Application resource) { + Permissions storedPermissions = resource.getPermissions(); + if (storedPermissions == null || !storedPermissions.isRestricted()) { + return Permissions.EMPTY; + } + + Map> authorizations = + Arrays.stream(Authorization.values()).collect(toMap(identity(), storedPermissions::get)); + + // If the execute permission wasn't set, copy the permissions from whatever is specified in the + // config's executeFallback flag + if (authorizations.get(Authorization.EXECUTE).isEmpty()) { + authorizations.put(Authorization.EXECUTE, authorizations.get(executeFallback)); + } + + return Permissions.Builder.factory(authorizations).build(); + } +} diff --git a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/ProviderException.java b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/ProviderException.java index fc6dca02d..3e4cac0a0 100644 --- a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/ProviderException.java +++ b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/ProviderException.java @@ -16,7 +16,9 @@ package com.netflix.spinnaker.fiat.providers; -public class ProviderException extends RuntimeException { +import com.netflix.spinnaker.kork.exceptions.IntegrationException; + +public class ProviderException extends IntegrationException { private Class provider; diff --git a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/ResourcePermissionProvider.java b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/ResourcePermissionProvider.java new file mode 100644 index 000000000..ee6cb67ec --- /dev/null +++ b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/ResourcePermissionProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019 Google, LLC + * + * 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. + */ + +package com.netflix.spinnaker.fiat.providers; + +import com.netflix.spinnaker.fiat.model.resources.Permissions; +import com.netflix.spinnaker.fiat.model.resources.Resource; +import javax.annotation.Nonnull; + +/** + * A ResourcePermissionProvider is responsible for supplying the full set of Permissions for a + * specific Resource. + * + *

Note that while the API signature matches ResourcePermissionSource the intent of + * ResourcePermissionProvider is the interface for consumers interested in the actual Permissions of + * a resource, while ResourcePermissionSource models a single source of Permissions (for example + * CloudDriver as a source of Account permissions). + * + * @param the type of Resource for which this ResourcePermissionProvider supplies Permissions. + */ +public interface ResourcePermissionProvider { + + /** + * Retrieves Permissions for the supplied resource. + * + * @param resource the resource for which to get permissions (never null) + * @return the Permissions for the resource (never null - use Permissions.EMPTY or apply some + * restriction) + */ + @Nonnull + Permissions getPermissions(@Nonnull T resource); +} diff --git a/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/ResourcePermissionSource.java b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/ResourcePermissionSource.java new file mode 100644 index 000000000..f88484658 --- /dev/null +++ b/fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/ResourcePermissionSource.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019 Netflix, Inc. + * + * 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. + */ + +package com.netflix.spinnaker.fiat.providers; + +import com.netflix.spinnaker.fiat.model.resources.Permissions; +import com.netflix.spinnaker.fiat.model.resources.Resource; +import javax.annotation.Nonnull; + +/** + * A ResourcePermissionSource is capable of resolving the Permissions for a specified Resource from + * a specified single source. + * + *

Note that while the API signature matches ResourcePermissionProvider the intent of + * ResourcePermissionSource is to model a single source of Permissions (for example CloudDriver as a + * source of Account permissions), while ResourcePermissionProvider is responsible for supplying the + * computed Permission considering all available ResourcePermissionSources. + * + * @param the type of Resource this ResourcePermissionSource will supply Permissions for + */ +public interface ResourcePermissionSource { + + /** + * Retrieves Permissions for the supplied resource. + * + * @param resource the resource for which to get permissions (never null) + * @return the Permissions for the resource (never null - use Permissions.EMPTY or apply some + * restriction) + */ + @Nonnull + Permissions getPermissions(@Nonnull T resource); +} diff --git a/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/permissions/DefaultPermissionsResolverSpec.groovy b/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/permissions/DefaultPermissionsResolverSpec.groovy index 7eccb9114..a4cc0becb 100644 --- a/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/permissions/DefaultPermissionsResolverSpec.groovy +++ b/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/permissions/DefaultPermissionsResolverSpec.groovy @@ -24,8 +24,11 @@ import com.netflix.spinnaker.fiat.model.resources.Account import com.netflix.spinnaker.fiat.model.resources.Application import com.netflix.spinnaker.fiat.model.resources.Role import com.netflix.spinnaker.fiat.model.resources.ServiceAccount -import com.netflix.spinnaker.fiat.providers.DefaultAccountProvider -import com.netflix.spinnaker.fiat.providers.DefaultServiceAccountProvider +import com.netflix.spinnaker.fiat.providers.AccessControlledResourcePermissionSource +import com.netflix.spinnaker.fiat.providers.AggregatingResourcePermissionProvider +import com.netflix.spinnaker.fiat.providers.DefaultAccountResourceProvider +import com.netflix.spinnaker.fiat.providers.DefaultServiceAccountResourceProvider +import com.netflix.spinnaker.fiat.providers.ResourcePermissionProvider import com.netflix.spinnaker.fiat.providers.ResourceProvider import com.netflix.spinnaker.fiat.providers.internal.ClouddriverService import com.netflix.spinnaker.fiat.providers.internal.Front50Service @@ -53,7 +56,11 @@ class DefaultPermissionsResolverSpec extends Specification { } @Shared - DefaultAccountProvider accountProvider = new DefaultAccountProvider(clouddriverService) + ResourcePermissionProvider accountResourcePermissionProvider = + new AggregatingResourcePermissionProvider<>([new AccessControlledResourcePermissionSource<>()]) + + @Shared + DefaultAccountResourceProvider accountProvider = new DefaultAccountResourceProvider(clouddriverService, accountResourcePermissionProvider) @Shared ServiceAccount group1SvcAcct = new ServiceAccount().setName("group1") .setMemberOf(["group1"]) @@ -71,7 +78,7 @@ class DefaultPermissionsResolverSpec extends Specification { } @Shared - DefaultServiceAccountProvider serviceAccountProvider = new DefaultServiceAccountProvider(front50Service, fiatRoleConfig) + DefaultServiceAccountResourceProvider serviceAccountProvider = new DefaultServiceAccountResourceProvider(front50Service, fiatRoleConfig) @Shared ResourceProvider applicationProvider = Mock(ResourceProvider) { @@ -85,11 +92,8 @@ class DefaultPermissionsResolverSpec extends Specification { def "should resolve the anonymous user permission, when enabled"() { setup: - @Subject DefaultPermissionsResolver resolver = new DefaultPermissionsResolver() - .setResourceProviders(resourceProviders) - .setMapper(new ObjectMapper()) - .setFiatAdminConfig(new FiatAdminConfig()) - .setUserRolesProvider(userRolesProvider) + @Subject DefaultPermissionsResolver resolver = new DefaultPermissionsResolver( + userRolesProvider, serviceAccountProvider, resourceProviders, new FiatAdminConfig(), new ObjectMapper()) when: def result = resolver.resolveUnrestrictedUser() @@ -107,10 +111,8 @@ class DefaultPermissionsResolverSpec extends Specification { setup: def testUserId = "testUserId" UserRolesProvider userRolesProvider = Mock(UserRolesProvider) - @Subject DefaultPermissionsResolver resolver = new DefaultPermissionsResolver() - .setUserRolesProvider(userRolesProvider) - .setResourceProviders(resourceProviders) - .setFiatAdminConfig(new FiatAdminConfig()) + @Subject DefaultPermissionsResolver resolver = new DefaultPermissionsResolver( + userRolesProvider, serviceAccountProvider, resourceProviders, new FiatAdminConfig(), new ObjectMapper()) def role1 = new Role("group1") def role2 = new Role("gRoUP2") // to test case insensitivity. @@ -167,10 +169,8 @@ class DefaultPermissionsResolverSpec extends Specification { def testUserId = "testUserId" UserRolesProvider userRolesProvider = Mock(UserRolesProvider) - @Subject DefaultPermissionsResolver resolver = new DefaultPermissionsResolver() - .setUserRolesProvider(userRolesProvider) - .setResourceProviders(resourceProviders) - .setFiatAdminConfig(fiatAdminConfig) + @Subject DefaultPermissionsResolver resolver = new DefaultPermissionsResolver( + userRolesProvider, serviceAccountProvider, resourceProviders, fiatAdminConfig, new ObjectMapper()) def role1 = new Role("delivery-team") def testUser = new ExternalUser().setId(testUserId).setExternalRoles([role1]) @@ -191,12 +191,8 @@ class DefaultPermissionsResolverSpec extends Specification { def "should resolve all user's permissions"() { setup: UserRolesProvider userRolesProvider = Mock(UserRolesProvider) - @Subject DefaultPermissionsResolver resolver = new DefaultPermissionsResolver() - .setUserRolesProvider(userRolesProvider) - .setResourceProviders(resourceProviders) - .setMapper(new ObjectMapper()) - .setFiatAdminConfig(new FiatAdminConfig()) - .setServiceAccountProvider(serviceAccountProvider) + @Subject DefaultPermissionsResolver resolver = new DefaultPermissionsResolver( + userRolesProvider, serviceAccountProvider, resourceProviders, new FiatAdminConfig(), new ObjectMapper()) def role1 = new Role("group1") def role2 = new Role("group2") @@ -249,12 +245,8 @@ class DefaultPermissionsResolverSpec extends Specification { def "should resolve service account permissions"() { setup: UserRolesProvider userRolesProvider = Mock(UserRolesProvider) - @Subject DefaultPermissionsResolver resolver = new DefaultPermissionsResolver() - .setUserRolesProvider(userRolesProvider) - .setResourceProviders(resourceProviders) - .setMapper(new ObjectMapper()) - .setFiatAdminConfig(new FiatAdminConfig()) - .setServiceAccountProvider(serviceAccountProvider) + @Subject DefaultPermissionsResolver resolver = new DefaultPermissionsResolver( + userRolesProvider, serviceAccountProvider, resourceProviders, new FiatAdminConfig(), new ObjectMapper()) def role1 = new Role(group1SvcAcct.memberOf[0]) def svc1 = new ExternalUser().setId(group1SvcAcct.name).setExternalRoles([role1]) diff --git a/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/permissions/RedisPermissionsRepositorySpec.groovy b/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/permissions/RedisPermissionsRepositorySpec.groovy index c81148dd8..bed71424a 100644 --- a/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/permissions/RedisPermissionsRepositorySpec.groovy +++ b/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/permissions/RedisPermissionsRepositorySpec.groovy @@ -19,9 +19,11 @@ package com.netflix.spinnaker.fiat.permissions import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.databind.ObjectMapper import com.netflix.spinnaker.fiat.config.UnrestrictedResourceConfig +import com.netflix.spinnaker.fiat.model.Authorization import com.netflix.spinnaker.fiat.model.UserPermission import com.netflix.spinnaker.fiat.model.resources.Account import com.netflix.spinnaker.fiat.model.resources.Application +import com.netflix.spinnaker.fiat.model.resources.Permissions import com.netflix.spinnaker.fiat.model.resources.Role import com.netflix.spinnaker.fiat.model.resources.ServiceAccount import com.netflix.spinnaker.kork.jedis.EmbeddedRedis @@ -35,6 +37,8 @@ import spock.lang.Subject class RedisPermissionsRepositorySpec extends Specification { + private static final String EMPTY_PERM_JSON = "{${Authorization.values().collect {/"$it":[]/}.join(',')}}" + private static final String UNRESTRICTED = UnrestrictedResourceConfig.UNRESTRICTED_USERNAME static String prefix = "unittests" @@ -91,9 +95,9 @@ class RedisPermissionsRepositorySpec extends Specification { jedis.smembers("unittests:roles:role1") == ["testUser"] as Set jedis.hgetAll("unittests:permissions:testUser:accounts") == - ['account': '{"name":"account","permissions":{}}'] + ['account': /{"name":"account","permissions":$EMPTY_PERM_JSON}/.toString()] jedis.hgetAll("unittests:permissions:testUser:applications") == - ['app': '{"name":"app","permissions":{}}'] + ['app': /{"name":"app","permissions":$EMPTY_PERM_JSON}/.toString()] jedis.hgetAll("unittests:permissions:testUser:service_accounts") == ['serviceAccount': '{"name":"serviceAccount","memberOf":["role1"]}'] jedis.hgetAll("unittests:permissions:testUser:roles") == diff --git a/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/providers/BaseProviderSpec.groovy b/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/providers/BaseResourceProviderSpec.groovy similarity index 93% rename from fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/providers/BaseProviderSpec.groovy rename to fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/providers/BaseResourceProviderSpec.groovy index c537e9e15..78a2962e0 100644 --- a/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/providers/BaseProviderSpec.groovy +++ b/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/providers/BaseResourceProviderSpec.groovy @@ -27,7 +27,7 @@ import groovy.transform.builder.SimpleStrategy import spock.lang.Specification import spock.lang.Subject -class BaseProviderSpec extends Specification { +class BaseResourceProviderSpec extends Specification { private static Authorization R = Authorization.READ private static Authorization W = Authorization.WRITE @@ -51,7 +51,7 @@ class BaseProviderSpec extends Specification { def "should get all unrestricted"() { setup: - @Subject provider = new TestResourceProvider() + @Subject provider = new TestResourceResourceProvider() when: provider.all = [noReqGroups] @@ -72,7 +72,7 @@ class BaseProviderSpec extends Specification { def "should get restricted"() { setup: - @Subject provider = new TestResourceProvider() + @Subject provider = new TestResourceResourceProvider() when: provider.all = [noReqGroups] @@ -111,7 +111,7 @@ class BaseProviderSpec extends Specification { thrown IllegalArgumentException } - class TestResourceProvider extends BaseProvider { + class TestResourceResourceProvider extends BaseResourceProvider { Set all = new HashSet<>() @Override diff --git a/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/providers/DefaultApplicationProviderSpec.groovy b/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/providers/DefaultApplicationProviderSpec.groovy index 281a12965..7168918e9 100644 --- a/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/providers/DefaultApplicationProviderSpec.groovy +++ b/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/providers/DefaultApplicationProviderSpec.groovy @@ -34,8 +34,9 @@ class DefaultApplicationProviderSpec extends Specification { ClouddriverService clouddriverService = Mock(ClouddriverService) Front50Service front50Service = Mock(Front50Service) + ResourcePermissionProvider defaultProvider = new AggregatingResourcePermissionProvider<>([new Front50ApplicationResourcePermissionSource(Authorization.READ)]) - @Subject DefaultApplicationProvider provider + @Subject DefaultApplicationResourceProvider provider def makePerms(Map> auths) { return Permissions.Builder.factory(auths).build() @@ -50,7 +51,6 @@ class DefaultApplicationProviderSpec extends Specification { new Application().setName("app1") .setPermissions(new Permissions.Builder().add(Authorization.READ, "role").build()), ] - } ClouddriverService clouddriverService = Mock(ClouddriverService) { getApplications() >> [ @@ -59,7 +59,7 @@ class DefaultApplicationProviderSpec extends Specification { ] } - provider = new DefaultApplicationProvider(front50Service, clouddriverService, allowAccessToUnknownApplications, Authorization.READ) + provider = new DefaultApplicationResourceProvider(front50Service, clouddriverService, defaultProvider, allowAccessToUnknownApplications) when: def restrictedResult = provider.getAllRestricted([new Role(role)] as Set, false) @@ -91,9 +91,8 @@ class DefaultApplicationProviderSpec extends Specification { when: app.setPermissions(makePerms(givenPermissions)) - provider = new DefaultApplicationProvider( - front50Service, clouddriverService, allowAccessToUnknownApplications, Authorization.READ - ) + provider = new DefaultApplicationResourceProvider( + front50Service, clouddriverService, defaultProvider, allowAccessToUnknownApplications) def resultApps = provider.getAll() then: @@ -106,19 +105,20 @@ class DefaultApplicationProviderSpec extends Specification { where: givenPermissions | allowAccessToUnknownApplications || expectedPermissions [:] | false || [:] - [(R): ['r']] | false || [(R): ['r'], (W): [], (E): ['r']] - [(R): ['r'], (E): ['foo']] | false || [(R): ['r'], (W): [], (E): ['foo']] - [(R): ['r']] | true || [(R): ['r'], (W): [], (E): ['r']] + [(R): ['r']] | false || [(R): ['r'], (E): ['r']] + [(R): ['r'], (E): ['foo']] | false || [(R): ['r'], (E): ['foo']] + [(R): ['r']] | true || [(R): ['r'], (E): ['r']] } @Unroll def "should add fallback execute permissions based on executeFallback value" () { setup: def app = new Application().setName("app") + def provider = new AggregatingResourcePermissionProvider([new Front50ApplicationResourcePermissionSource(fallback)]) when: app.setPermissions(makePerms(givenPermissions)) - provider = new DefaultApplicationProvider(front50Service, clouddriverService, false, fallback) + provider = new DefaultApplicationResourceProvider(front50Service, clouddriverService, provider, false) def resultApps = provider.getAll() then: @@ -129,8 +129,9 @@ class DefaultApplicationProviderSpec extends Specification { makePerms(expectedPermissions) == resultApps.permissions[0] where: - fallback || givenPermissions || expectedPermissions - R || [(R): ['r']] || [(R): ['r'], (W): [], (E): ['r']] - W || [(R): ['r'], (W): ['w']] || [(R): ['r'], (W): ['w'], (E): ['w']] + fallback | givenPermissions || expectedPermissions + R | [:] || [:] + R | [(R): ['r']] || [(R): ['r'], (E): ['r']] + W | [(R): ['r'], (W): ['w']] || [(R): ['r'], (W): ['w'], (E): ['w']] } } diff --git a/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/providers/DefaultServiceAccountProviderSpec.groovy b/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/providers/DefaultServiceAccountProviderSpec.groovy index 8e5f8ab1e..6e383dba9 100644 --- a/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/providers/DefaultServiceAccountProviderSpec.groovy +++ b/fiat-roles/src/test/groovy/com/netflix/spinnaker/fiat/providers/DefaultServiceAccountProviderSpec.groovy @@ -23,7 +23,6 @@ import com.netflix.spinnaker.fiat.providers.internal.Front50Service import org.apache.commons.collections4.CollectionUtils import spock.lang.Shared import spock.lang.Specification -import spock.lang.Subject import spock.lang.Unroll class DefaultServiceAccountProviderSpec extends Specification { @@ -48,7 +47,7 @@ class DefaultServiceAccountProviderSpec extends Specification { FiatRoleConfig fiatRoleConfig = Mock(FiatRoleConfig) { isOrMode() >> false } - DefaultServiceAccountProvider provider = new DefaultServiceAccountProvider(front50Service, fiatRoleConfig) + DefaultServiceAccountResourceProvider provider = new DefaultServiceAccountResourceProvider(front50Service, fiatRoleConfig) when: def result = provider.getAllRestricted(input.collect { new Role(it) } as Set, isAdmin) @@ -80,7 +79,7 @@ class DefaultServiceAccountProviderSpec extends Specification { FiatRoleConfig fiatRoleConfig = Mock(FiatRoleConfig) { isOrMode() >> true } - DefaultServiceAccountProvider provider = new DefaultServiceAccountProvider(front50Service, fiatRoleConfig) + DefaultServiceAccountResourceProvider provider = new DefaultServiceAccountResourceProvider(front50Service, fiatRoleConfig) when: def result = provider.getAllRestricted(input.collect { new Role(it) } as Set, isAdmin) diff --git a/fiat-web/src/main/java/com/netflix/spinnaker/fiat/config/DefaultResourcePermissionConfig.java b/fiat-web/src/main/java/com/netflix/spinnaker/fiat/config/DefaultResourcePermissionConfig.java new file mode 100644 index 000000000..84b194480 --- /dev/null +++ b/fiat-web/src/main/java/com/netflix/spinnaker/fiat/config/DefaultResourcePermissionConfig.java @@ -0,0 +1,109 @@ +/* + * Copyright 2019 Google, LLC + * + * 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. + */ + +package com.netflix.spinnaker.fiat.config; + +import com.netflix.spinnaker.fiat.model.resources.Account; +import com.netflix.spinnaker.fiat.model.resources.Application; +import com.netflix.spinnaker.fiat.model.resources.BuildService; +import com.netflix.spinnaker.fiat.providers.*; +import java.util.List; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +class DefaultResourcePermissionConfig { + + @Bean + @ConditionalOnProperty( + value = "auth.permissions.source.account.resource.enabled", + matchIfMissing = true) + ResourcePermissionSource accountResourcePermissionSource() { + return new AccessControlledResourcePermissionSource<>(); + } + + @Bean + @ConditionalOnProperty( + value = "auth.permissions.provider.account", + havingValue = "default", + matchIfMissing = true) + public ResourcePermissionProvider defaultAccountPermissionProvider( + ResourcePermissionSource accountResourcePermissionSource) { + return new DefaultResourcePermissionProvider<>(accountResourcePermissionSource); + } + + @Bean + @ConditionalOnProperty(value = "auth.permissions.provider.account", havingValue = "aggregate") + public ResourcePermissionProvider aggregateAccountPermissionProvider( + List> sources) { + return new AggregatingResourcePermissionProvider<>(sources); + } + + @Bean + @ConditionalOnProperty( + value = "auth.permissions.source.application.front50.enabled", + matchIfMissing = true) + ResourcePermissionSource front50ResourcePermissionSource( + FiatServerConfigurationProperties fiatServerConfigurationProperties) { + return new Front50ApplicationResourcePermissionSource( + fiatServerConfigurationProperties.getExecuteFallback()); + } + + @Bean + @ConditionalOnProperty( + value = "auth.permissions.provider.application", + havingValue = "default", + matchIfMissing = true) + public ResourcePermissionProvider defaultApplicationPermissionProvider( + ResourcePermissionSource front50ResourcePermissionSource) { + return new DefaultResourcePermissionProvider<>(front50ResourcePermissionSource); + } + + @Bean + @ConditionalOnProperty(value = "auth.permissions.provider.application", havingValue = "aggregate") + public ResourcePermissionProvider aggregateApplicationPermissionProvider( + List> sources) { + return new AggregatingResourcePermissionProvider<>(sources); + } + + @Bean + @ConditionalOnProperty( + value = "auth.permissions.source.build-service.resource.enabled", + matchIfMissing = true) + ResourcePermissionSource buildServiceResourcePermissionSource() { + return new AccessControlledResourcePermissionSource<>(); + } + + @Bean + @ConditionalOnProperty( + value = "auth.permissions.provider.build-service", + havingValue = "default", + matchIfMissing = true) + public ResourcePermissionProvider defaultBuildServicePermissionProvider( + ResourcePermissionSource buildServiceResourcePermissionSource) { + return new DefaultResourcePermissionProvider<>(buildServiceResourcePermissionSource); + } + + @Bean + @ConditionalOnProperty( + value = "auth.permissions.provider.build-service", + havingValue = "aggregate") + public ResourcePermissionProvider aggregateBuildServicePermissionProvider( + List> sources) { + return new AggregatingResourcePermissionProvider<>(sources); + } +} diff --git a/fiat-web/src/main/java/com/netflix/spinnaker/fiat/config/FiatConfig.java b/fiat-web/src/main/java/com/netflix/spinnaker/fiat/config/FiatConfig.java index 80de71828..016dabd33 100644 --- a/fiat-web/src/main/java/com/netflix/spinnaker/fiat/config/FiatConfig.java +++ b/fiat-web/src/main/java/com/netflix/spinnaker/fiat/config/FiatConfig.java @@ -2,9 +2,11 @@ import com.google.common.collect.ImmutableList; import com.netflix.spectator.api.Registry; +import com.netflix.spinnaker.fiat.model.resources.Application; import com.netflix.spinnaker.fiat.model.resources.Role; import com.netflix.spinnaker.fiat.permissions.ExternalUser; -import com.netflix.spinnaker.fiat.providers.DefaultApplicationProvider; +import com.netflix.spinnaker.fiat.providers.DefaultApplicationResourceProvider; +import com.netflix.spinnaker.fiat.providers.ResourcePermissionProvider; import com.netflix.spinnaker.fiat.providers.internal.ClouddriverService; import com.netflix.spinnaker.fiat.providers.internal.Front50Service; import com.netflix.spinnaker.fiat.roles.UserRolesProvider; @@ -68,15 +70,16 @@ public List loadRoles(ExternalUser user) { } @Bean - DefaultApplicationProvider applicationProvider( + DefaultApplicationResourceProvider applicationProvider( Front50Service front50Service, ClouddriverService clouddriverService, + ResourcePermissionProvider permissionProvider, FiatServerConfigurationProperties properties) { - return new DefaultApplicationProvider( + return new DefaultApplicationResourceProvider( front50Service, clouddriverService, - properties.isAllowAccessToUnknownApplications(), - properties.getExecuteFallback()); + permissionProvider, + properties.isAllowAccessToUnknownApplications()); } /**