diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index be58781..64d0f81 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -41,9 +41,15 @@ jobs: java-version: "21" distribution: "temurin" server-id: github + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 - name: Maven Verify run: | - mvn --batch-mode clean install -s settings.xml + mvn --batch-mode clean verify -s settings.xml env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -61,6 +67,18 @@ jobs: java-version: "17" distribution: "temurin" server-id: github + - name: Cache SonarCloud packages + uses: actions/cache@v4 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 - name: Maven Test Coverage env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/nifi-tdf-controller-services-api-nar/pom.xml b/nifi-tdf-controller-services-api-nar/pom.xml index 58b599c..913e347 100644 --- a/nifi-tdf-controller-services-api-nar/pom.xml +++ b/nifi-tdf-controller-services-api-nar/pom.xml @@ -28,6 +28,25 @@ + + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + default-prepare-agent + none + + + default-report + none + + + + + org.apache.nifi diff --git a/nifi-tdf-processors/pom.xml b/nifi-tdf-processors/pom.xml index a62f03a..940b719 100644 --- a/nifi-tdf-processors/pom.xml +++ b/nifi-tdf-processors/pom.xml @@ -97,37 +97,4 @@ test - - - - - org.jacoco - jacoco-maven-plugin - - - prepare-agent - - prepare-agent - - - ${project.parent.basedir}/target/jacoco.exec - - - - report - test - - report - - - ${project.parent.basedir}/target/jacoco.exec - - XML - - - - - - - diff --git a/nifi-tdf-processors/src/main/java/io/opentdf/nifi/AbstractTDFProcessor.java b/nifi-tdf-processors/src/main/java/io/opentdf/nifi/AbstractTDFProcessor.java index f8a50ce..df43cdb 100644 --- a/nifi-tdf-processors/src/main/java/io/opentdf/nifi/AbstractTDFProcessor.java +++ b/nifi-tdf-processors/src/main/java/io/opentdf/nifi/AbstractTDFProcessor.java @@ -28,6 +28,11 @@ */ public abstract class AbstractTDFProcessor extends AbstractProcessor { + /** + * Configuration property representing the limit on the number of FlowFiles + * that can be pulled from the FlowFile queue at a time. + * It supports expression language through the variable registry and has a default value of 10. + */ public static final PropertyDescriptor FLOWFILE_PULL_SIZE = new org.apache.nifi.components.PropertyDescriptor.Builder() .name("FlowFile queue pull limit") .description("FlowFile queue pull size limit") @@ -37,6 +42,12 @@ public abstract class AbstractTDFProcessor extends AbstractProcessor { .addValidator(StandardValidators.INTEGER_VALIDATOR) .build(); + /** + * Property descriptor representing an optional SSL Context Service. + * This descriptor defines a property that can be used to configure + * an SSLContextService, which is optional for the processor. This + * service provides the SSL/TLS context needed for secure communication. + */ public static final PropertyDescriptor SSL_CONTEXT_SERVICE = new org.apache.nifi.components.PropertyDescriptor.Builder() .name("SSL Context Service") .description("Optional SSL Context Service") @@ -44,6 +55,13 @@ public abstract class AbstractTDFProcessor extends AbstractProcessor { .identifiesControllerService(SSLContextService.class) .build(); + /** + * Represents a property descriptor for the OpenTDF Config Service. + *

+ * This descriptor specifies that the property is required and identifies + * a controller service of type {@link OpenTDFControllerService}. The controller service + * provides the necessary configuration for the OpenTDF platform. + */ public static final PropertyDescriptor OPENTDF_CONFIG_SERVICE = new org.apache.nifi.components.PropertyDescriptor.Builder() .name("OpenTDF Config Service") .description("Controller Service providing OpenTDF Platform Configuration") @@ -51,30 +69,54 @@ public abstract class AbstractTDFProcessor extends AbstractProcessor { .identifiesControllerService(OpenTDFControllerService.class) .build(); + /** + * Defines a successful relationship for the NiFi processor. This relationship is used to route flow files + * that have been successfully processed. Flow files sent to this relationship indicate that the processor + * completed its intended action without errors. + *

+ * This relationship is commonly used as an output route for data that has passed all validation, transformation, + * and processing steps. + */ public static final Relationship REL_SUCCESS = new Relationship.Builder() .name("success") .description("") .build(); + /** + * Relationship representing a failure in processing flow files. + *

+ * This relationship should be used to route flow files that could not + * be processed successfully by the processor. The reasons for failure + * can vary widely and may include issues like invalid data, processing + * errors, or configuration issues. + */ public static final Relationship REL_FAILURE = new Relationship.Builder() .name("failure") .description("") .build(); /** - * Get a property value by evaluating attribute expressions if present. + * Evaluates the provided PropertyValue if expression language is present, + * otherwise returns the original PropertyValue. * - * @param propertyValue - * @return + * @param propertyValue The PropertyValue to evaluate or return. + * @return The evaluated PropertyValue if expression language is present, + * otherwise the original PropertyValue. */ PropertyValue getPropertyValue(PropertyValue propertyValue) { return propertyValue.isExpressionLanguagePresent() ? propertyValue.evaluateAttributeExpressions() : propertyValue; } - Optional getPropertyValue(PropertyDescriptor propertyDescriptor, ProcessContext processContext) { + /** + * Retrieves the value of the specified property from the given process context. + * + * @param processContext The context from which to retrieve the property value. + * @return An Optional containing the PropertyValue if it is set, or an empty Optional otherwise. + */ + Optional getPropertyValue(ProcessContext processContext) { PropertyValue propertyValue = null; - if(processContext.getProperty(propertyDescriptor).isSet()){ - propertyValue = getPropertyValue(processContext.getProperty(propertyDescriptor)); + if(processContext.getProperty(ConvertToZTDF.SIGN_ASSERTIONS).isSet()){ + propertyValue = getPropertyValue(processContext.getProperty(ConvertToZTDF.SIGN_ASSERTIONS)); } return Optional.ofNullable(propertyValue); } @@ -82,10 +124,10 @@ Optional getPropertyValue(PropertyDescriptor propertyDescriptor, private SDK sdk; /** - * Create a new TDF SDK using the OpenTDFController Service as a source of configuration + * Retrieves an instance of the TDF SDK, initializing it if it is not already created. * - * @param processContext - * @return + * @param processContext the NiFi ProcessContext providing necessary configuration and controller services. + * @return an instance of the initialized SDK. */ SDK getTDFSDK(ProcessContext processContext) { if (sdk == null) { @@ -159,17 +201,31 @@ public void onTrigger(ProcessContext processContext, ProcessSession processSessi */ abstract void processFlowFiles(ProcessContext processContext, ProcessSession processSession, List flowFiles) throws ProcessException; + /** + * Creates and returns a new instance of TDF. + * + * @return A new instance of TDF. + */ TDF getTDF() { return new TDF(); } + /** + * Creates and returns a new instance of NanoTDF. + * + * @return A new instance of NanoTDF. + */ NanoTDF getNanoTDF(){ return new NanoTDF(); } + /** + * Retrieves the list of property descriptors that are supported by this processor. + * + * @return A list containing the supported property descriptors. + */ @Override public List getSupportedPropertyDescriptors() { - return Collections.unmodifiableList(Arrays.asList(SSL_CONTEXT_SERVICE, OPENTDF_CONFIG_SERVICE, FLOWFILE_PULL_SIZE)); + return List.of(SSL_CONTEXT_SERVICE, OPENTDF_CONFIG_SERVICE, FLOWFILE_PULL_SIZE); } - } diff --git a/nifi-tdf-processors/src/main/java/io/opentdf/nifi/AbstractToProcessor.java b/nifi-tdf-processors/src/main/java/io/opentdf/nifi/AbstractToProcessor.java index a0c81ca..df96bc0 100644 --- a/nifi-tdf-processors/src/main/java/io/opentdf/nifi/AbstractToProcessor.java +++ b/nifi-tdf-processors/src/main/java/io/opentdf/nifi/AbstractToProcessor.java @@ -34,37 +34,52 @@ public List getSupportedPropertyDescriptors() { return Collections.unmodifiableList(Arrays.asList(SSL_CONTEXT_SERVICE, OPENTDF_CONFIG_SERVICE, FLOWFILE_PULL_SIZE, KAS_URL)); } - /**{ - * Get the kas urls from a flowfile attribute or if none present fallback to processor configuration KAS URL; - * format is a comma separated list - * @param flowFile - * @param processContext - * @return - * @throws Exception + /** + * Retrieves a list of KAS (Key Access Service) URLs either from the flow file attributes or from the process context. + * If the KAS URL is not provided through the flow file attribute and is not set in the process context, an exception is thrown. + * + * @param flowFile the flow file from which KAS URL attributes are retrieved. + * @param processContext the process context to get the default KAS URL if not available in the flow file. + * @return a list of KAS URLs. + * @throws Exception if no KAS URL is provided via the flow file or the default in the process context, or if the KAS URLs provided are empty. */ - List getKasUrl(FlowFile flowFile, ProcessContext processContext) throws Exception{ + List getKasUrl(FlowFile flowFile, ProcessContext processContext) throws Exception { String kasUrlAttribute = flowFile.getAttribute(KAS_URL_ATTRIBUTE); - //check kas url + // Check kas url if (!processContext.getProperty(KAS_URL).isSet() && kasUrlAttribute == null) { throw new Exception("no " + KAS_URL_ATTRIBUTE + " flowfile attribute and no default KAS URL configured"); } String kasUrlValues = kasUrlAttribute != null ? kasUrlAttribute : getPropertyValue(processContext.getProperty(KAS_URL)).getValue(); - List kasUrls = Arrays.stream(kasUrlValues.split(",")).filter(x->!x.isEmpty()).collect(Collectors.toList()); - if (kasUrlValues.isEmpty()){ + List kasUrls = Arrays.stream(kasUrlValues.split(",")) + .filter(x -> !x.isEmpty()) + .toList(); // Use Stream.toList() for an unmodifiable list + if (kasUrlValues.isEmpty()) { throw new Exception("no KAS Urls provided"); } return kasUrls; } + /** + * Converts a list of KAS (Key Access Service) URLs into a list of Config.KASInfo objects. + * + * @param kasUrls a list of strings representing the KAS URLs + * @return a list of Config.KASInfo objects with each object's URL field set to the corresponding string from the input list + */ List getKASInfoFromKASURLs(List kasUrls){ - return kasUrls.stream().map(x->{ var ki = new Config.KASInfo(); ki.URL=x; return ki;}).collect(Collectors.toList()); + return kasUrls.stream().map(x -> { + var ki = new Config.KASInfo(); + ki.URL = x; + return ki; + }).toList(); } /** - * Get data attributes on a FlowFile from attribute value - * @param flowFile - * @return - * @throws Exception + * Extracts and returns a set of data attributes from the given FlowFile's attribute specified by TDF_ATTRIBUTE. + * The attributes are split by commas and filtered to remove empty strings. + * + * @param flowFile the FlowFile from which to retrieve the data attributes. + * @return a set of data attributes extracted from the given FlowFile. + * @throws Exception if no data attributes are provided via the TDF_ATTRIBUTE FlowFile attribute. */ Set getDataAttributes(FlowFile flowFile) throws Exception{ Set dataAttributes = Arrays.stream((flowFile.getAttribute(TDF_ATTRIBUTE) == null ? "" : diff --git a/nifi-tdf-processors/src/main/java/io/opentdf/nifi/ConvertFromNanoTDF.java b/nifi-tdf-processors/src/main/java/io/opentdf/nifi/ConvertFromNanoTDF.java index 63f4d6a..54800d1 100644 --- a/nifi-tdf-processors/src/main/java/io/opentdf/nifi/ConvertFromNanoTDF.java +++ b/nifi-tdf-processors/src/main/java/io/opentdf/nifi/ConvertFromNanoTDF.java @@ -12,10 +12,27 @@ import java.nio.ByteBuffer; import java.util.List; +/** + * A processor for decrypting NanoTDF flow file content using the OpenTDF framework. + *

+ * This processor reads encrypted NanoTDF data from incoming flow files and decrypts + * it using the associated SDK. The decrypted content is then written back into the + * flow file and routed to the success relationship. If decryption fails, the flow file + * is routed to the failure relationship. + */ @CapabilityDescription("Decrypts NanoTDF flow file content") @Tags({"NanoTDF", "OpenTDF", "Decrypt", "Data Centric Security"}) public class ConvertFromNanoTDF extends AbstractTDFProcessor { + /** + * Processes the provided list of flow files by decrypting their content using the NanoTDF protocol. + * If decryption succeeds, the flow file is routed to the success relationship; otherwise, it is routed to the failure relationship. + * + * @param processContext the NiFi ProcessContext which provides configuration and controller services + * @param processSession the ProcessSession which provides mechanisms for reading, writing, transferring, and penalizing flow files + * @param flowFiles the list of FlowFile objects to be processed + * @throws ProcessException if any error occurs during the processing of flow files + */ @Override public void processFlowFiles(ProcessContext processContext, ProcessSession processSession, List flowFiles) throws ProcessException { SDK sdk = getTDFSDK(processContext); diff --git a/nifi-tdf-processors/src/main/java/io/opentdf/nifi/ConvertFromZTDF.java b/nifi-tdf-processors/src/main/java/io/opentdf/nifi/ConvertFromZTDF.java index 66a7051..580d0da 100644 --- a/nifi-tdf-processors/src/main/java/io/opentdf/nifi/ConvertFromZTDF.java +++ b/nifi-tdf-processors/src/main/java/io/opentdf/nifi/ConvertFromZTDF.java @@ -16,12 +16,46 @@ import java.util.ArrayList; import java.util.List; - +/** + * Converts and decrypts ZTDF (Zero Trust Data Format) flow file content. + * This class takes encrypted ZTDF content and decrypts it, + * transferring the decrypted data to a specified success relationship. + * If an error occurs during decryption, it transfers the flow file to a failure relationship. + *

+ * This processor uses TDF (Trusted Data Format) SDK for decryption and + * requires configuration of assertion verification keys to verify + * the integrity and authenticity of the encrypted data. + *

+ * It provides the primary method `processFlowFiles` which reads the encrypted + * content from incoming flow files, decrypts it, and writes the decrypted + * content back to the flow files. + *

+ * The method `processFlowFiles` performs the following steps: + * 1. Retrieves the TDF SDK instance. + * 2. Reads the entire encrypted content of each flow file into an in-memory byte channel. + * 3. Uses TDF Reader to load and decrypt the content. + * 4. Writes the decrypted content back into the flow file and transfers it to the success relationship. + * 5. If any error occurs during the decryption process, logs the error and transfers the flow file to the failure relationship. + */ @CapabilityDescription("Decrypts ZTDF flow file content") @Tags({"ZTDF", "Zero Trust Data Format", "OpenTDF", "Decrypt", "Data Centric Security"}) public class ConvertFromZTDF extends AbstractTDFProcessor { + /** + * Processes a list of flow files by decrypting their content using the TDF (Trusted Data Format) SDK. + * For each flow file in the provided list, the following steps are executed: + * 1. Reads the entire encrypted content of the flow file into an in-memory byte channel. + * 2. Uses a TDF Reader to load and decrypt the content using the SDK. + * 3. Writes the decrypted content back to the flow file. + * 4. Transfers the successfully decrypted flow files to the success relationship. + * 5. In case of an error during decryption, logs the error and transfers the flow file to the failure relationship. + * + * @param processContext the NiFi ProcessContext providing configuration and controller services. + * @param processSession the NiFi ProcessSession used to read, write, and transfer flow files. + * @param flowFiles a list of flow files to be decrypted. + * @throws ProcessException if an error occurs during the decryption process. + */ @Override void processFlowFiles(ProcessContext processContext, ProcessSession processSession, List flowFiles) throws ProcessException { SDK sdk = getTDFSDK(processContext); diff --git a/nifi-tdf-processors/src/main/java/io/opentdf/nifi/ConvertToNanoTDF.java b/nifi-tdf-processors/src/main/java/io/opentdf/nifi/ConvertToNanoTDF.java index 8ce0e55..118066b 100644 --- a/nifi-tdf-processors/src/main/java/io/opentdf/nifi/ConvertToNanoTDF.java +++ b/nifi-tdf-processors/src/main/java/io/opentdf/nifi/ConvertToNanoTDF.java @@ -20,6 +20,25 @@ import java.util.List; import java.util.Set; +/** + * Processor for converting the content of a FlowFile into a NanoTDF (Trusted Data Format). + *

+ * This class extends AbstractToProcessor and handles the encryption of FlowFile content into NanoTDF, + * applying specified KAS (Key Access Service) URLs and data attributes as defined in the flow file attributes + * or processor properties. + *

+ * Relationships: + * - REL_SUCCESS: When the conversion to NanoTDF is successful. + * - REL_FAILURE: When the conversion to NanoTDF fails. + * - REL_FLOWFILE_EXCEEDS_NANO_SIZE: When the content size exceeds the maximum allowed size for NanoTDF. + *

+ * Property Descriptors: + * - Inherited from AbstractToProcessor (e.g., KAS URL, SSL_CONTEXT_SERVICE, OPENTDF_CONFIG_SERVICE, etc.) + *

+ * Reads Attributes: + * - kas_url: The Key Access Server (KAS) URL used for TDF creation. Overrides the default KAS URL property. + * - tdf_attribute: A comma-separated list of data attributes added to the created TDF Data Policy. + */ @CapabilityDescription("Transforms flow file content into a NanoTDF") @Tags({"NanoTDF", "OpenTDF", "Encrypt", "Data Centric Security"}) @ReadsAttributes(value = { @@ -30,19 +49,43 @@ }) public class ConvertToNanoTDF extends AbstractToProcessor { + /** + * Defines a relationship indicating that NanoTDF creation has failed + * due to the content size exceeding the maximum allowed NanoTDF size threshold. + */ public static final Relationship REL_FLOWFILE_EXCEEDS_NANO_SIZE = new Relationship.Builder() .name("exceeds_size_limit") .description("NanoTDF creation failed due to the content size exceeding the max NanoTDF size threshold") .build(); + /** + * Represents the maximum allowable size for processing a flow file in nano TDF conversion. + * Value is set to 16 MB. + */ static final long MAX_SIZE = 16777218; + /** + * Retrieves all the relationships defined in the ConvertToNanoTDF processor. + * + * @return a Set of Relationship objects representing the different relationships for the processor. + */ @Override public Set getRelationships() { return new HashSet<>(Arrays.asList(REL_SUCCESS, REL_FAILURE, REL_FLOWFILE_EXCEEDS_NANO_SIZE)); } + /** + * Processes a list of FlowFiles to convert them to NanoTDF format. + * If a FlowFile's size exceeds the maximum allowed size, it is routed to a specific relationship. + * Otherwise, it attempts to convert the FlowFile's content and transfer it to a success relationship. + * In case of an error during processing, the FlowFile is routed to a failure relationship. + * + * @param processContext the NiFi ProcessContext providing necessary configuration and controller services. + * @param processSession the NiFi ProcessSession representing a transaction context for the processing of FlowFiles. + * @param flowFiles a list of FlowFiles to be processed. + * @throws ProcessException if an error occurs during the processing of the FlowFiles. + */ @Override public void processFlowFiles(ProcessContext processContext, ProcessSession processSession, List flowFiles) throws ProcessException { SDK sdk = getTDFSDK(processContext); @@ -50,6 +93,8 @@ public void processFlowFiles(ProcessContext processContext, ProcessSession proce try { var kasInfoList = getKASInfoFromKASURLs(getKasUrl(flowFile, processContext)); Set dataAttributes = getDataAttributes(flowFile); + // Config.newNanoTDFConfig is correctly handling the varargs + @SuppressWarnings("unchecked") Config.NanoTDFConfig config = Config.newNanoTDFConfig( Config.withNanoKasInformation(kasInfoList.toArray(new Config.KASInfo[0])), Config.witDataAttributes(dataAttributes.toArray(new String[0])) diff --git a/nifi-tdf-processors/src/main/java/io/opentdf/nifi/ConvertToZTDF.java b/nifi-tdf-processors/src/main/java/io/opentdf/nifi/ConvertToZTDF.java index 81088e4..c6e9ddb 100644 --- a/nifi-tdf-processors/src/main/java/io/opentdf/nifi/ConvertToZTDF.java +++ b/nifi-tdf-processors/src/main/java/io/opentdf/nifi/ConvertToZTDF.java @@ -20,12 +20,15 @@ import org.apache.nifi.processor.exception.ProcessException; import java.io.IOException; -import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; -import java.security.spec.InvalidKeySpecException; import java.util.*; import java.util.function.Consumer; +/** + * The ConvertToZTDF class transforms flow file content into a ZTDF (Zero Trust Data Format). + * It builds assertions from flow file attributes and configures TDF options based on these assertions. + * This class supports property descriptors and signing of assertions. + */ @CapabilityDescription("Transforms flow file content into a ZTDF") @Tags({"ZTDF", "OpenTDF", "Zero Trust Data Format", "Encrypt", "Data Centric Security"}) @ReadsAttributes(value = { @@ -49,6 +52,17 @@ }) public class ConvertToZTDF extends AbstractToProcessor { + /** + * Property descriptor for the "Sign Assertions" feature in the ConvertToZTDF processor. This property allows specifying whether + * the assertions should be signed or not. It is not a required property and defaults to "false". + *

+ * - Name: Sign Assertions + * - Description: sign assertions + * - Required: false + * - Default Value: false + * - Allowable Values: true, false + * - Expression Language Supported: {@link ExpressionLanguageScope#VARIABLE_REGISTRY} + */ public static final PropertyDescriptor SIGN_ASSERTIONS = new org.apache.nifi.components.PropertyDescriptor.Builder() .name("Sign Assertions") .description("sign assertions") @@ -58,6 +72,15 @@ public class ConvertToZTDF extends AbstractToProcessor { .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY) .build(); + /** + * Property descriptor for the "Private Key Controller Service". + *

+ * This descriptor defines an optional Private Key Service which is required + * for assertion signing. The property is compulsory and identifies the + * PrivateKeyService class as the controller service. It is dependent on + * the SIGN_ASSERTIONS property being set to "true" and supports expression + * language in the variable registry scope. + */ public static final PropertyDescriptor PRIVATE_KEY_CONTROLLER_SERVICE = new org.apache.nifi.components.PropertyDescriptor.Builder() .name("Private Key Controller Service") .description("Optional Private Key Service; this is need for assertion signing") @@ -68,12 +91,23 @@ public class ConvertToZTDF extends AbstractToProcessor { .build(); + /** + * Retrieves the PrivateKeyService from the given process context if it is set. + * + * @param processContext the NiFi ProcessContext providing necessary configuration and controller services. + * @return an instance of PrivateKeyService if it is set in the process context, otherwise null. + */ PrivateKeyService getPrivateKeyService(ProcessContext processContext) { return processContext.getProperty(PRIVATE_KEY_CONTROLLER_SERVICE).isSet() ? processContext.getProperty(PRIVATE_KEY_CONTROLLER_SERVICE) .asControllerService(PrivateKeyService.class) : null; } + /** + * Retrieves a list of supported property descriptors for this processor. + * + * @return an unmodifiable list of PropertyDescriptor objects representing the supported properties. + */ @Override public List getSupportedPropertyDescriptors() { List propertyDescriptors = new ArrayList<>(super.getSupportedPropertyDescriptors()); @@ -91,18 +125,25 @@ public List getSupportedPropertyDescriptors() { Map assertionAppliesToStateMap = Map.of("encrypted", AssertionConfig.AppliesToState.Encrypted, "unencrypted", AssertionConfig.AppliesToState.Unencrypted); /** - * Build an assertion config from the flow file attribute - * @return - * @throws Exception throw exception when assertion is not valid + * Builds an {@link AssertionConfig} instance from the given NiFi {@link ProcessContext} and {@link FlowFile}, + * using the provided flowFile attribute name to retrieve relevant data. This method deserializes an assertion + * JSON string from the flowFile attribute, populates the {@link AssertionConfig}, and performs necessary validations. + * + * @param processContext the NiFi ProcessContext providing necessary configuration and controller services + * @param flowFile the NiFi FlowFile containing the assertion JSON string in its attributes + * @param flowFileAttributeName the name of the attribute in the flowFile which contains the assertion JSON string + * @return an {@link AssertionConfig} instance populated with values from the assertion JSON string in the flowFile attribute + * @throws Exception if any essential assertion information is missing or invalid */ AssertionConfig buildAssertion(ProcessContext processContext, FlowFile flowFile, String flowFileAttributeName) throws Exception{ String assertionJson = flowFile.getAttribute(flowFileAttributeName); Map assertionMap = gson.fromJson(assertionJson, Map.class); AssertionConfig assertionConfig = new AssertionConfig(); assertionConfig.id = assertionMap.containsKey("id") ? (String)assertionMap.get("id") : null; - assertionConfig.type = assertionMap.containsKey("type") ? assertionTypeMap.get(assertionMap.get("type")) : null; - assertionConfig.scope =assertionMap.containsKey("scope") ? assertionScopeMap.get(assertionMap.get("scope")) : null; - assertionConfig.appliesToState = assertionMap.containsKey("appliesToState") ? assertionAppliesToStateMap.get(assertionMap.get("appliesToState")): null; + + populateFieldFromMap(assertionMap, "type", assertionTypeMap, value -> assertionConfig.type = (AssertionConfig.Type) value); + populateFieldFromMap(assertionMap, "scope", assertionScopeMap, value -> assertionConfig.scope = (AssertionConfig.Scope) value); + populateFieldFromMap(assertionMap, "appliesToState", assertionAppliesToStateMap, value -> assertionConfig.appliesToState = (AssertionConfig.AppliesToState) value); assertionConfig.statement = new AssertionConfig.Statement(); Map statementMap = (Map)assertionMap.get("statement"); @@ -129,6 +170,22 @@ AssertionConfig buildAssertion(ProcessContext processContext, FlowFile flowFile, return assertionConfig; } + private void populateFieldFromMap(Map sourceMap, String key, Map destinationMap, Consumer setter) { + if (sourceMap.containsKey(key)) { + setter.accept(destinationMap.get(sourceMap.get(key))); + } else { + setter.accept(null); + } + } + + /** + * Processes a list of FlowFiles to convert them into TDF (Trusted Data Format) files. + * + * @param processContext the NiFi ProcessContext providing necessary configuration and controller services. + * @param processSession the NiFi ProcessSession used to interact with the FlowFiles. + * @param flowFiles the list of FlowFiles to be processed. + * @throws ProcessException if there are any errors during the processing of the FlowFiles. + */ @Override void processFlowFiles(ProcessContext processContext, ProcessSession processSession, List flowFiles) throws ProcessException { SDK sdk = getTDFSDK(processContext); @@ -144,12 +201,17 @@ void processFlowFiles(ProcessContext processContext, ProcessSession processSessi getLogger().debug(String.format("Adding assertion for NiFi attribute = %s", nifiAssertionAttributeKey)); configurationOptions.add(Config.withAssertionConfig(buildAssertion(processContext, flowFile, nifiAssertionAttributeKey))); } + // Config.newTDFConfig is correctly handling the varargs + @SuppressWarnings("unchecked") TDFConfig config = Config.newTDFConfig(configurationOptions.toArray(new Consumer[0])); //write ZTDF to FlowFile FlowFile updatedFlowFile = processSession.write(flowFile, (inputStream, outputStream) -> { try { getTDF().createTDF(inputStream, outputStream, config, sdk.getServices().kas(), sdk.getServices().attributes()); + } catch (InterruptedException e) { + getLogger().error("Interrupted inner", e); + Thread.currentThread().interrupt(); } catch (Exception e) { getLogger().error("error creating ZTDF", e); throw new IOException(e); @@ -158,6 +220,9 @@ void processFlowFiles(ProcessContext processContext, ProcessSession processSessi ); updatedFlowFile = processSession.putAttribute(updatedFlowFile, "mime.type", "application/ztdf+zip"); processSession.transfer(updatedFlowFile, REL_SUCCESS); + } catch (InterruptedException e) { + getLogger().error("Interrupted outer", e); + Thread.currentThread().interrupt(); } catch (Exception e) { getLogger().error(flowFile.getId() + ": error converting plain text to ZTDF", e); processSession.transfer(flowFile, REL_FAILURE); @@ -165,10 +230,18 @@ void processFlowFiles(ProcessContext processContext, ProcessSession processSessi } } - private void addSigningInfoToAssertionConfig(ProcessContext processContext, AssertionConfig assertionConfig) throws NoSuchAlgorithmException, InvalidKeySpecException { - Optional signAssertions = getPropertyValue(SIGN_ASSERTIONS, processContext); + /** + * Adds signing information to the given AssertionConfig if the signing property is enabled + * in the ProcessContext and the private key service is available. + * + * @param processContext the NiFi ProcessContext providing necessary configuration + * and controller services. + * @param assertionConfig the AssertionConfig to which the signing information will be added. + */ + private void addSigningInfoToAssertionConfig(ProcessContext processContext, AssertionConfig assertionConfig) { + Optional signAssertions = getPropertyValue(processContext); //populate assertion signing config only when sign assertions property is true and assertions exist - if (signAssertions.isPresent() && signAssertions.get().asBoolean()) { + if (signAssertions.isPresent() && Boolean.TRUE.equals(signAssertions.get().asBoolean())) { getLogger().debug("signed assertions is active"); PrivateKeyService privateKeyService = getPrivateKeyService(processContext); if (privateKeyService != null) { diff --git a/nifi-tdf-processors/src/main/java/io/opentdf/nifi/SimpleOpenTDFControllerService.java b/nifi-tdf-processors/src/main/java/io/opentdf/nifi/SimpleOpenTDFControllerService.java index 4f4b97b..54e8c4d 100644 --- a/nifi-tdf-processors/src/main/java/io/opentdf/nifi/SimpleOpenTDFControllerService.java +++ b/nifi-tdf-processors/src/main/java/io/opentdf/nifi/SimpleOpenTDFControllerService.java @@ -15,10 +15,17 @@ import java.util.Arrays; import java.util.List; +/** + * Provides an implementation of the OpenTDFControllerService API for OpenTDF SDK Configuration Parameters. + */ @Tags({"TDF", "ZTDF", "OpenTDF", "Configuration"}) @CapabilityDescription("Provides An implementation of the OpenTDFControllerService API for OpenTDF SDK Configuration Parameters") public class SimpleOpenTDFControllerService extends AbstractControllerService implements OpenTDFControllerService { + /** + * The endpoint of the OpenTDF platform in GRPC compatible format, excluding the protocol prefix. + * This is a required property and supports expression language within the scope of the variable registry. + */ public static final PropertyDescriptor PLATFORM_ENDPOINT = new PropertyDescriptor.Builder() .name("platform-endpoint") .displayName("OpenTDF Platform ENDPOINT") @@ -29,6 +36,14 @@ public class SimpleOpenTDFControllerService extends AbstractControllerService im .description("OpenTDF Platform ENDPOINT in GRPC compatible format (no protocol prefix)") .build(); + /** + * The client secret used for authentication with the OpenTDF Platform. + *

+ * This property is required and must be configured with a valid client secret. + * It supports expression language within the scope of the variable registry, ensuring that the client secret + * can be dynamically set based on external configurations. + * The value of this property is sensitive and will be handled securely. + */ public static final PropertyDescriptor CLIENT_SECRET = new PropertyDescriptor.Builder() .name("clientSecret") .displayName("Client Secret") @@ -39,6 +54,11 @@ public class SimpleOpenTDFControllerService extends AbstractControllerService im .description("OpenTDF Platform Authentication Client Secret") .build(); + /** + * Property for specifying the Client ID used for authentication against the OpenTDF Platform. + * This is a required field, and it must be non-empty. Expression language is supported for + * variable registry scope. + */ public static final PropertyDescriptor CLIENT_ID = new PropertyDescriptor.Builder() .name("clientId") .displayName("Client ID") @@ -50,6 +70,12 @@ public class SimpleOpenTDFControllerService extends AbstractControllerService im .build(); + /** + * Configuration property that determines whether the connection to the OpenTDF Platform should use plaintext. + * This is a required property and can have a default value of "false". + * The acceptable values for this property are "true" and "false". It is not marked as sensitive. + * A non-empty validator is applied to ensure the property is not left blank. + */ public static final PropertyDescriptor USE_PLAINTEXT = new PropertyDescriptor.Builder() .name("usePlaintext") .displayName("Platform Use Plaintext Connection") @@ -63,24 +89,67 @@ public class SimpleOpenTDFControllerService extends AbstractControllerService im Config config = null; + /** + * Returns a list of property descriptors that are supported by this controller service. + * + * @return a list of PropertyDescriptor objects + */ @Override protected List getSupportedPropertyDescriptors() { return Arrays.asList(PLATFORM_ENDPOINT, CLIENT_ID, CLIENT_SECRET, USE_PLAINTEXT); } + /** + * Initializes the controller service with the provided configuration context. + * + * @param configurationContext the context containing configuration properties to be applied during service enablement + * @throws InitializationException if any required configuration property is missing or invalid + */ @OnEnabled public void enabled(final ConfigurationContext configurationContext) throws InitializationException { config = new Config(); - config.setClientId(getPropertyValue(configurationContext.getProperty(CLIENT_ID)).getValue()); - config.setClientSecret(getPropertyValue(configurationContext.getProperty(CLIENT_SECRET)).getValue()); - config.setPlatformEndpoint(getPropertyValue(configurationContext.getProperty(PLATFORM_ENDPOINT)).getValue()); - config.setUsePlainText(getPropertyValue(configurationContext.getProperty(USE_PLAINTEXT)).asBoolean()); + + PropertyValue clientIdValue = getPropertyValue(configurationContext.getProperty(CLIENT_ID)); + PropertyValue clientSecretValue = getPropertyValue(configurationContext.getProperty(CLIENT_SECRET)); + PropertyValue platformEndpointValue = getPropertyValue(configurationContext.getProperty(PLATFORM_ENDPOINT)); + PropertyValue usePlainTextValue = getPropertyValue(configurationContext.getProperty(USE_PLAINTEXT)); + + // Ensure that no required property is null + if (clientIdValue.getValue() == null || + clientSecretValue.getValue() == null || + platformEndpointValue.getValue() == null || + usePlainTextValue == null) { + throw new InitializationException("One or more required properties are not configured properly."); + } + + config.setClientId(clientIdValue.getValue()); + config.setClientSecret(clientSecretValue.getValue()); + config.setPlatformEndpoint(platformEndpointValue.getValue()); + + Boolean usePlainText = usePlainTextValue.asBoolean(); + if (usePlainText == null) { + throw new InitializationException("The 'usePlaintext' property must be either 'true' or 'false'."); + } + config.setUsePlainText(usePlainText); } + /** + * Evaluates the provided PropertyValue and returns the result of the evaluation if expression language is present. + * Otherwise, it returns the original PropertyValue. + * + * @param propertyValue the PropertyValue object to be evaluated + * @return the evaluated PropertyValue if expression language is present, otherwise the original PropertyValue + */ PropertyValue getPropertyValue(PropertyValue propertyValue) { return propertyValue.isExpressionLanguagePresent() ? propertyValue.evaluateAttributeExpressions() : propertyValue; } + /** + * Retrieves the current configuration settings for the OpenTDF controller service. + * + * @return the current Config object containing configuration properties + * @throws ProcessException if an error occurs while retrieving the configuration + */ @Override public Config getConfig() throws ProcessException { return config; diff --git a/nifi-tdf-processors/src/test/java/io/opentdf/nifi/ConvertFromNanoTDFTest.java b/nifi-tdf-processors/src/test/java/io/opentdf/nifi/ConvertFromNanoTDFTest.java index 0a7cd8e..bd5fa55 100644 --- a/nifi-tdf-processors/src/test/java/io/opentdf/nifi/ConvertFromNanoTDFTest.java +++ b/nifi-tdf-processors/src/test/java/io/opentdf/nifi/ConvertFromNanoTDFTest.java @@ -37,7 +37,7 @@ void setup() { } @Test - public void testConvertFromTDF() throws Exception { + void testConvertFromTDF() throws Exception { TestRunner runner = TestRunners.newTestRunner(MockRunner.class); SDKBuilder mockSDKBuilder = mock(SDKBuilder.class); ((MockRunner) runner.getProcessor()).mockNanoTDF = mockNanoTDF; diff --git a/nifi-tdf-processors/src/test/java/io/opentdf/nifi/ConvertFromZTDFTest.java b/nifi-tdf-processors/src/test/java/io/opentdf/nifi/ConvertFromZTDFTest.java index ef1b673..4b35e7b 100644 --- a/nifi-tdf-processors/src/test/java/io/opentdf/nifi/ConvertFromZTDFTest.java +++ b/nifi-tdf-processors/src/test/java/io/opentdf/nifi/ConvertFromZTDFTest.java @@ -37,7 +37,7 @@ void setup() { } @Test - public void testConvertFromTDF() throws Exception { + void testConvertFromTDF() throws Exception { TestRunner runner = TestRunners.newTestRunner(MockRunner.class); SDKBuilder mockSDKBuilder = mock(SDKBuilder.class); ((MockRunner) runner.getProcessor()).mockTDF = mockTDF; diff --git a/nifi-tdf-processors/src/test/java/io/opentdf/nifi/ConvertToZTDFTest.java b/nifi-tdf-processors/src/test/java/io/opentdf/nifi/ConvertToZTDFTest.java index b6debd9..64d9e15 100644 --- a/nifi-tdf-processors/src/test/java/io/opentdf/nifi/ConvertToZTDFTest.java +++ b/nifi-tdf-processors/src/test/java/io/opentdf/nifi/ConvertToZTDFTest.java @@ -39,10 +39,10 @@ void setup() { } @Test - public void testToTDF() throws Exception { + void testToTDF() throws Exception { TestRunner runner = TestRunners.newTestRunner(MockRunner.class); Utils.setupTDFControllerService(runner); - Captures captures = commonProcessorTestSetup(runner); + commonProcessorTestSetup(runner); //message one has no attribute MockFlowFile messageOne = runner.enqueue("message one".getBytes()); @@ -72,7 +72,7 @@ public void testToTDF() throws Exception { @Test - public void testToTDF_WithAssertionsOn_None_Provided() throws Exception { + void testToTDF_WithAssertionsOn_None_Provided() throws Exception { TestRunner runner = TestRunners.newTestRunner(MockRunner.class); runner.setProperty(ConvertToZTDF.SIGN_ASSERTIONS, "true"); PrivateKeyService privateKeyService = mock(PrivateKeyService.class); @@ -95,7 +95,7 @@ public void testToTDF_WithAssertionsOn_None_Provided() throws Exception { @Test - public void testToTDF_WithAssertionsOn_And_Assertions_Provided() throws Exception { + void testToTDF_WithAssertionsOn_And_Assertions_Provided() throws Exception { TestRunner runner = TestRunners.newTestRunner(MockRunner.class); runner.setProperty(ConvertToZTDF.SIGN_ASSERTIONS, "true"); PrivateKeyService privateKeyService = mock(PrivateKeyService.class); @@ -130,11 +130,11 @@ public void testToTDF_WithAssertionsOn_And_Assertions_Provided() throws Exceptio """)); runner.run(1); List assertionConfigList = captures.configArgumentCaptor.getValue().assertionConfigList; - assertEquals(assertionConfigList.size(), 1); + assertEquals(1, assertionConfigList.size()); AssertionConfig assertionConfig = assertionConfigList.get(0); assertNotNull(assertionConfig, "Assertion configuration present"); assertNotNull(assertionConfig.assertionKey.key, "signing key present"); - assertEquals(assertionConfig.assertionKey.alg, AssertionConfig.AssertionKeyAlg.RS256); + assertEquals(AssertionConfig.AssertionKeyAlg.RS256, assertionConfig.assertionKey.alg); assertEquals("a test assertion", assertionConfig.statement.value); assertEquals("sample", assertionConfig.statement.format); assertEquals(AssertionConfig.Scope.Payload, assertionConfig.scope); @@ -207,6 +207,4 @@ TDF getTDF() { return mockTDF; } } - - -} \ No newline at end of file +} diff --git a/nifi-tdf-processors/src/test/java/io/opentdf/nifi/SimpleOpenTDFControllerServiceTest.java b/nifi-tdf-processors/src/test/java/io/opentdf/nifi/SimpleOpenTDFControllerServiceTest.java index 4a5c813..221b215 100644 --- a/nifi-tdf-processors/src/test/java/io/opentdf/nifi/SimpleOpenTDFControllerServiceTest.java +++ b/nifi-tdf-processors/src/test/java/io/opentdf/nifi/SimpleOpenTDFControllerServiceTest.java @@ -1,5 +1,69 @@ package io.opentdf.nifi; +import org.apache.nifi.controller.ConfigurationContext; +import org.apache.nifi.reporting.InitializationException; +import org.apache.nifi.components.PropertyValue; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + class SimpleOpenTDFControllerServiceTest { + @Test + void testEnabledWithValidValues() throws InitializationException { + SimpleOpenTDFControllerService service = new SimpleOpenTDFControllerService(); + + ConfigurationContext context = Mockito.mock(ConfigurationContext.class); + PropertyValue clientID = Mockito.mock(PropertyValue.class); + PropertyValue clientSecret = Mockito.mock(PropertyValue.class); + PropertyValue platformEndpoint = Mockito.mock(PropertyValue.class); + PropertyValue usePlainText = Mockito.mock(PropertyValue.class); + + Mockito.when(context.getProperty(SimpleOpenTDFControllerService.CLIENT_ID)).thenReturn(clientID); + Mockito.when(context.getProperty(SimpleOpenTDFControllerService.CLIENT_SECRET)).thenReturn(clientSecret); + Mockito.when(context.getProperty(SimpleOpenTDFControllerService.PLATFORM_ENDPOINT)).thenReturn(platformEndpoint); + Mockito.when(context.getProperty(SimpleOpenTDFControllerService.USE_PLAINTEXT)).thenReturn(usePlainText); + + Mockito.when(clientID.getValue()).thenReturn("Valid client ID"); + Mockito.when(clientSecret.getValue()).thenReturn("Valid client Secret"); + Mockito.when(platformEndpoint.getValue()).thenReturn("Valid platform endpoint"); + Mockito.when(usePlainText.asBoolean()).thenReturn(true); + + service.enabled(context); + + assertNotNull(service.getConfig()); + assertEquals("Valid client ID", service.getConfig().getClientId()); + assertEquals("Valid client Secret", service.getConfig().getClientSecret()); + assertEquals("Valid platform endpoint", service.getConfig().getPlatformEndpoint()); + } + + @Test + void testEnabledWithInvalidValues() { + SimpleOpenTDFControllerService service = new SimpleOpenTDFControllerService(); + + ConfigurationContext context = Mockito.mock(ConfigurationContext.class); + + // Mock property values with invalid data + PropertyValue clientID = Mockito.mock(PropertyValue.class); + PropertyValue clientSecret = Mockito.mock(PropertyValue.class); + PropertyValue platformEndpoint = Mockito.mock(PropertyValue.class); + PropertyValue usePlainText = Mockito.mock(PropertyValue.class); + + // Return null for all properties to simulate invalid values + Mockito.when(context.getProperty(SimpleOpenTDFControllerService.CLIENT_ID)).thenReturn(clientID); + Mockito.when(context.getProperty(SimpleOpenTDFControllerService.CLIENT_SECRET)).thenReturn(clientSecret); + Mockito.when(context.getProperty(SimpleOpenTDFControllerService.PLATFORM_ENDPOINT)).thenReturn(platformEndpoint); + Mockito.when(context.getProperty(SimpleOpenTDFControllerService.USE_PLAINTEXT)).thenReturn(usePlainText); + + Mockito.when(clientID.getValue()).thenReturn(null); + Mockito.when(clientSecret.getValue()).thenReturn(null); + Mockito.when(platformEndpoint.getValue()).thenReturn(null); + Mockito.when(usePlainText.asBoolean()).thenReturn(null); + + // Ensure that the enabled method throws an InitializationException + assertThrows(InitializationException.class, () -> service.enabled(context)); + } } \ No newline at end of file diff --git a/pom.xml b/pom.xml index 0af8523..a4462ed 100644 --- a/pom.xml +++ b/pom.xml @@ -32,6 +32,7 @@ 5.10.0 17 17 + 17 .7 opentdf opentdf_nifi @@ -100,6 +101,84 @@ 1.5.1 true + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + jacoco-prepare-agent + + prepare-agent + + + + jacoco-prepare-agent-integration + + prepare-agent-integration + + + + jacoco-report + + report + + test + + + check + + check + + + + + BUNDLE + + + LINE + COVEREDRATIO + ${jacoco.line.coverage} + + + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + UTF-8 + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + UTF-8 + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.release} + + -Xlint:unchecked + + + + + org.apache.maven.plugins + maven-install-plugin + 3.1.3 + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.3 + @@ -131,6 +210,25 @@ + + develop + + true + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + 1 + + + + + stage @@ -244,11 +342,18 @@ - report-aggregate - verify + report + test - report-aggregate + report + + ${project.parent.basedir}/target/jacoco.exec + ${project.parent.basedir}/target/site/jacoco/ + + XML + +