Skip to content

Commit

Permalink
feat(security): Add more Fiat abstractions (#1138)
Browse files Browse the repository at this point in the history
* feat(security): Add more Fiat abstractions

Related to spinnaker/spinnaker#6911

This adds a copy of the Authorization enum from fiat-core, an AbstractPermissionEvaluator base class which fiat-api will be updated to use, some extensions to AccessControlled, and some other security utility functions.

* fix(security): Make Authorization parsing robust
  • Loading branch information
jvz authored Dec 20, 2023
1 parent c53be8f commit 0c80746
Show file tree
Hide file tree
Showing 11 changed files with 617 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright 2023 Apple, 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.security;

import java.io.Serializable;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;

/**
* Base implementation for permission evaluators that support {@link AccessControlled} domain
* objects.
*/
public abstract class AbstractPermissionEvaluator implements PermissionEvaluator {

@Override
public boolean hasPermission(
Authentication authentication, Object targetDomainObject, Object permission) {
if (isDisabled()) {
return true;
}
if (authentication == null || targetDomainObject == null) {
return false;
}
if (SpinnakerAuthorities.isAdmin(authentication)) {
return true;
}
if (targetDomainObject instanceof AccessControlled) {
return ((AccessControlled) targetDomainObject).isAuthorized(authentication, permission);
}
return false;
}

@Override
public boolean hasPermission(
Authentication authentication, Serializable targetId, String targetType, Object permission) {
if (isDisabled()) {
return true;
}
return hasPermission(
SpinnakerUsers.getUserId(authentication), targetId, targetType, permission);
}

/**
* Indicates whether permission evaluation is disabled. When this is true, {@code hasPermission}
* calls should return true. This should be overridden to allow for toggling this evaluator at
* runtime.
*/
protected boolean isDisabled() {
return false;
}

/**
* Alternative method for evaluating a permission where only the identifier of the user and target
* object is available, rather than the authenticated user and target objects themselves.
*
* @param username identifier for user to check permissions for
* @param targetId identifier of the target resource to check permissions
* @param targetType the type of the target resource being checked
* @param permission the permission being validated
* @return true if the permission is granted
*/
public abstract boolean hasPermission(
String username, Serializable targetId, String targetType, Object permission);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,26 @@

package com.netflix.spinnaker.security;

import java.util.Collection;
import org.springframework.security.core.Authentication;

/**
* An AccessControlled object is an object that knows its own permissions and can check them against
* a given user and authorization. This allows resources to support access control checks via Spring
* Security against the resource object directly.
*
* @see AbstractPermissionEvaluator
*/
public interface AccessControlled {
/**
* Checks if the authenticated user has a particular authorization on this object. Note that
* checking if the user is an admin should be performed by a {@link
* org.springframework.security.access.PermissionEvaluator} rather than in these domain objects.
* org.springframework.security.access.PermissionEvaluator} or by checking {@link
* SpinnakerAuthorities#isAdmin(Authentication)} rather than via this method as the admin role is
* a Spinnaker-specific role.
*
* @see Authorization
* @see SpinnakerAuthorities#hasAnyRole(Authentication, Collection)
*/
boolean isAuthorized(Authentication authentication, Object authorization);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2022 Apple 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.security;

import java.util.Locale;
import javax.annotation.Nullable;
import org.springframework.security.core.Authentication;

/**
* Defines types of authorizations supported by {@link AccessControlled#isAuthorized(Authentication,
* Object)}.
*/
public enum Authorization {
READ,
WRITE,
EXECUTE,
CREATE,
;

public static @Nullable Authorization parse(@Nullable Object o) {
if (o == null) {
return null;
}
String name = o.toString().toUpperCase(Locale.ROOT);
try {
return valueOf(name);
} catch (IllegalArgumentException e) {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2023 Apple, 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.security;

import javax.annotation.Nullable;

/**
* Common interface for access-controlled classes which use a permission map of {@link
* Authorization} enums.
*/
public interface AuthorizationMapControlled extends PermissionMapControlled<Authorization> {
@Nullable
@Override
default Authorization valueOf(@Nullable Object authorization) {
return Authorization.parse(authorization);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2023 Apple 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.security;

import com.netflix.spinnaker.kork.annotations.Alpha;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.springframework.security.core.Authentication;

/**
* Common interface for access-controlled classes which use a permission map.
*
* @param <Authorization> Authorization enum type
*/
@Alpha
public interface PermissionMapControlled<Authorization extends Enum<Authorization>>
extends AccessControlled {
@Nullable
Authorization valueOf(@Nullable Object authorization);

@Nonnull
default Map<Authorization, Set<String>> getPermissions() {
return Map.of();
}

@Override
default boolean isAuthorized(Authentication authentication, Object authorization) {
Authorization auth = valueOf(authorization);
if (auth == null) {
return false;
}
Set<String> permittedRoles = getPermissions().getOrDefault(auth, Set.of());
return permittedRoles.isEmpty()
|| SpinnakerAuthorities.hasAnyRole(authentication, permittedRoles);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2022 Apple, 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.security;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

/**
* Constants and utilities for working with Spring Security GrantedAuthority objects specific to
* Spinnaker and Fiat. Spinnaker-specific roles are represented here as granted authorities with the
* {@code SPINNAKER_} prefix.
*/
public class SpinnakerAuthorities {
private static final String ROLE_PREFIX = "ROLE_";

public static final String ADMIN = "SPINNAKER_ADMIN";
/** Granted authority for Spinnaker administrators. */
public static final GrantedAuthority ADMIN_AUTHORITY = new SimpleGrantedAuthority(ADMIN);

/** Granted authority for anonymous users. */
public static final GrantedAuthority ANONYMOUS_AUTHORITY = forRoleName("ANONYMOUS");

/** Creates a granted authority corresponding to the provided name of a role. */
@Nonnull
public static GrantedAuthority forRoleName(@Nonnull String role) {
return new SimpleGrantedAuthority(ROLE_PREFIX + role);
}

/** Checks if the given user is a Spinnaker admin. */
public static boolean isAdmin(@Nullable Authentication authentication) {
return authentication != null
&& authentication.getAuthorities().contains(SpinnakerAuthorities.ADMIN_AUTHORITY);
}

/** Checks if the given user has the provided role. */
public static boolean hasRole(@Nullable Authentication authentication, @Nonnull String role) {
return authentication != null && streamRoles(authentication).anyMatch(role::equals);
}

/** Checks if the given user has any of the provided roles. */
public static boolean hasAnyRole(
@Nullable Authentication authentication, @Nonnull Collection<String> roles) {
return authentication != null && streamRoles(authentication).anyMatch(roles::contains);
}

/** Gets the list of roles assigned to the given user. */
@Nonnull
public static List<String> getRoles(@Nullable Authentication authentication) {
if (authentication == null) {
return List.of();
}
return streamRoles(authentication).distinct().collect(Collectors.toList());
}

@Nonnull
private static Stream<String> streamRoles(@Nonnull Authentication authentication) {
return authentication.getAuthorities().stream()
.filter(SpinnakerAuthorities::isRole)
.map(SpinnakerAuthorities::getRole);
}

private static boolean isRole(@Nonnull GrantedAuthority authority) {
return authority.getAuthority().startsWith(ROLE_PREFIX);
}

private static String getRole(@Nonnull GrantedAuthority authority) {
return authority.getAuthority().substring(ROLE_PREFIX.length());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2023 Apple, 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.security;

import com.netflix.spinnaker.kork.annotations.NonnullByDefault;
import javax.annotation.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

/** Constants and utilities related to Spinnaker users (AKA principals). */
@NonnullByDefault
public class SpinnakerUsers {
/** String constant for the anonymous userid. */
public static final String ANONYMOUS = "anonymous";

/** Gets the userid of the provided authentication token. */
public static String getUserId(@Nullable Authentication authentication) {
return authentication != null ? authentication.getName() : ANONYMOUS;
}

/** Gets the current Spinnaker userid. */
public static String getCurrentUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
return getUserId(authentication);
}
// fall back to request header context if relevant (AuthenticatedRequestFilter)
return AuthenticatedRequest.getSpinnakerUser().orElse(ANONYMOUS);
}
}
Loading

0 comments on commit 0c80746

Please sign in to comment.