Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(credentials-api): Add CredentialsDefinitionRepository #1133

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions kork-credentials-api/kork-credentials-api.gradle
Expand Up @@ -21,12 +21,19 @@ dependencies {
implementation(platform(project(":spinnaker-dependencies")))
implementation project(":kork-annotations")
implementation project(":kork-exceptions")
implementation project(":kork-plugins-api")
implementation 'javax.annotation:javax.annotation-api'
implementation 'jakarta.validation:jakarta.validation-api'
implementation "org.springframework:spring-beans"
implementation "org.apache.logging.log4j:log4j-api"
implementation "io.micrometer:micrometer-core"

testRuntimeOnly "cglib:cglib-nodep"
testRuntimeOnly "org.objenesis:objenesis"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine"

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-starter-validation'
testImplementation "org.mockito:mockito-core"
testImplementation "org.assertj:assertj-core"
testImplementation "org.junit.jupiter:junit-jupiter-api"
Expand Down
@@ -0,0 +1,35 @@
/*
* 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.credentials;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;

/** Describes an error encountered loading or parsing credentials. */
@NoArgsConstructor(force = true)
@RequiredArgsConstructor
@AllArgsConstructor
@Data
public class CredentialsError {
@Nonnull private String errorCode;
@Nonnull private String message;
@Nullable private String field;
}
@@ -0,0 +1,37 @@
/*
* 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.credentials;

import com.netflix.spinnaker.credentials.definition.CredentialsDefinitionRepository;
import com.netflix.spinnaker.kork.annotations.Alpha;

/** Describes where these credentials come from. */
@Alpha
public enum CredentialsSource {
/**
* Describes credentials stored in {@linkplain CredentialsDefinitionRepository account storage}.
*/
STORAGE,
/** Describes credentials stored in configuration files. */
CONFIG,
/** Describes credentials provided by a plugin. */
PLUGIN,
/**
* Describes credentials provided from an external source such as a custom resource definition.
*/
EXTERNAL,
}
@@ -0,0 +1,102 @@
/*
* 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.credentials;

import com.netflix.spinnaker.credentials.definition.CredentialsDefinition;
import com.netflix.spinnaker.credentials.definition.CredentialsType;
import com.netflix.spinnaker.kork.annotations.Alpha;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/** Provides a view into a credential definition for a user. */
@Data
@Alpha
@NoArgsConstructor
@AllArgsConstructor
public class CredentialsView {
/**
* Provides metadata regarding a credential definition including the account name, the source from
* which it came, and the type of credentials (corresponding to its {@link CredentialsType} type
* discriminator value).
*
* @see CredentialsSource
*/
private Metadata metadata = new Metadata();

/**
* Provides the current specification of the credentials. This may be an instance of a {@link
* Map}, a {@code JsonNode}, or a {@link CredentialsDefinition} depending on how far the parser
* gets before encountering an error. For a map result, this will contain a key named {@code data}
* pointing to a string containing the invalid JSON data. For a {@code JsonNode} result, this is
* valid JSON but not in the expected form of a {@link CredentialsDefinition}. For a {@link
* CredentialsDefinition}, this will not have any secrets decrypted. If more than one credential
* is available from multiple services, this will be a list of them.
*/
private Object spec;

/** Describes the status of this credential definition. */
private final Status status = new Status();

@Data
public static class Metadata {
/** Provides the name of the account. May be null if unknown (and is not likely parseable). */
@Nullable private String name;
/** Provides the type discriminator of the credentials. May be null if unknown. */
@Nullable private String type;
/** Provides the entity tag of the credentials. If no history is tracked, this may be null. */
@Nullable private String eTag;
/**
* Provides the instant these credentials were last modified. If untracked, this may be null.
*/
@Nullable private Instant lastModified;

/** Describes the source of this credential definition. */
private CredentialsSource source;
}

@Data
public static class Status {
/**
* Indicates if these credentials are valid. If false, then the errors list will be non-empty.
*/
private boolean valid = true;
/** Lists the errors encountered if the account is invalid. */
private List<CredentialsError> errors = new ArrayList<>();

/** Adds an error to this status and resets the {@link #isValid()} flag to {@code false}. */
public void addError(CredentialsError error) {
valid = false;
errors.add(error);
}

/**
* Adds a collection of errors to this status and resets the {@link #isValid()} flag to {@code
* false}.
*/
public void addErrors(Collection<CredentialsError> credentialsErrors) {
valid = false;
errors.addAll(credentialsErrors);
}
}
}
@@ -0,0 +1,70 @@
/*
* 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.credentials.constraint;

import com.netflix.spinnaker.credentials.definition.CredentialsDefinition;
import java.util.regex.Pattern;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.util.StringUtils;

/**
* Constraint validator implementation for the {@link ValidatedCredentials} annotation. Validation
* of credentials includes checking that its name matches the configured {@link
* CredentialsDefinitionNamePatternProvider} bean-provided regular expression using the credentials
* type name extracted by the configured {@link CredentialsDefinitionTypeExtractor} bean along with
* any other {@link CredentialsDefinitionValidator} beans found at runtime.
*/
@RequiredArgsConstructor
public class CredentialsDefinitionConstraintValidator
implements ConstraintValidator<ValidatedCredentials, CredentialsDefinition> {
private final CredentialsDefinitionTypeExtractor typeExtractor;
private final CredentialsDefinitionNamePatternProvider namePatternProvider;
private final ObjectProvider<CredentialsDefinitionValidator> validators;

@Override
public boolean isValid(CredentialsDefinition value, ConstraintValidatorContext context) {
context.disableDefaultConstraintViolation();
String name = value.getName();
if (!StringUtils.hasText(name)) {
context
.buildConstraintViolationWithTemplate("missing account name")
.addPropertyNode("name")
.addConstraintViolation();
return false;
}
String type = typeExtractor.getCredentialsType(value);
if (type == null) {
context.buildConstraintViolationWithTemplate("missing account type").addConstraintViolation();
return false;
}
Pattern pattern = namePatternProvider.getPatternForType(type);
boolean valid = pattern.matcher(name).matches();
if (!valid) {
context
.buildConstraintViolationWithTemplate("account name does not match pattern " + pattern)
.addPropertyNode("name")
.addConstraintViolation();
}
for (CredentialsDefinitionValidator validator : validators) {
valid &= validator.isValid(value, context);
}
return valid;
}
}
@@ -0,0 +1,33 @@
/*
* 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.credentials.constraint;

import com.netflix.spinnaker.credentials.definition.CredentialsDefinition;
import java.util.regex.Pattern;

/**
* Provides validation patterns for {@linkplain CredentialsDefinition#getName() credentials names}.
* A bean of this type (along with {@link CredentialsDefinitionTypeExtractor}) must be registered in
* order to use the {@link ValidatedCredentials} constraint annotation on credentials.
*
* @see CredentialsDefinitionTypeExtractor
* @see ValidatedCredentials
*/
@FunctionalInterface
public interface CredentialsDefinitionNamePatternProvider {
Pattern getPatternForType(String type);
}
@@ -0,0 +1,35 @@
/*
* 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.credentials.constraint;

import com.netflix.spinnaker.credentials.definition.CredentialsDefinition;
import javax.annotation.Nullable;

/**
* Strategy for extracting a credentials type name from a credential definition. A bean of this type
* must be registered (along with {@link CredentialsDefinitionNamePatternProvider}) to use the
* {@link ValidatedCredentials} constraint annotation on credentials.
*
* @see com.netflix.spinnaker.credentials.definition.CredentialsType
* @see CredentialsDefinitionNamePatternProvider
* @see ValidatedCredentials
*/
@FunctionalInterface
public interface CredentialsDefinitionTypeExtractor {
@Nullable
String getCredentialsType(CredentialsDefinition definition);
}
@@ -0,0 +1,29 @@
/*
* 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.credentials.constraint;

import com.netflix.spinnaker.credentials.definition.CredentialsDefinition;
import com.netflix.spinnaker.kork.plugins.api.internal.SpinnakerExtensionPoint;
import javax.validation.ConstraintValidator;

/**
* Validates a {@link CredentialsDefinition} instance. Beans of this type are used when validating
* the {@link ValidatedCredentials} constraint annotation.
*/
public interface CredentialsDefinitionValidator
extends SpinnakerExtensionPoint,
ConstraintValidator<ValidatedCredentials, CredentialsDefinition> {}