Skip to content

Commit

Permalink
Merge pull request spinnaker#2 in SPKR/clouddriver-nflx from validate…
Browse files Browse the repository at this point in the history
…-iam-roles to master

* commit '5d678599ce4a07c42ffed982e6e72b6e0d5a6c73':
  Support for validating IAM Role access against a tag
  • Loading branch information
ajordens committed Nov 4, 2016
2 parents d7a9882 + 5d67859 commit 606c2fb
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,37 @@ import com.netflix.frigga.Names
import com.netflix.spinnaker.clouddriver.aws.deploy.description.BasicAmazonDeployDescription
import com.netflix.spinnaker.clouddriver.aws.security.AmazonClientProvider
import com.netflix.spinnaker.clouddriver.aws.security.NetflixAmazonCredentials
import com.netflix.spinnaker.clouddriver.core.services.Front50Service
import com.netflix.spinnaker.clouddriver.helpers.OperationPoller
import com.netflix.spinnaker.clouddriver.model.EntityTags
import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperationDescriptionPreProcessor
import com.netflix.spinnaker.clouddriver.security.AccountCredentialsProvider
import groovy.util.logging.Slf4j
import retrofit.RetrofitError

@Slf4j
class ApplicationSpecificIamRoleDescriptionPreProcessor implements AtomicOperationDescriptionPreProcessor {
private static final int BACKOFF = 1000
private static final int MAX_RETRIES = 4

static final String LAMBDA_ACCOUNT = "mgmt"
static final String LAMBDA_REGION = "us-west-2"
static final String LAMBDA_NAME = "ApplicationRoleCreator"
static final String SPINNAKER_ALLOWED_USAGE_TAG = "spinnaker_allowed_usage"

final ObjectMapper objectMapper
final AmazonClientProvider amazonClientProvider
final AccountCredentialsProvider accountCredentialsProvider
final Front50Service front50Service

ApplicationSpecificIamRoleDescriptionPreProcessor(ObjectMapper objectMapper,
AccountCredentialsProvider accountCredentialsProvider,
AmazonClientProvider amazonClientProvider) {
AmazonClientProvider amazonClientProvider,
Front50Service front50Service) {
this.objectMapper = objectMapper
this.accountCredentialsProvider = accountCredentialsProvider
this.amazonClientProvider = amazonClientProvider
this.front50Service = front50Service
}

@Override
Expand All @@ -59,6 +70,9 @@ class ApplicationSpecificIamRoleDescriptionPreProcessor implements AtomicOperati
def iamRole = preProcessorData.iamRole
def applicationSpecificRole = buildApplicationSpecificRoleName(preProcessorData.application)

def credentials = preProcessorData.credentials
def amazonCredentials = accountCredentialsProvider.getCredentials(credentials) as NetflixAmazonCredentials

if (iamRole == null) {
// default to `applicationSpecificRole` if one wasn't provided in the request
// iamRole = applicationSpecificRole
Expand All @@ -70,9 +84,6 @@ class ApplicationSpecificIamRoleDescriptionPreProcessor implements AtomicOperati
* If the target role is BaseIAMRole, than create the application-specific role
* If the target role is application-specific, ensure it exists (and create it if missing)
*/
def credentials = preProcessorData.credentials
def amazonCredentials = accountCredentialsProvider.getCredentials(credentials) as NetflixAmazonCredentials

def region = preProcessorData.region
def identityManagement = amazonClientProvider.getAmazonIdentityManagement(amazonCredentials, region, false)

Expand Down Expand Up @@ -105,6 +116,10 @@ class ApplicationSpecificIamRoleDescriptionPreProcessor implements AtomicOperati
}
}
}

validateIamRole(
front50Service, amazonCredentials, preProcessorData.application, description.iamRole as String, BACKOFF, MAX_RETRIES
)
return description
}

Expand Down Expand Up @@ -172,6 +187,72 @@ class ApplicationSpecificIamRoleDescriptionPreProcessor implements AtomicOperati
return application.toLowerCase() + "InstanceProfile"
}

