diff --git a/src/main/java/com/getaxonflow/sdk/AxonFlow.java b/src/main/java/com/getaxonflow/sdk/AxonFlow.java index 6a50c18..e8e2860 100644 --- a/src/main/java/com/getaxonflow/sdk/AxonFlow.java +++ b/src/main/java/com/getaxonflow/sdk/AxonFlow.java @@ -17,6 +17,7 @@ import com.getaxonflow.sdk.exceptions.*; import com.getaxonflow.sdk.types.*; +import com.getaxonflow.sdk.types.policies.PolicyTypes.*; import com.getaxonflow.sdk.util.*; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; @@ -584,6 +585,392 @@ public CompletableFuture queryConnectorAsync(ConnectorQuery q return CompletableFuture.supplyAsync(() -> queryConnector(query), asyncExecutor); } + // ======================================================================== + // Policy CRUD - Static Policies + // ======================================================================== + + /** + * Lists static policies with optional filtering. + * + * @return list of static policies + */ + public List listStaticPolicies() { + return listStaticPolicies(null); + } + + /** + * Lists static policies with filtering options. + * + * @param options filtering options + * @return list of static policies + */ + public List listStaticPolicies(ListStaticPoliciesOptions options) { + return retryExecutor.execute(() -> { + String path = buildPolicyQueryString("/api/v1/static-policies", options); + Request httpRequest = buildRequest("GET", path, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + StaticPoliciesResponse wrapper = parseResponse(response, StaticPoliciesResponse.class); + return wrapper.getPolicies() != null ? wrapper.getPolicies() : java.util.Collections.emptyList(); + } + }, "listStaticPolicies"); + } + + /** + * Gets a specific static policy by ID. + * + * @param policyId the policy ID + * @return the static policy + */ + public StaticPolicy getStaticPolicy(String policyId) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + + return retryExecutor.execute(() -> { + Request httpRequest = buildRequest("GET", "/api/v1/static-policies/" + policyId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, StaticPolicy.class); + } + }, "getStaticPolicy"); + } + + /** + * Creates a new static policy. + * + * @param request the create request + * @return the created policy + */ + public StaticPolicy createStaticPolicy(CreateStaticPolicyRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute(() -> { + Request httpRequest = buildRequest("POST", "/api/v1/static-policies", request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, StaticPolicy.class); + } + }, "createStaticPolicy"); + } + + /** + * Updates an existing static policy. + * + * @param policyId the policy ID + * @param request the update request + * @return the updated policy + */ + public StaticPolicy updateStaticPolicy(String policyId, UpdateStaticPolicyRequest request) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute(() -> { + Request httpRequest = buildRequest("PUT", "/api/v1/static-policies/" + policyId, request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, StaticPolicy.class); + } + }, "updateStaticPolicy"); + } + + /** + * Deletes a static policy. + * + * @param policyId the policy ID + */ + public void deleteStaticPolicy(String policyId) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + + retryExecutor.execute(() -> { + Request httpRequest = buildRequest("DELETE", "/api/v1/static-policies/" + policyId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful() && response.code() != 204) { + handleErrorResponse(response); + } + return null; + } + }, "deleteStaticPolicy"); + } + + /** + * Toggles a static policy's enabled status. + * + * @param policyId the policy ID + * @param enabled the new enabled status + * @return the updated policy + */ + public StaticPolicy toggleStaticPolicy(String policyId, boolean enabled) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + + return retryExecutor.execute(() -> { + Map body = Map.of("enabled", enabled); + Request httpRequest = buildPatchRequest("/api/v1/static-policies/" + policyId, body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, StaticPolicy.class); + } + }, "toggleStaticPolicy"); + } + + /** + * Gets effective static policies after inheritance and overrides. + * + * @return list of effective policies + */ + public List getEffectiveStaticPolicies() { + return getEffectiveStaticPolicies(null); + } + + /** + * Gets effective static policies with options. + * + * @param options filtering options + * @return list of effective policies + */ + public List getEffectiveStaticPolicies(EffectivePoliciesOptions options) { + return retryExecutor.execute(() -> { + StringBuilder path = new StringBuilder("/api/v1/static-policies/effective"); + if (options != null) { + String query = buildEffectivePoliciesQuery(options); + if (!query.isEmpty()) { + path.append("?").append(query); + } + } + Request httpRequest = buildRequest("GET", path.toString(), null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + EffectivePoliciesResponse wrapper = parseResponse(response, EffectivePoliciesResponse.class); + return wrapper.getStaticPolicies() != null ? wrapper.getStaticPolicies() : java.util.Collections.emptyList(); + } + }, "getEffectiveStaticPolicies"); + } + + /** + * Tests a regex pattern against sample inputs. + * + * @param pattern the regex pattern + * @param testInputs sample inputs to test + * @return the test result + */ + public TestPatternResult testPattern(String pattern, List testInputs) { + Objects.requireNonNull(pattern, "pattern cannot be null"); + Objects.requireNonNull(testInputs, "testInputs cannot be null"); + + return retryExecutor.execute(() -> { + Map body = Map.of( + "pattern", pattern, + "inputs", testInputs + ); + Request httpRequest = buildRequest("POST", "/api/v1/static-policies/test", body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, TestPatternResult.class); + } + }, "testPattern"); + } + + /** + * Gets version history for a static policy. + * + * @param policyId the policy ID + * @return list of policy versions + */ + public List getStaticPolicyVersions(String policyId) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + + return retryExecutor.execute(() -> { + Request httpRequest = buildRequest("GET", "/api/v1/static-policies/" + policyId + "/versions", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, new TypeReference>() {}); + } + }, "getStaticPolicyVersions"); + } + + // ======================================================================== + // Policy CRUD - Overrides (Enterprise) + // ======================================================================== + + /** + * Creates a policy override. + * + * @param policyId the policy ID + * @param request the override request + * @return the created override + */ + public PolicyOverride createPolicyOverride(String policyId, CreatePolicyOverrideRequest request) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute(() -> { + Request httpRequest = buildRequest("POST", "/api/v1/static-policies/" + policyId + "/override", request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, PolicyOverride.class); + } + }, "createPolicyOverride"); + } + + /** + * Deletes a policy override. + * + * @param policyId the policy ID + */ + public void deletePolicyOverride(String policyId) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + + retryExecutor.execute(() -> { + Request httpRequest = buildRequest("DELETE", "/api/v1/static-policies/" + policyId + "/override", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful() && response.code() != 204) { + handleErrorResponse(response); + } + return null; + } + }, "deletePolicyOverride"); + } + + // ======================================================================== + // Policy CRUD - Dynamic Policies + // ======================================================================== + + /** + * Lists dynamic policies. + * + * @return list of dynamic policies + */ + public List listDynamicPolicies() { + return listDynamicPolicies(null); + } + + /** + * Lists dynamic policies with filtering options. + * + * @param options filtering options + * @return list of dynamic policies + */ + public List listDynamicPolicies(ListDynamicPoliciesOptions options) { + return retryExecutor.execute(() -> { + String path = buildDynamicPolicyQueryString("/api/v1/policies", options); + Request httpRequest = buildRequest("GET", path, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, new TypeReference>() {}); + } + }, "listDynamicPolicies"); + } + + /** + * Gets a specific dynamic policy by ID. + * + * @param policyId the policy ID + * @return the dynamic policy + */ + public DynamicPolicy getDynamicPolicy(String policyId) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + + return retryExecutor.execute(() -> { + Request httpRequest = buildRequest("GET", "/api/v1/policies/" + policyId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, DynamicPolicy.class); + } + }, "getDynamicPolicy"); + } + + /** + * Creates a new dynamic policy. + * + * @param request the create request + * @return the created policy + */ + public DynamicPolicy createDynamicPolicy(CreateDynamicPolicyRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute(() -> { + Request httpRequest = buildRequest("POST", "/api/v1/policies", request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, DynamicPolicy.class); + } + }, "createDynamicPolicy"); + } + + /** + * Updates an existing dynamic policy. + * + * @param policyId the policy ID + * @param request the update request + * @return the updated policy + */ + public DynamicPolicy updateDynamicPolicy(String policyId, UpdateDynamicPolicyRequest request) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute(() -> { + Request httpRequest = buildRequest("PUT", "/api/v1/policies/" + policyId, request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, DynamicPolicy.class); + } + }, "updateDynamicPolicy"); + } + + /** + * Deletes a dynamic policy. + * + * @param policyId the policy ID + */ + public void deleteDynamicPolicy(String policyId) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + + retryExecutor.execute(() -> { + Request httpRequest = buildRequest("DELETE", "/api/v1/policies/" + policyId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful() && response.code() != 204) { + handleErrorResponse(response); + } + return null; + } + }, "deleteDynamicPolicy"); + } + + /** + * Toggles a dynamic policy's enabled status. + * + * @param policyId the policy ID + * @param enabled the new enabled status + * @return the updated policy + */ + public DynamicPolicy toggleDynamicPolicy(String policyId, boolean enabled) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + + return retryExecutor.execute(() -> { + Map body = Map.of("enabled", enabled); + Request httpRequest = buildPatchRequest("/api/v1/policies/" + policyId, body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, DynamicPolicy.class); + } + }, "toggleDynamicPolicy"); + } + + /** + * Gets effective dynamic policies after inheritance. + * + * @return list of effective policies + */ + public List getEffectiveDynamicPolicies() { + return getEffectiveDynamicPolicies(null); + } + + /** + * Gets effective dynamic policies with options. + * + * @param options filtering options + * @return list of effective policies + */ + public List getEffectiveDynamicPolicies(EffectivePoliciesOptions options) { + return retryExecutor.execute(() -> { + StringBuilder path = new StringBuilder("/api/v1/policies/effective"); + if (options != null) { + String query = buildEffectivePoliciesQuery(options); + if (!query.isEmpty()) { + path.append("?").append(query); + } + } + Request httpRequest = buildRequest("GET", path.toString(), null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, new TypeReference>() {}); + } + }, "getEffectiveDynamicPolicies"); + } + // ======================================================================== // Configuration Access // ======================================================================== @@ -631,6 +1018,11 @@ private Request buildRequest(String method, String path, Object body) { // Add authentication headers addAuthHeaders(builder); + // Add tenant ID for policy APIs (uses clientId) + if (config.getClientId() != null && !config.getClientId().isEmpty()) { + builder.header("X-Tenant-ID", config.getClientId()); + } + // Add mode header if (config.getMode() != null) { builder.header("X-AxonFlow-Mode", config.getMode().getValue()); @@ -667,6 +1059,138 @@ private Request buildRequest(String method, String path, Object body) { return builder.build(); } + private Request buildPatchRequest(String path, Object body) { + HttpUrl url = HttpUrl.parse(config.getAgentUrl() + path); + if (url == null) { + throw new ConfigurationException("Invalid URL: " + config.getAgentUrl() + path); + } + + Request.Builder builder = new Request.Builder() + .url(url) + .header("User-Agent", config.getUserAgent()) + .header("Accept", "application/json"); + + addAuthHeaders(builder); + + if (config.getMode() != null) { + builder.header("X-AxonFlow-Mode", config.getMode().getValue()); + } + + RequestBody requestBody = null; + if (body != null) { + try { + String json = objectMapper.writeValueAsString(body); + requestBody = RequestBody.create(json, JSON); + } catch (JsonProcessingException e) { + throw new AxonFlowException("Failed to serialize request body", e); + } + } + + builder.patch(requestBody != null ? requestBody : RequestBody.create("", JSON)); + return builder.build(); + } + + private String buildPolicyQueryString(String basePath, ListStaticPoliciesOptions options) { + if (options == null) { + return basePath; + } + + StringBuilder path = new StringBuilder(basePath); + StringBuilder query = new StringBuilder(); + + if (options.getCategory() != null) { + appendQueryParam(query, "category", options.getCategory().getValue()); + } + if (options.getTier() != null) { + appendQueryParam(query, "tier", options.getTier().getValue()); + } + if (options.getEnabled() != null) { + appendQueryParam(query, "enabled", options.getEnabled().toString()); + } + if (options.getLimit() != null) { + appendQueryParam(query, "limit", options.getLimit().toString()); + } + if (options.getOffset() != null) { + appendQueryParam(query, "offset", options.getOffset().toString()); + } + if (options.getSortBy() != null) { + appendQueryParam(query, "sort_by", options.getSortBy()); + } + if (options.getSortOrder() != null) { + appendQueryParam(query, "sort_order", options.getSortOrder()); + } + if (options.getSearch() != null) { + appendQueryParam(query, "search", options.getSearch()); + } + + if (query.length() > 0) { + path.append("?").append(query); + } + return path.toString(); + } + + private String buildDynamicPolicyQueryString(String basePath, ListDynamicPoliciesOptions options) { + if (options == null) { + return basePath; + } + + StringBuilder path = new StringBuilder(basePath); + StringBuilder query = new StringBuilder(); + + if (options.getCategory() != null) { + appendQueryParam(query, "category", options.getCategory().getValue()); + } + if (options.getTier() != null) { + appendQueryParam(query, "tier", options.getTier().getValue()); + } + if (options.getEnabled() != null) { + appendQueryParam(query, "enabled", options.getEnabled().toString()); + } + if (options.getLimit() != null) { + appendQueryParam(query, "limit", options.getLimit().toString()); + } + if (options.getOffset() != null) { + appendQueryParam(query, "offset", options.getOffset().toString()); + } + if (options.getSortBy() != null) { + appendQueryParam(query, "sort_by", options.getSortBy()); + } + if (options.getSortOrder() != null) { + appendQueryParam(query, "sort_order", options.getSortOrder()); + } + if (options.getSearch() != null) { + appendQueryParam(query, "search", options.getSearch()); + } + + if (query.length() > 0) { + path.append("?").append(query); + } + return path.toString(); + } + + private String buildEffectivePoliciesQuery(EffectivePoliciesOptions options) { + StringBuilder query = new StringBuilder(); + + if (options.getCategory() != null) { + appendQueryParam(query, "category", options.getCategory().getValue()); + } + if (options.isIncludeDisabled()) { + appendQueryParam(query, "include_disabled", "true"); + } + if (options.isIncludeOverridden()) { + appendQueryParam(query, "include_overridden", "true"); + } + + return query.toString(); + } + + private void appendQueryParam(StringBuilder query, String name, String value) { + if (query.length() > 0) { + query.append("&"); + } + query.append(name).append("=").append(value); + } + private void addAuthHeaders(Request.Builder builder) { // Skip auth for localhost in self-hosted mode if (config.isLocalhost()) { diff --git a/src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java b/src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java new file mode 100644 index 0000000..414dd49 --- /dev/null +++ b/src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java @@ -0,0 +1,959 @@ +/* + * Copyright 2025 AxonFlow + * + * 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.getaxonflow.sdk.types.policies; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +/** + * Policy CRUD types for the Unified Policy Architecture v2.0.0. + */ +public final class PolicyTypes { + + private PolicyTypes() {} + + // ======================================================================== + // Enums + // ======================================================================== + + /** + * Policy categories for organization and filtering. + */ + public enum PolicyCategory { + SECURITY_SQLI("security-sqli"), + SECURITY_ADMIN("security-admin"), + PII_GLOBAL("pii-global"), + PII_US("pii-us"), + PII_EU("pii-eu"), + PII_INDIA("pii-india"), + DYNAMIC_RISK("dynamic-risk"), + DYNAMIC_COMPLIANCE("dynamic-compliance"), + DYNAMIC_SECURITY("dynamic-security"), + DYNAMIC_COST("dynamic-cost"), + DYNAMIC_ACCESS("dynamic-access"), + CUSTOM("custom"); + + private final String value; + + PolicyCategory(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + } + + /** + * Policy tiers determine where policies apply. + */ + public enum PolicyTier { + SYSTEM("system"), + ORGANIZATION("organization"), + TENANT("tenant"); + + private final String value; + + PolicyTier(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + } + + /** + * Override action for policy overrides. + */ + public enum OverrideAction { + BLOCK("block"), + WARN("warn"), + LOG("log"), + REDACT("redact"); + + private final String value; + + OverrideAction(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + } + + /** + * Action to take when a policy matches. + */ + public enum PolicyAction { + BLOCK("block"), + WARN("warn"), + LOG("log"), + REDACT("redact"), + ALLOW("allow"); + + private final String value; + + PolicyAction(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + } + + /** + * Policy severity levels. + */ + public enum PolicySeverity { + CRITICAL("critical"), + HIGH("high"), + MEDIUM("medium"), + LOW("low"); + + private final String value; + + PolicySeverity(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + } + + // ======================================================================== + // Static Policy Types + // ======================================================================== + + /** + * Static policy definition. + */ + public static class StaticPolicy { + private String id; + private String name; + private String description; + private PolicyCategory category; + private PolicyTier tier; + private String pattern; + private PolicySeverity severity; + private boolean enabled; + private PolicyAction action; + @JsonProperty("organization_id") + private String organizationId; + @JsonProperty("tenant_id") + private String tenantId; + @JsonProperty("created_at") + private Instant createdAt; + @JsonProperty("updated_at") + private Instant updatedAt; + private Integer version; + @JsonProperty("has_override") + private Boolean hasOverride; + private PolicyOverride override; + + // Getters and setters + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + public PolicyCategory getCategory() { return category; } + public void setCategory(PolicyCategory category) { this.category = category; } + public PolicyTier getTier() { return tier; } + public void setTier(PolicyTier tier) { this.tier = tier; } + public String getPattern() { return pattern; } + public void setPattern(String pattern) { this.pattern = pattern; } + public PolicySeverity getSeverity() { return severity; } + public void setSeverity(PolicySeverity severity) { this.severity = severity; } + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + public PolicyAction getAction() { return action; } + public void setAction(PolicyAction action) { this.action = action; } + public String getOrganizationId() { return organizationId; } + public void setOrganizationId(String organizationId) { this.organizationId = organizationId; } + public String getTenantId() { return tenantId; } + public void setTenantId(String tenantId) { this.tenantId = tenantId; } + public Instant getCreatedAt() { return createdAt; } + public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } + public Instant getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; } + public Integer getVersion() { return version; } + public void setVersion(Integer version) { this.version = version; } + public Boolean getHasOverride() { return hasOverride; } + public void setHasOverride(Boolean hasOverride) { this.hasOverride = hasOverride; } + public PolicyOverride getOverride() { return override; } + public void setOverride(PolicyOverride override) { this.override = override; } + } + + /** + * Policy override configuration. + */ + public static class PolicyOverride { + @JsonProperty("policy_id") + private String policyId; + private OverrideAction action; + private String reason; + @JsonProperty("created_by") + private String createdBy; + @JsonProperty("created_at") + private Instant createdAt; + @JsonProperty("expires_at") + private Instant expiresAt; + private boolean active; + + // Getters and setters + public String getPolicyId() { return policyId; } + public void setPolicyId(String policyId) { this.policyId = policyId; } + public OverrideAction getAction() { return action; } + public void setAction(OverrideAction action) { this.action = action; } + public String getReason() { return reason; } + public void setReason(String reason) { this.reason = reason; } + public String getCreatedBy() { return createdBy; } + public void setCreatedBy(String createdBy) { this.createdBy = createdBy; } + public Instant getCreatedAt() { return createdAt; } + public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } + public Instant getExpiresAt() { return expiresAt; } + public void setExpiresAt(Instant expiresAt) { this.expiresAt = expiresAt; } + public boolean isActive() { return active; } + public void setActive(boolean active) { this.active = active; } + } + + /** + * Options for listing static policies. + */ + public static class ListStaticPoliciesOptions { + private PolicyCategory category; + private PolicyTier tier; + private Boolean enabled; + private Integer limit; + private Integer offset; + private String sortBy; + private String sortOrder; + private String search; + + public static Builder builder() { + return new Builder(); + } + + public PolicyCategory getCategory() { return category; } + public PolicyTier getTier() { return tier; } + public Boolean getEnabled() { return enabled; } + public Integer getLimit() { return limit; } + public Integer getOffset() { return offset; } + public String getSortBy() { return sortBy; } + public String getSortOrder() { return sortOrder; } + public String getSearch() { return search; } + + public static class Builder { + private final ListStaticPoliciesOptions options = new ListStaticPoliciesOptions(); + + public Builder category(PolicyCategory category) { + options.category = category; + return this; + } + + public Builder tier(PolicyTier tier) { + options.tier = tier; + return this; + } + + public Builder enabled(Boolean enabled) { + options.enabled = enabled; + return this; + } + + public Builder limit(Integer limit) { + options.limit = limit; + return this; + } + + public Builder offset(Integer offset) { + options.offset = offset; + return this; + } + + public Builder sortBy(String sortBy) { + options.sortBy = sortBy; + return this; + } + + public Builder sortOrder(String sortOrder) { + options.sortOrder = sortOrder; + return this; + } + + public Builder search(String search) { + options.search = search; + return this; + } + + public ListStaticPoliciesOptions build() { + return options; + } + } + } + + /** + * Request to create a new static policy. + */ + public static class CreateStaticPolicyRequest { + private String name; + private String description; + private PolicyCategory category; + private PolicyTier tier = PolicyTier.TENANT; + private String pattern; + private PolicySeverity severity = PolicySeverity.MEDIUM; + private boolean enabled = true; + private PolicyAction action = PolicyAction.BLOCK; + + public static Builder builder() { + return new Builder(); + } + + public String getName() { return name; } + public String getDescription() { return description; } + public PolicyCategory getCategory() { return category; } + public PolicyTier getTier() { return tier; } + public String getPattern() { return pattern; } + public PolicySeverity getSeverity() { return severity; } + public boolean isEnabled() { return enabled; } + public PolicyAction getAction() { return action; } + + public static class Builder { + private final CreateStaticPolicyRequest request = new CreateStaticPolicyRequest(); + + public Builder name(String name) { + request.name = name; + return this; + } + + public Builder description(String description) { + request.description = description; + return this; + } + + public Builder category(PolicyCategory category) { + request.category = category; + return this; + } + + public Builder tier(PolicyTier tier) { + request.tier = tier; + return this; + } + + public Builder pattern(String pattern) { + request.pattern = pattern; + return this; + } + + public Builder severity(PolicySeverity severity) { + request.severity = severity; + return this; + } + + public Builder enabled(boolean enabled) { + request.enabled = enabled; + return this; + } + + public Builder action(PolicyAction action) { + request.action = action; + return this; + } + + public CreateStaticPolicyRequest build() { + return request; + } + } + } + + /** + * Request to update an existing static policy. + */ + public static class UpdateStaticPolicyRequest { + private String name; + private String description; + private PolicyCategory category; + private String pattern; + private PolicySeverity severity; + private Boolean enabled; + private PolicyAction action; + + public static Builder builder() { + return new Builder(); + } + + public String getName() { return name; } + public String getDescription() { return description; } + public PolicyCategory getCategory() { return category; } + public String getPattern() { return pattern; } + public PolicySeverity getSeverity() { return severity; } + public Boolean getEnabled() { return enabled; } + public PolicyAction getAction() { return action; } + + public static class Builder { + private final UpdateStaticPolicyRequest request = new UpdateStaticPolicyRequest(); + + public Builder name(String name) { + request.name = name; + return this; + } + + public Builder description(String description) { + request.description = description; + return this; + } + + public Builder category(PolicyCategory category) { + request.category = category; + return this; + } + + public Builder pattern(String pattern) { + request.pattern = pattern; + return this; + } + + public Builder severity(PolicySeverity severity) { + request.severity = severity; + return this; + } + + public Builder enabled(Boolean enabled) { + request.enabled = enabled; + return this; + } + + public Builder action(PolicyAction action) { + request.action = action; + return this; + } + + public UpdateStaticPolicyRequest build() { + return request; + } + } + } + + /** + * Request to create a policy override. + */ + public static class CreatePolicyOverrideRequest { + private OverrideAction action; + private String reason; + @JsonProperty("expires_at") + private Instant expiresAt; + + public static Builder builder() { + return new Builder(); + } + + public OverrideAction getAction() { return action; } + public String getReason() { return reason; } + public Instant getExpiresAt() { return expiresAt; } + + public static class Builder { + private final CreatePolicyOverrideRequest request = new CreatePolicyOverrideRequest(); + + public Builder action(OverrideAction action) { + request.action = action; + return this; + } + + public Builder reason(String reason) { + request.reason = reason; + return this; + } + + public Builder expiresAt(Instant expiresAt) { + request.expiresAt = expiresAt; + return this; + } + + public CreatePolicyOverrideRequest build() { + return request; + } + } + } + + // ======================================================================== + // Dynamic Policy Types + // ======================================================================== + + /** + * Dynamic policy configuration. + */ + public static class DynamicPolicyConfig { + private String type; + private Map rules; + private List conditions; + private PolicyAction action; + private Map parameters; + + // Getters and setters + public String getType() { return type; } + public void setType(String type) { this.type = type; } + public Map getRules() { return rules; } + public void setRules(Map rules) { this.rules = rules; } + public List getConditions() { return conditions; } + public void setConditions(List conditions) { this.conditions = conditions; } + public PolicyAction getAction() { return action; } + public void setAction(PolicyAction action) { this.action = action; } + public Map getParameters() { return parameters; } + public void setParameters(Map parameters) { this.parameters = parameters; } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private final DynamicPolicyConfig config = new DynamicPolicyConfig(); + + public Builder type(String type) { + config.type = type; + return this; + } + + public Builder rules(Map rules) { + config.rules = rules; + return this; + } + + public Builder action(PolicyAction action) { + config.action = action; + return this; + } + + public DynamicPolicyConfig build() { + return config; + } + } + } + + /** + * Condition for dynamic policy evaluation. + */ + public static class DynamicPolicyCondition { + private String field; + private String operator; + private Object value; + + public String getField() { return field; } + public void setField(String field) { this.field = field; } + public String getOperator() { return operator; } + public void setOperator(String operator) { this.operator = operator; } + public Object getValue() { return value; } + public void setValue(Object value) { this.value = value; } + } + + /** + * Dynamic policy definition. + */ + public static class DynamicPolicy { + private String id; + private String name; + private String description; + private PolicyCategory category; + private PolicyTier tier; + private boolean enabled; + @JsonProperty("organization_id") + private String organizationId; + @JsonProperty("tenant_id") + private String tenantId; + private DynamicPolicyConfig config; + @JsonProperty("created_at") + private Instant createdAt; + @JsonProperty("updated_at") + private Instant updatedAt; + private Integer version; + + // Getters and setters + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + public PolicyCategory getCategory() { return category; } + public void setCategory(PolicyCategory category) { this.category = category; } + public PolicyTier getTier() { return tier; } + public void setTier(PolicyTier tier) { this.tier = tier; } + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + public String getOrganizationId() { return organizationId; } + public void setOrganizationId(String organizationId) { this.organizationId = organizationId; } + public String getTenantId() { return tenantId; } + public void setTenantId(String tenantId) { this.tenantId = tenantId; } + public DynamicPolicyConfig getConfig() { return config; } + public void setConfig(DynamicPolicyConfig config) { this.config = config; } + public Instant getCreatedAt() { return createdAt; } + public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } + public Instant getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; } + public Integer getVersion() { return version; } + public void setVersion(Integer version) { this.version = version; } + } + + /** + * Options for listing dynamic policies. + */ + public static class ListDynamicPoliciesOptions { + private PolicyCategory category; + private PolicyTier tier; + private Boolean enabled; + private Integer limit; + private Integer offset; + private String sortBy; + private String sortOrder; + private String search; + + public static Builder builder() { + return new Builder(); + } + + public PolicyCategory getCategory() { return category; } + public PolicyTier getTier() { return tier; } + public Boolean getEnabled() { return enabled; } + public Integer getLimit() { return limit; } + public Integer getOffset() { return offset; } + public String getSortBy() { return sortBy; } + public String getSortOrder() { return sortOrder; } + public String getSearch() { return search; } + + public static class Builder { + private final ListDynamicPoliciesOptions options = new ListDynamicPoliciesOptions(); + + public Builder category(PolicyCategory category) { + options.category = category; + return this; + } + + public Builder tier(PolicyTier tier) { + options.tier = tier; + return this; + } + + public Builder enabled(Boolean enabled) { + options.enabled = enabled; + return this; + } + + public Builder limit(Integer limit) { + options.limit = limit; + return this; + } + + public Builder offset(Integer offset) { + options.offset = offset; + return this; + } + + public Builder sortBy(String sortBy) { + options.sortBy = sortBy; + return this; + } + + public Builder sortOrder(String sortOrder) { + options.sortOrder = sortOrder; + return this; + } + + public Builder search(String search) { + options.search = search; + return this; + } + + public ListDynamicPoliciesOptions build() { + return options; + } + } + } + + /** + * Request to create a dynamic policy. + */ + public static class CreateDynamicPolicyRequest { + private String name; + private String description; + private PolicyCategory category; + private DynamicPolicyConfig config; + private boolean enabled = true; + + public static Builder builder() { + return new Builder(); + } + + public String getName() { return name; } + public String getDescription() { return description; } + public PolicyCategory getCategory() { return category; } + public DynamicPolicyConfig getConfig() { return config; } + public boolean isEnabled() { return enabled; } + + public static class Builder { + private final CreateDynamicPolicyRequest request = new CreateDynamicPolicyRequest(); + + public Builder name(String name) { + request.name = name; + return this; + } + + public Builder description(String description) { + request.description = description; + return this; + } + + public Builder category(PolicyCategory category) { + request.category = category; + return this; + } + + public Builder config(DynamicPolicyConfig config) { + request.config = config; + return this; + } + + public Builder enabled(boolean enabled) { + request.enabled = enabled; + return this; + } + + public CreateDynamicPolicyRequest build() { + return request; + } + } + } + + /** + * Request to update a dynamic policy. + */ + public static class UpdateDynamicPolicyRequest { + private String name; + private String description; + private PolicyCategory category; + private DynamicPolicyConfig config; + private Boolean enabled; + + public static Builder builder() { + return new Builder(); + } + + public String getName() { return name; } + public String getDescription() { return description; } + public PolicyCategory getCategory() { return category; } + public DynamicPolicyConfig getConfig() { return config; } + public Boolean getEnabled() { return enabled; } + + public static class Builder { + private final UpdateDynamicPolicyRequest request = new UpdateDynamicPolicyRequest(); + + public Builder name(String name) { + request.name = name; + return this; + } + + public Builder description(String description) { + request.description = description; + return this; + } + + public Builder category(PolicyCategory category) { + request.category = category; + return this; + } + + public Builder config(DynamicPolicyConfig config) { + request.config = config; + return this; + } + + public Builder enabled(Boolean enabled) { + request.enabled = enabled; + return this; + } + + public UpdateDynamicPolicyRequest build() { + return request; + } + } + } + + // ======================================================================== + // Pattern Testing Types + // ======================================================================== + + /** + * Result of testing a regex pattern. + */ + public static class TestPatternResult { + private boolean valid; + private String error; + private String pattern; + private List inputs; + private List matches; + + public boolean isValid() { return valid; } + public void setValid(boolean valid) { this.valid = valid; } + public String getError() { return error; } + public void setError(String error) { this.error = error; } + public String getPattern() { return pattern; } + public void setPattern(String pattern) { this.pattern = pattern; } + public List getInputs() { return inputs; } + public void setInputs(List inputs) { this.inputs = inputs; } + public List getMatches() { return matches; } + public void setMatches(List matches) { this.matches = matches; } + } + + /** + * Individual pattern match result. + */ + public static class TestPatternMatch { + private String input; + private boolean matched; + @JsonProperty("matched_text") + private String matchedText; + private Integer position; + + public String getInput() { return input; } + public void setInput(String input) { this.input = input; } + public boolean isMatched() { return matched; } + public void setMatched(boolean matched) { this.matched = matched; } + public String getMatchedText() { return matchedText; } + public void setMatchedText(String matchedText) { this.matchedText = matchedText; } + public Integer getPosition() { return position; } + public void setPosition(Integer position) { this.position = position; } + } + + // ======================================================================== + // Policy Version Types + // ======================================================================== + + /** + * Policy version history entry. + */ + public static class PolicyVersion { + private int version; + @JsonProperty("changed_by") + private String changedBy; + @JsonProperty("changed_at") + private Instant changedAt; + @JsonProperty("change_type") + private String changeType; + @JsonProperty("change_description") + private String changeDescription; + @JsonProperty("previous_values") + private Map previousValues; + @JsonProperty("new_values") + private Map newValues; + + public int getVersion() { return version; } + public void setVersion(int version) { this.version = version; } + public String getChangedBy() { return changedBy; } + public void setChangedBy(String changedBy) { this.changedBy = changedBy; } + public Instant getChangedAt() { return changedAt; } + public void setChangedAt(Instant changedAt) { this.changedAt = changedAt; } + public String getChangeType() { return changeType; } + public void setChangeType(String changeType) { this.changeType = changeType; } + public String getChangeDescription() { return changeDescription; } + public void setChangeDescription(String changeDescription) { this.changeDescription = changeDescription; } + public Map getPreviousValues() { return previousValues; } + public void setPreviousValues(Map previousValues) { this.previousValues = previousValues; } + public Map getNewValues() { return newValues; } + public void setNewValues(Map newValues) { this.newValues = newValues; } + } + + /** + * Options for getting effective policies. + */ + public static class EffectivePoliciesOptions { + private PolicyCategory category; + private boolean includeDisabled; + private boolean includeOverridden; + + public static Builder builder() { + return new Builder(); + } + + public PolicyCategory getCategory() { return category; } + public boolean isIncludeDisabled() { return includeDisabled; } + public boolean isIncludeOverridden() { return includeOverridden; } + + public static class Builder { + private final EffectivePoliciesOptions options = new EffectivePoliciesOptions(); + + public Builder category(PolicyCategory category) { + options.category = category; + return this; + } + + public Builder includeDisabled(boolean includeDisabled) { + options.includeDisabled = includeDisabled; + return this; + } + + public Builder includeOverridden(boolean includeOverridden) { + options.includeOverridden = includeOverridden; + return this; + } + + public EffectivePoliciesOptions build() { + return options; + } + } + } + + // ======================================================================== + // Response Wrappers + // ======================================================================== + + /** + * Wrapper for list static policies response. + */ + public static class StaticPoliciesResponse { + private List policies; + + public List getPolicies() { return policies; } + public void setPolicies(List policies) { this.policies = policies; } + } + + /** + * Wrapper for effective policies response. + */ + public static class EffectivePoliciesResponse { + @JsonProperty("static") + private List staticPolicies; + @JsonProperty("dynamic") + private List dynamicPolicies; + + public List getStaticPolicies() { return staticPolicies; } + public void setStaticPolicies(List staticPolicies) { this.staticPolicies = staticPolicies; } + public List getDynamicPolicies() { return dynamicPolicies; } + public void setDynamicPolicies(List dynamicPolicies) { this.dynamicPolicies = dynamicPolicies; } + } +} diff --git a/src/test/java/com/getaxonflow/sdk/PolicyTest.java b/src/test/java/com/getaxonflow/sdk/PolicyTest.java new file mode 100644 index 0000000..4019fe8 --- /dev/null +++ b/src/test/java/com/getaxonflow/sdk/PolicyTest.java @@ -0,0 +1,667 @@ +/* + * Copyright 2025 AxonFlow + * + * 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.getaxonflow.sdk; + +import com.getaxonflow.sdk.types.policies.PolicyTypes.*; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + +/** + * Tests for Policy CRUD methods. + * Part of Unified Policy Architecture v2.0.0. + */ +@WireMockTest +@DisplayName("Policy CRUD Methods") +class PolicyTest { + + private AxonFlow axonflow; + + private static final String SAMPLE_STATIC_POLICY = + "{" + + "\"id\": \"pol_123\"," + + "\"name\": \"Block SQL Injection\"," + + "\"description\": \"Blocks SQL injection attempts\"," + + "\"category\": \"security-sqli\"," + + "\"tier\": \"system\"," + + "\"pattern\": \"(?i)(union\\\\s+select|drop\\\\s+table)\"," + + "\"severity\": \"critical\"," + + "\"enabled\": true," + + "\"action\": \"block\"," + + "\"created_at\": \"2025-01-01T00:00:00Z\"," + + "\"updated_at\": \"2025-01-01T00:00:00Z\"," + + "\"version\": 1" + + "}"; + + private static final String SAMPLE_DYNAMIC_POLICY = + "{" + + "\"id\": \"dpol_456\"," + + "\"name\": \"Rate Limit API\"," + + "\"description\": \"Rate limit API calls\"," + + "\"category\": \"dynamic-cost\"," + + "\"tier\": \"organization\"," + + "\"enabled\": true," + + "\"config\": {" + + "\"type\": \"rate-limit\"," + + "\"rules\": {\"maxRequestsPerMinute\": 100}," + + "\"action\": \"block\"" + + "}," + + "\"created_at\": \"2025-01-01T00:00:00Z\"," + + "\"updated_at\": \"2025-01-01T00:00:00Z\"," + + "\"version\": 1" + + "}"; + + private static final String SAMPLE_OVERRIDE = + "{" + + "\"policy_id\": \"pol_123\"," + + "\"action\": \"warn\"," + + "\"reason\": \"Testing override\"," + + "\"created_at\": \"2025-01-01T00:00:00Z\"," + + "\"active\": true" + + "}"; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + axonflow = AxonFlow.create(AxonFlowConfig.builder() + .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) + .build()); + } + + // ======================================================================== + // Static Policy Tests + // ======================================================================== + + @Nested + @DisplayName("Static Policies") + class StaticPolicies { + + @Test + @DisplayName("listStaticPolicies should return policies") + void listStaticPoliciesShouldReturnPolicies() { + stubFor(get(urlPathEqualTo("/api/v1/static-policies")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"policies\": [" + SAMPLE_STATIC_POLICY + "]}"))); + + List policies = axonflow.listStaticPolicies(); + + assertThat(policies).hasSize(1); + assertThat(policies.get(0).getId()).isEqualTo("pol_123"); + assertThat(policies.get(0).getName()).isEqualTo("Block SQL Injection"); + } + + @Test + @DisplayName("listStaticPolicies with filters should include query params") + void listStaticPoliciesWithFiltersShouldIncludeQueryParams() { + stubFor(get(urlPathEqualTo("/api/v1/static-policies")) + .withQueryParam("category", equalTo("security-sqli")) + .withQueryParam("tier", equalTo("system")) + .withQueryParam("enabled", equalTo("true")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"policies\": [" + SAMPLE_STATIC_POLICY + "]}"))); + + ListStaticPoliciesOptions options = ListStaticPoliciesOptions.builder() + .category(PolicyCategory.SECURITY_SQLI) + .tier(PolicyTier.SYSTEM) + .enabled(true) + .build(); + + List policies = axonflow.listStaticPolicies(options); + + assertThat(policies).hasSize(1); + } + + @Test + @DisplayName("getStaticPolicy should return policy by ID") + void getStaticPolicyShouldReturnPolicyById() { + stubFor(get(urlEqualTo("/api/v1/static-policies/pol_123")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_STATIC_POLICY))); + + StaticPolicy policy = axonflow.getStaticPolicy("pol_123"); + + assertThat(policy.getId()).isEqualTo("pol_123"); + assertThat(policy.getCategory()).isEqualTo(PolicyCategory.SECURITY_SQLI); + } + + @Test + @DisplayName("getStaticPolicy should require non-null policyId") + void getStaticPolicyShouldRequirePolicyId() { + assertThatThrownBy(() -> axonflow.getStaticPolicy(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("createStaticPolicy should create and return policy") + void createStaticPolicyShouldCreateAndReturnPolicy() { + stubFor(post(urlEqualTo("/api/v1/static-policies")) + .willReturn(aResponse() + .withStatus(201) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_STATIC_POLICY))); + + CreateStaticPolicyRequest request = CreateStaticPolicyRequest.builder() + .name("Block SQL Injection") + .category(PolicyCategory.SECURITY_SQLI) + .pattern("(?i)(union\\\\s+select|drop\\\\s+table)") + .severity(PolicySeverity.CRITICAL) + .build(); + + StaticPolicy policy = axonflow.createStaticPolicy(request); + + assertThat(policy.getId()).isEqualTo("pol_123"); + + verify(postRequestedFor(urlEqualTo("/api/v1/static-policies")) + .withHeader("Content-Type", containing("application/json"))); + } + + @Test + @DisplayName("createStaticPolicy should require non-null request") + void createStaticPolicyShouldRequireRequest() { + assertThatThrownBy(() -> axonflow.createStaticPolicy(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("updateStaticPolicy should update and return policy") + void updateStaticPolicyShouldUpdateAndReturnPolicy() { + String updatedPolicy = SAMPLE_STATIC_POLICY.replace("\"severity\": \"critical\"", "\"severity\": \"high\""); + stubFor(put(urlEqualTo("/api/v1/static-policies/pol_123")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(updatedPolicy))); + + UpdateStaticPolicyRequest request = UpdateStaticPolicyRequest.builder() + .severity(PolicySeverity.HIGH) + .build(); + + StaticPolicy policy = axonflow.updateStaticPolicy("pol_123", request); + + assertThat(policy.getSeverity()).isEqualTo(PolicySeverity.HIGH); + + verify(putRequestedFor(urlEqualTo("/api/v1/static-policies/pol_123"))); + } + + @Test + @DisplayName("deleteStaticPolicy should delete policy") + void deleteStaticPolicyShouldDeletePolicy() { + stubFor(delete(urlEqualTo("/api/v1/static-policies/pol_123")) + .willReturn(aResponse() + .withStatus(204))); + + axonflow.deleteStaticPolicy("pol_123"); + + verify(deleteRequestedFor(urlEqualTo("/api/v1/static-policies/pol_123"))); + } + + @Test + @DisplayName("deleteStaticPolicy should require non-null policyId") + void deleteStaticPolicyShouldRequirePolicyId() { + assertThatThrownBy(() -> axonflow.deleteStaticPolicy(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("toggleStaticPolicy should toggle enabled status") + void toggleStaticPolicyShouldToggleEnabledStatus() { + String toggledPolicy = SAMPLE_STATIC_POLICY.replace("\"enabled\": true", "\"enabled\": false"); + stubFor(patch(urlEqualTo("/api/v1/static-policies/pol_123")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(toggledPolicy))); + + StaticPolicy policy = axonflow.toggleStaticPolicy("pol_123", false); + + assertThat(policy.isEnabled()).isFalse(); + + verify(patchRequestedFor(urlEqualTo("/api/v1/static-policies/pol_123")) + .withRequestBody(containing("\"enabled\":false"))); + } + + @Test + @DisplayName("getEffectiveStaticPolicies should return effective policies") + void getEffectiveStaticPoliciesShouldReturnEffectivePolicies() { + stubFor(get(urlPathEqualTo("/api/v1/static-policies/effective")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"static\": [" + SAMPLE_STATIC_POLICY + "], \"dynamic\": []}"))); + + List policies = axonflow.getEffectiveStaticPolicies(); + + assertThat(policies).hasSize(1); + } + + @Test + @DisplayName("testPattern should test pattern against inputs") + void testPatternShouldTestPatternAgainstInputs() { + String responseBody = + "{" + + "\"valid\": true," + + "\"matches\": [" + + "{\"input\": \"SELECT * FROM users\", \"matched\": true}," + + "{\"input\": \"Hello world\", \"matched\": false}" + + "]" + + "}"; + + stubFor(post(urlEqualTo("/api/v1/static-policies/test")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseBody))); + + TestPatternResult result = axonflow.testPattern( + "(?i)select", + Arrays.asList("SELECT * FROM users", "Hello world") + ); + + assertThat(result.isValid()).isTrue(); + assertThat(result.getMatches()).hasSize(2); + assertThat(result.getMatches().get(0).isMatched()).isTrue(); + assertThat(result.getMatches().get(1).isMatched()).isFalse(); + } + + @Test + @DisplayName("testPattern should require non-null parameters") + void testPatternShouldRequireParameters() { + assertThatThrownBy(() -> axonflow.testPattern(null, Arrays.asList("test"))) + .isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> axonflow.testPattern("pattern", null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("getStaticPolicyVersions should return version history") + void getStaticPolicyVersionsShouldReturnVersionHistory() { + String responseBody = + "[" + + "{\"version\": 2, \"changed_at\": \"2025-01-02T00:00:00Z\", \"change_type\": \"updated\"}," + + "{\"version\": 1, \"changed_at\": \"2025-01-01T00:00:00Z\", \"change_type\": \"created\"}" + + "]"; + + stubFor(get(urlEqualTo("/api/v1/static-policies/pol_123/versions")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseBody))); + + List versions = axonflow.getStaticPolicyVersions("pol_123"); + + assertThat(versions).hasSize(2); + assertThat(versions.get(0).getVersion()).isEqualTo(2); + } + } + + // ======================================================================== + // Policy Override Tests + // ======================================================================== + + @Nested + @DisplayName("Policy Overrides") + class PolicyOverrides { + + @Test + @DisplayName("createPolicyOverride should create override") + void createPolicyOverrideShouldCreateOverride() { + stubFor(post(urlEqualTo("/api/v1/static-policies/pol_123/override")) + .willReturn(aResponse() + .withStatus(201) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_OVERRIDE))); + + CreatePolicyOverrideRequest request = CreatePolicyOverrideRequest.builder() + .action(OverrideAction.WARN) + .reason("Testing override") + .build(); + + PolicyOverride override = axonflow.createPolicyOverride("pol_123", request); + + assertThat(override.getAction()).isEqualTo(OverrideAction.WARN); + assertThat(override.getReason()).isEqualTo("Testing override"); + + verify(postRequestedFor(urlEqualTo("/api/v1/static-policies/pol_123/override"))); + } + + @Test + @DisplayName("createPolicyOverride should require non-null parameters") + void createPolicyOverrideShouldRequireParameters() { + CreatePolicyOverrideRequest request = CreatePolicyOverrideRequest.builder() + .action(OverrideAction.WARN) + .build(); + + assertThatThrownBy(() -> axonflow.createPolicyOverride(null, request)) + .isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> axonflow.createPolicyOverride("pol_123", null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("deletePolicyOverride should delete override") + void deletePolicyOverrideShouldDeleteOverride() { + stubFor(delete(urlEqualTo("/api/v1/static-policies/pol_123/override")) + .willReturn(aResponse() + .withStatus(204))); + + axonflow.deletePolicyOverride("pol_123"); + + verify(deleteRequestedFor(urlEqualTo("/api/v1/static-policies/pol_123/override"))); + } + + @Test + @DisplayName("deletePolicyOverride should require non-null policyId") + void deletePolicyOverrideShouldRequirePolicyId() { + assertThatThrownBy(() -> axonflow.deletePolicyOverride(null)) + .isInstanceOf(NullPointerException.class); + } + } + + // ======================================================================== + // Dynamic Policy Tests + // ======================================================================== + + @Nested + @DisplayName("Dynamic Policies") + class DynamicPolicies { + + @Test + @DisplayName("listDynamicPolicies should return policies") + void listDynamicPoliciesShouldReturnPolicies() { + stubFor(get(urlPathEqualTo("/api/v1/policies")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[" + SAMPLE_DYNAMIC_POLICY + "]"))); + + List policies = axonflow.listDynamicPolicies(); + + assertThat(policies).hasSize(1); + assertThat(policies.get(0).getId()).isEqualTo("dpol_456"); + assertThat(policies.get(0).getName()).isEqualTo("Rate Limit API"); + } + + @Test + @DisplayName("listDynamicPolicies with filters should include query params") + void listDynamicPoliciesWithFiltersShouldIncludeQueryParams() { + stubFor(get(urlPathEqualTo("/api/v1/policies")) + .withQueryParam("category", equalTo("dynamic-cost")) + .withQueryParam("enabled", equalTo("true")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[" + SAMPLE_DYNAMIC_POLICY + "]"))); + + ListDynamicPoliciesOptions options = ListDynamicPoliciesOptions.builder() + .category(PolicyCategory.DYNAMIC_COST) + .enabled(true) + .build(); + + axonflow.listDynamicPolicies(options); + + verify(getRequestedFor(urlPathEqualTo("/api/v1/policies")) + .withQueryParam("category", equalTo("dynamic-cost"))); + } + + @Test + @DisplayName("getDynamicPolicy should return policy by ID") + void getDynamicPolicyShouldReturnPolicyById() { + stubFor(get(urlEqualTo("/api/v1/policies/dpol_456")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_DYNAMIC_POLICY))); + + DynamicPolicy policy = axonflow.getDynamicPolicy("dpol_456"); + + assertThat(policy.getId()).isEqualTo("dpol_456"); + assertThat(policy.getConfig().getType()).isEqualTo("rate-limit"); + } + + @Test + @DisplayName("getDynamicPolicy should require non-null policyId") + void getDynamicPolicyShouldRequirePolicyId() { + assertThatThrownBy(() -> axonflow.getDynamicPolicy(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("createDynamicPolicy should create and return policy") + void createDynamicPolicyShouldCreateAndReturnPolicy() { + stubFor(post(urlEqualTo("/api/v1/policies")) + .willReturn(aResponse() + .withStatus(201) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_DYNAMIC_POLICY))); + + CreateDynamicPolicyRequest request = CreateDynamicPolicyRequest.builder() + .name("Rate Limit API") + .category(PolicyCategory.DYNAMIC_COST) + .config(DynamicPolicyConfig.builder() + .type("rate-limit") + .rules(Map.of("maxRequestsPerMinute", 100)) + .action(PolicyAction.BLOCK) + .build()) + .build(); + + DynamicPolicy policy = axonflow.createDynamicPolicy(request); + + assertThat(policy.getId()).isEqualTo("dpol_456"); + + verify(postRequestedFor(urlEqualTo("/api/v1/policies"))); + } + + @Test + @DisplayName("createDynamicPolicy should require non-null request") + void createDynamicPolicyShouldRequireRequest() { + assertThatThrownBy(() -> axonflow.createDynamicPolicy(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("updateDynamicPolicy should update and return policy") + void updateDynamicPolicyShouldUpdateAndReturnPolicy() { + stubFor(put(urlEqualTo("/api/v1/policies/dpol_456")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_DYNAMIC_POLICY))); + + UpdateDynamicPolicyRequest request = UpdateDynamicPolicyRequest.builder() + .config(DynamicPolicyConfig.builder() + .type("rate-limit") + .rules(Map.of("maxRequestsPerMinute", 200)) + .action(PolicyAction.BLOCK) + .build()) + .build(); + + DynamicPolicy policy = axonflow.updateDynamicPolicy("dpol_456", request); + + assertThat(policy).isNotNull(); + + verify(putRequestedFor(urlEqualTo("/api/v1/policies/dpol_456"))); + } + + @Test + @DisplayName("deleteDynamicPolicy should delete policy") + void deleteDynamicPolicyShouldDeletePolicy() { + stubFor(delete(urlEqualTo("/api/v1/policies/dpol_456")) + .willReturn(aResponse() + .withStatus(204))); + + axonflow.deleteDynamicPolicy("dpol_456"); + + verify(deleteRequestedFor(urlEqualTo("/api/v1/policies/dpol_456"))); + } + + @Test + @DisplayName("deleteDynamicPolicy should require non-null policyId") + void deleteDynamicPolicyShouldRequirePolicyId() { + assertThatThrownBy(() -> axonflow.deleteDynamicPolicy(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("toggleDynamicPolicy should toggle enabled status") + void toggleDynamicPolicyShouldToggleEnabledStatus() { + String toggledPolicy = SAMPLE_DYNAMIC_POLICY.replace("\"enabled\": true", "\"enabled\": false"); + stubFor(patch(urlEqualTo("/api/v1/policies/dpol_456")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(toggledPolicy))); + + DynamicPolicy policy = axonflow.toggleDynamicPolicy("dpol_456", false); + + assertThat(policy.isEnabled()).isFalse(); + + verify(patchRequestedFor(urlEqualTo("/api/v1/policies/dpol_456")) + .withRequestBody(containing("\"enabled\":false"))); + } + + @Test + @DisplayName("getEffectiveDynamicPolicies should return effective policies") + void getEffectiveDynamicPoliciesShouldReturnEffectivePolicies() { + stubFor(get(urlPathEqualTo("/api/v1/policies/effective")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[" + SAMPLE_DYNAMIC_POLICY + "]"))); + + List policies = axonflow.getEffectiveDynamicPolicies(); + + assertThat(policies).hasSize(1); + } + } + + // ======================================================================== + // Type Validation Tests + // ======================================================================== + + @Nested + @DisplayName("Policy Types") + class PolicyTypes { + + @Test + @DisplayName("CreateStaticPolicyRequest should have defaults") + void createStaticPolicyRequestShouldHaveDefaults() { + CreateStaticPolicyRequest request = CreateStaticPolicyRequest.builder() + .name("Test Policy") + .category(PolicyCategory.PII_GLOBAL) + .pattern("\\d{3}-\\d{2}-\\d{4}") + .build(); + + assertThat(request.getName()).isEqualTo("Test Policy"); + assertThat(request.getCategory()).isEqualTo(PolicyCategory.PII_GLOBAL); + assertThat(request.isEnabled()).isTrue(); + assertThat(request.getSeverity()).isEqualTo(PolicySeverity.MEDIUM); + assertThat(request.getAction()).isEqualTo(PolicyAction.BLOCK); + } + + @Test + @DisplayName("CreateDynamicPolicyRequest should have defaults") + void createDynamicPolicyRequestShouldHaveDefaults() { + CreateDynamicPolicyRequest request = CreateDynamicPolicyRequest.builder() + .name("Test Dynamic") + .category(PolicyCategory.DYNAMIC_RISK) + .config(DynamicPolicyConfig.builder() + .type("custom") + .rules(Map.of("threshold", 0.8)) + .action(PolicyAction.WARN) + .build()) + .build(); + + assertThat(request.getName()).isEqualTo("Test Dynamic"); + assertThat(request.isEnabled()).isTrue(); + assertThat(request.getConfig().getAction()).isEqualTo(PolicyAction.WARN); + } + + @Test + @DisplayName("ListStaticPoliciesOptions should build correctly") + void listStaticPoliciesOptionsShouldBuildCorrectly() { + ListStaticPoliciesOptions options = ListStaticPoliciesOptions.builder() + .category(PolicyCategory.SECURITY_SQLI) + .tier(PolicyTier.SYSTEM) + .enabled(true) + .limit(10) + .offset(0) + .sortBy("name") + .sortOrder("asc") + .search("sql") + .build(); + + assertThat(options.getCategory()).isEqualTo(PolicyCategory.SECURITY_SQLI); + assertThat(options.getTier()).isEqualTo(PolicyTier.SYSTEM); + assertThat(options.getEnabled()).isTrue(); + assertThat(options.getLimit()).isEqualTo(10); + assertThat(options.getOffset()).isEqualTo(0); + assertThat(options.getSortBy()).isEqualTo("name"); + assertThat(options.getSortOrder()).isEqualTo("asc"); + assertThat(options.getSearch()).isEqualTo("sql"); + } + + @Test + @DisplayName("PolicyCategory enum values should serialize correctly") + void policyCategoryEnumValuesShouldSerializeCorrectly() { + assertThat(PolicyCategory.SECURITY_SQLI.getValue()).isEqualTo("security-sqli"); + assertThat(PolicyCategory.PII_GLOBAL.getValue()).isEqualTo("pii-global"); + assertThat(PolicyCategory.DYNAMIC_COST.getValue()).isEqualTo("dynamic-cost"); + assertThat(PolicyCategory.CUSTOM.getValue()).isEqualTo("custom"); + } + + @Test + @DisplayName("PolicyTier enum values should serialize correctly") + void policyTierEnumValuesShouldSerializeCorrectly() { + assertThat(PolicyTier.SYSTEM.getValue()).isEqualTo("system"); + assertThat(PolicyTier.ORGANIZATION.getValue()).isEqualTo("organization"); + assertThat(PolicyTier.TENANT.getValue()).isEqualTo("tenant"); + } + + @Test + @DisplayName("OverrideAction enum values should serialize correctly") + void overrideActionEnumValuesShouldSerializeCorrectly() { + assertThat(OverrideAction.BLOCK.getValue()).isEqualTo("block"); + assertThat(OverrideAction.WARN.getValue()).isEqualTo("warn"); + assertThat(OverrideAction.LOG.getValue()).isEqualTo("log"); + assertThat(OverrideAction.REDACT.getValue()).isEqualTo("redact"); + } + + @Test + @DisplayName("PolicyAction enum values should serialize correctly") + void policyActionEnumValuesShouldSerializeCorrectly() { + assertThat(PolicyAction.BLOCK.getValue()).isEqualTo("block"); + assertThat(PolicyAction.WARN.getValue()).isEqualTo("warn"); + assertThat(PolicyAction.LOG.getValue()).isEqualTo("log"); + assertThat(PolicyAction.REDACT.getValue()).isEqualTo("redact"); + assertThat(PolicyAction.ALLOW.getValue()).isEqualTo("allow"); + } + } +}