Skip to content

Commit

Permalink
feat(authorization): implement application prefix permission source (#…
Browse files Browse the repository at this point in the history
…490)

A ResourcePermissionSource that takes resource names or prefixes and supplies Permissions.

This is currently only wired up for Application permissions.

Enable by setting `auth.permissions.source.application.prefix.enabled: true`

Config looks like:
```
auth.permissions.source.application.prefix:
  enabled: true
  prefixes:
    - prefix: "fooapp"
      permissions:
        READ: 
          - "foo-readers-role@mycompany.org"
    - prefix: "bar*"
      permissions:
        CREATE:
          - "bar-ops-team@mycompany.org"
```

In the example above, there are two prefixes defined:

* `fooapp` - this is an exact match to an application named `fooapp`
* `bar*` - this will match any application that starts with `bar`

An additional configuration option exists:
`auth.permissions.source.application.prefix.resolutionStrategy` supporting either `AGGREGATE` (the default) or `MOST_SPECIFIC`

* `AGGREGATE`: the resulting Permissions will contain all values that matched the supplied app (if there are multiple prefixes that match, combine all the permissions - e.g `bar*` and `bardev*` for an app named `bardev01`)
* `BEST_MATCH`: the resulting Permissions will contain the value from the longest prefix match or exact match for the app name (for `bar*` and `bardev*` select the Permissions from `bardev*` for the app named `bardev01`
  • Loading branch information
AbdulRahmanAlHamali authored and cfieber committed Oct 28, 2019
1 parent b8f8ce7 commit 37ac06d
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* 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 java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import lombok.Data;
import org.springframework.util.StringUtils;

@Data
public class ResourcePrefixPermissionSource<T extends Resource.AccessControlled>
implements ResourcePermissionSource<T> {

public enum ResolutionStrategy {
AGGREGATE,
MOST_SPECIFIC
}

private List<PrefixEntry<T>> prefixes;
private ResolutionStrategy resolutionStrategy = ResolutionStrategy.AGGREGATE;

@Data
public static class PrefixEntry<T extends Resource.AccessControlled> {
private String prefix;
private Permissions permissions;

private boolean isFullApplicationName;

public PrefixEntry setPrefix(String prefix) {
if (StringUtils.isEmpty(prefix)) {
throw new IllegalArgumentException(
"Prefix must either end with *, or be a full application name");
}
isFullApplicationName = !prefix.endsWith("*");
this.prefix = prefix;

return this;
}

public PrefixEntry setPermissions(Map<Authorization, List<String>> permissions) {
this.permissions = Permissions.factory(permissions);
return this;
}

public boolean contains(T resource) {
if (isFullApplicationName) {
return prefix.equals(resource.getName());
}

String prefixWithoutStar = prefix.substring(0, prefix.length() - 1);
return resource.getName().startsWith(prefixWithoutStar);
}
}

@Nonnull
@Override
public Permissions getPermissions(@Nonnull T resource) {

List<PrefixEntry<T>> matchingPrefixes =
prefixes.stream().filter(prefix -> prefix.contains(resource)).collect(Collectors.toList());

if (matchingPrefixes.isEmpty()) {
return Permissions.EMPTY;
}

switch (resolutionStrategy) {
case AGGREGATE:
return getAggregatePermissions(matchingPrefixes);
case MOST_SPECIFIC:
return getMostSpecificPermissions(matchingPrefixes);
default:
throw new IllegalStateException(
"Unrecognized Resolution Stratgey " + resolutionStrategy.name());
}
}

private Permissions getAggregatePermissions(List<PrefixEntry<T>> matchingPrefixes) {
Permissions.Builder builder = new Permissions.Builder();
for (PrefixEntry<T> prefix : matchingPrefixes) {
Permissions permissions = prefix.getPermissions();
if (permissions.isRestricted()) {
for (Authorization auth : Authorization.values()) {
builder.add(auth, permissions.get(auth));
}
}
}

return builder.build();
}

private Permissions getMostSpecificPermissions(List<PrefixEntry<T>> matchingPrefixes) {
return matchingPrefixes.stream()
.min(
(p1, p2) -> {
if (p1.isFullApplicationName()) {
return -1;
}
return p2.getPrefix().length() - p1.getPrefix().length();
})
.get()
.getPermissions();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* 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.Application
import spock.lang.Specification

class ResourcePrefixPermissionSourceSpec extends Specification {

def "should aggregate permissions matching a resource correctly if resolution strategy is aggregate"() {
given:
def source = new ResourcePrefixPermissionSource<Application>().setPrefixes([
new ResourcePrefixPermissionSource.PrefixEntry<Application>().setPrefix('*').setPermissions([
(Authorization.CREATE): ['admins']
]),
new ResourcePrefixPermissionSource.PrefixEntry<Application>().setPrefix('gotham*').setPermissions([
(Authorization.CREATE): ['police']
]),
new ResourcePrefixPermissionSource.PrefixEntry<Application>().setPrefix('gotham-joker').setPermissions([
(Authorization.CREATE): ['batman']
]),
]).setResolutionStrategy(ResourcePrefixPermissionSource.ResolutionStrategy.AGGREGATE)

when:
def application = new Application().setName(applicationName)

then:
source.getPermissions(application).get(Authorization.CREATE) as Set == expectedCreatePermissions as Set

where:
applicationName | expectedCreatePermissions
'new york' | ["admins"]
'gotham-criminals' | ["admins", "police"]
'gotham-joker' | ["admins", "police", "batman"]
}

def "should apply the most specific permissions matching a resource if resolution strategy is most_specific"() {
given:
def source = new ResourcePrefixPermissionSource<Application>().setPrefixes([
new ResourcePrefixPermissionSource.PrefixEntry<Application>().setPrefix('*').setPermissions([
(Authorization.CREATE): ['admins']
]),
new ResourcePrefixPermissionSource.PrefixEntry<Application>().setPrefix('gotham*').setPermissions([
(Authorization.CREATE): ['police']
]),
new ResourcePrefixPermissionSource.PrefixEntry<Application>().setPrefix('gotham-joker').setPermissions([
(Authorization.CREATE): ['batman']
]),
]).setResolutionStrategy(ResourcePrefixPermissionSource.ResolutionStrategy.MOST_SPECIFIC)

when:
def application = new Application().setName(applicationName)

then:
source.getPermissions(application).get(Authorization.CREATE) as Set == expectedCreatePermissions as Set

where:
applicationName | expectedCreatePermissions
'new york' | ["admins"]
'gotham-criminals' | ["police"]
'gotham-joker' | ["batman"]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.netflix.spinnaker.fiat.providers.*;
import java.util.List;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

Expand Down Expand Up @@ -53,6 +54,13 @@ public ResourcePermissionProvider<Account> aggregateAccountPermissionProvider(
return new AggregatingResourcePermissionProvider<>(sources);
}

@Bean
@ConditionalOnProperty("auth.permissions.source.application.prefix.enabled")
@ConfigurationProperties("auth.permissions.source.application.prefix")
ResourcePermissionSource<Application> applicationPrefixResourcePermissionSource() {
return new ResourcePrefixPermissionSource<Application>();
}

@Bean
@ConditionalOnProperty(
value = "auth.permissions.source.application.front50.enabled",
Expand Down

0 comments on commit 37ac06d

Please sign in to comment.