Skip to content

Commit

Permalink
feat(serviceaccounts): Delete dangling service accounts migration (#872)
Browse files Browse the repository at this point in the history
Earlier versions of Front50 did not delete managed service accounts when
the corresponding pipeline or application was deleted. This was fixed in
PR #769, but that change was not retroactive. This PR adds a migration
that will cleanup managed unused (dangling) service accounts.

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
parkerlarry and mergify[bot] committed Jul 2, 2020
1 parent 53109aa commit 8d4a357
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright 2020 Schibsted ASA.
*
* 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.front50.migrations;

import com.netflix.spinnaker.front50.config.annotations.ConditionalOnAnyProviderExceptRedisIsEnabled;
import com.netflix.spinnaker.front50.model.pipeline.PipelineDAO;
import com.netflix.spinnaker.front50.model.serviceaccount.ServiceAccount;
import com.netflix.spinnaker.front50.model.serviceaccount.ServiceAccountDAO;
import java.time.LocalDate;
import java.time.Month;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@ConditionalOnAnyProviderExceptRedisIsEnabled
public class DeleteDanglingServiceAccountsMigration implements Migration {

// Only valid until December 31, 2020
private static final LocalDate VALID_UNTIL = LocalDate.of(2020, Month.DECEMBER, 31);

private static final String SERVICE_ACCOUNT_SUFFIX = "@managed-service-account";
private static final String RUN_AS_USER = "runAsUser";

private final PipelineDAO pipelineDAO;
private final ServiceAccountDAO serviceAccountDAO;

@Autowired
public DeleteDanglingServiceAccountsMigration(
PipelineDAO pipelineDAO, ServiceAccountDAO serviceAccountDAO) {
this.pipelineDAO = pipelineDAO;
this.serviceAccountDAO = serviceAccountDAO;
}

@Override
public boolean isValid() {
return LocalDate.now().isBefore(VALID_UNTIL);
}

@Override
public void run() {
log.info(
"Starting deletion of dangling service accounts ({})", this.getClass().getSimpleName());

Set<String> serviceAccountsToKeep =
pipelineDAO
.all()
.parallelStream()
.flatMap(
pipeline ->
pipeline.getTriggers().stream()
.map(trigger -> (String) trigger.get(RUN_AS_USER))
.filter(isManagedServiceAccount())
.distinct())
.collect(Collectors.toSet());

serviceAccountDAO
.all()
.parallelStream()
.map(ServiceAccount::getName)
.filter(isManagedServiceAccount())
.filter(serviceAccount -> !serviceAccountsToKeep.contains(serviceAccount))
.peek(
serviceAccount ->
log.info(
"Deleting managed service account '{}' because it is not in use",
serviceAccount))
.forEach(serviceAccountDAO::delete);

log.info("Finished deletion of dangling service accounts ");
}

private Predicate<String> isManagedServiceAccount() {
return name -> name != null && name.endsWith(SERVICE_ACCOUNT_SUFFIX);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
/*
* Copyright (c) 2019 Schibsted Media Group. All rights reserved
* Copyright 2019 Schibsted ASA.
*
* 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.front50.migrations;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright 2020 Schibsted ASA.
*
* 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.front50.migrations

import com.netflix.spinnaker.front50.model.pipeline.Pipeline
import com.netflix.spinnaker.front50.model.pipeline.PipelineDAO
import com.netflix.spinnaker.front50.model.serviceaccount.ServiceAccount
import com.netflix.spinnaker.front50.model.serviceaccount.ServiceAccountDAO
import spock.lang.Specification
import spock.lang.Subject

class DeleteDanglingServiceAccountsMigrationSpec extends Specification {
PipelineDAO pipelineDAO = Mock()
ServiceAccountDAO serviceAccountDAO = Mock()

@Subject
def migration = new DeleteDanglingServiceAccountsMigration(pipelineDAO, serviceAccountDAO)

def "should delete service account not found in any triggers"() {
given:
def pipeline1 = new Pipeline([
application: "test",
id : "1",
name : "My Pipeline 1",
triggers : [
[
enabled : true,
job : "org/repo/master",
master : "travis",
runAsUser: "my-existing-service-user@org.com",
type : "travis"
], [
enabled : true,
job : "org/repo2/master",
master : "jenkins",
runAsUser: "1@managed-service-account",
type : "jenkins"
]
]
])

def pipeline2 = new Pipeline([
application: "test",
id : "2",
name : "My Pipeline 2",
triggers : [
[
enabled : true,
job : "org/repo/master",
master : "travis",
runAsUser: "2@managed-service-account",
type : "travis"
], [
enabled : true,
job : "org/repo2/master",
master : "jenkins",
runAsUser: "2@managed-service-account",
type : "jenkins"
]
]
])

def serviceAccount1 = new ServiceAccount(
name: "my-existing-service-user@org.com"
)

def serviceAccount2 = new ServiceAccount(
name: "1@managed-service-account"
)

def serviceAccount3 = new ServiceAccount(
name: "2@managed-service-account"
)
def serviceAccount4 = new ServiceAccount(
name: "3@managed-service-account"
)
def serviceAccount5 = new ServiceAccount(
name: "another-existing-service-user@org.com"
)

when:
migration.run()

then:
1 * serviceAccountDAO.all() >> [serviceAccount1, serviceAccount2, serviceAccount3, serviceAccount4, serviceAccount5]
1 * pipelineDAO.all() >> [pipeline1, pipeline2]
1 * serviceAccountDAO.delete("3@managed-service-account")
0 * serviceAccountDAO.delete(_)
}
}

0 comments on commit 8d4a357

Please sign in to comment.