/**
* Check for the existence of `spinnaker_allowed_usage` tag against the target IAM Role.
*
* If the tag exists, ensure that the current application is in the set of allowed applications.
*/
static void validateIamRole(Front50Service front50Service,
NetflixAmazonCredentials credentials,
String application,
String iamRole,
int backOff,
int maxRetries) {
if (iamRole.equalsIgnoreCase(buildApplicationSpecificRoleName(application))) {
// an application can _always_ launch with it's application-specific IAM role, no need to explicitly check
return
}

try {
def entityTagId = "aws:iamrole:${iamRole}:${credentials.accountId}:*".toLowerCase()
def entityTagsOptional = getEntityTags(front50Service, entityTagId, backOff, maxRetries)
if (!entityTagsOptional.isPresent()) {
// no EntityTags ... should short-circuit
return
}

def spinnakerUsageTag = entityTagsOptional.get().tags?.get(SPINNAKER_ALLOWED_USAGE_TAG) as Map
if (!spinnakerUsageTag) {
return;
}

if (!spinnakerUsageTag.containsKey("applications")) {
return
}

def allowedApplications = spinnakerUsageTag.get("applications") as Set<String>
if (allowedApplications.contains(application)) {
return
}

throw new InvalidIamRoleException("Unable to launch with IAM Role '${iamRole}' (application: ${application}, account: ${credentials})")
} catch (RetrofitError e) {
if (e?.response?.status == 404) {
return;
}

throw e
}
}

static Optional<EntityTags> getEntityTags(Front50Service front50Service, String id, int backOff, int maxRetries) {
Optional<EntityTags> entityTags = Optional.empty()

OperationPoller.retryWithBackoff({ o ->
try {
entityTags = Optional.ofNullable(front50Service.getEntityTags(id))
} catch (RetrofitError e) {
if (e?.response?.status == 404) {
return
}

throw e
}
}, backOff, maxRetries)

return entityTags
}

static class PreProcessorData {
String application
String credentials
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2016 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.clouddriver.internal

import groovy.transform.InheritConstructors

@InheritConstructors
class InvalidIamRoleException extends RuntimeException {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2016 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.clouddriver.internal

import groovy.util.logging.Slf4j
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController

@Slf4j
@RestController
@ControllerAdvice
class InvalidIamRoleExceptionHandler {
@ExceptionHandler(InvalidIamRoleException)
@ResponseStatus(HttpStatus.BAD_REQUEST)
Map handleInvalidIamRoleException(InvalidIamRoleException ex) {
log.error(ex.message)
return [error: "iamRole.invalid", message: ex.message, status: HttpStatus.BAD_REQUEST]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package com.netflix.spinnaker.config

import com.fasterxml.jackson.databind.ObjectMapper
import com.netflix.spinnaker.clouddriver.aws.security.AmazonClientProvider
import com.netflix.spinnaker.clouddriver.core.services.Front50Service
import com.netflix.spinnaker.clouddriver.internal.InvalidIamRoleExceptionHandler
import com.netflix.spinnaker.clouddriver.security.AccountCredentialsProvider
import com.netflix.spinnaker.clouddriver.internal.ApplicationSpecificIamRoleDescriptionPreProcessor
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression
Expand All @@ -32,10 +34,16 @@ class NetflixConfiguration {
ApplicationSpecificIamRoleDescriptionPreProcessor applicationSpecificIamRoleDescriptionPreProcessor(
ObjectMapper objectMapper,
AccountCredentialsProvider accountCredentialsProvider,
AmazonClientProvider amazonClientProvider
AmazonClientProvider amazonClientProvider,
Front50Service front50Service
) {
return new ApplicationSpecificIamRoleDescriptionPreProcessor(
objectMapper, accountCredentialsProvider, amazonClientProvider
objectMapper, accountCredentialsProvider, amazonClientProvider, front50Service
)
}

@Bean
InvalidIamRoleExceptionHandler invalidIamRoleExceptionHandler() {
return new InvalidIamRoleExceptionHandler()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import com.amazonaws.services.lambda.model.InvokeResult
import com.fasterxml.jackson.databind.ObjectMapper
import com.netflix.spinnaker.clouddriver.aws.security.AmazonClientProvider
import com.netflix.spinnaker.clouddriver.aws.security.NetflixAmazonCredentials
import com.netflix.spinnaker.clouddriver.core.services.Front50Service
import com.netflix.spinnaker.clouddriver.model.EntityTags
import com.netflix.spinnaker.clouddriver.security.AccountCredentialsProvider
import spock.lang.Specification
import spock.lang.Unroll
Expand Down Expand Up @@ -55,6 +57,7 @@ class ApplicationSpecificIamRoleDescriptionPreProcessorSpec extends Specificatio
return amazonIdentityManagement
}
}
def front50Service = Mock(Front50Service)

static Map baseIamDescription = [
application : "application",
Expand All @@ -72,7 +75,7 @@ class ApplicationSpecificIamRoleDescriptionPreProcessorSpec extends Specificatio
def "should replace iamRole when iamRole == 'BaseIAMRole'"() {
given:
def preProcessor = new ApplicationSpecificIamRoleDescriptionPreProcessor(
new ObjectMapper(), accountCredentialsProvider, amazonClientProvider
new ObjectMapper(), accountCredentialsProvider, amazonClientProvider, front50Service
)

when:
Expand All @@ -86,7 +89,7 @@ class ApplicationSpecificIamRoleDescriptionPreProcessorSpec extends Specificatio
description || expectedProcessedDescription || expectedInvocationCount
baseIamDescription || baseIamDescription + [iamRole: "applicationInstanceProfile"] || 1 // should create the application-specific instance profile
applicationIamDescription || applicationIamDescription || 1 // should verify the application-specific instance profile exists
randomIamDescription || randomIamDescription || 0 // should do nothing (non application-specific instance profile provided)
randomIamDescription || randomIamDescription || 1 // should do nothing (non application-specific instance profile provided)
}

@Unroll
Expand Down Expand Up @@ -173,4 +176,48 @@ class ApplicationSpecificIamRoleDescriptionPreProcessorSpec extends Specificatio
4 | 1 || true
1 | 1 || true
}

def "should short circuit validation when application is launch with it's application-specific role"() {
when:
validateIamRole(null, null, "front50", "front50InstanceProfile", 0, 0)

then:
notThrown(NullPointerException)

when:
validateIamRole(null, null, "front50", "spinnakerInstanceProfile", 0, 0)

then:
thrown(NullPointerException)
}

@Unroll
def "should validate access to IAM Role against `spinnaker_allowed_usage` tag "() {
given:
1 * netflixAmazonCredentials.getAccountId() >> "1"
1 * front50Service.getEntityTags("aws:iamrole:spinnakerinstanceprofile:1:*") >> entityTags

expect:
try {
validateIamRole(front50Service, netflixAmazonCredentials, "front50", "spinnakerInstanceProfile", 1, 5)
assert !exceptionExpected
} catch (InvalidIamRoleException ignored) {
assert exceptionExpected
}

where:
entityTags || exceptionExpected
null || false
entityTags(null) || false
entityTags([:]) || false
entityTags(["random_tag": "random_value"]) || false
entityTags(["spinnaker_allowed_usage": [:]]) || false
entityTags(["spinnaker_allowed_usage": [applications: ["front50"]]]) || false
entityTags(["spinnaker_allowed_usage": [applications: []]]) || true
entityTags(["spinnaker_allowed_usage": [applications: ["clouddriver"]]]) || true
}

static EntityTags entityTags(Map tags) {
return new EntityTags(tags: tags)
}
}

0 comments on commit 606c2fb

Please sign in to comment.